Warning: Can't perform a React state update on an unmounted component.

1,957 阅读2分钟

由于公司项目原因,从四年Vue老狗转React,在开发上遇到了许多稀奇古怪的问题,在此记录一下,方便复盘

直接上Error

react_devtools_backend.js:2540 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
    at RenderDeliveryStage (http://10.30.58.157:9000/Home.index_bundle.js:17651:81)

有道翻译:无法对已卸载的组件执行React状态更新。这是一个无操作,但它表明应用程序中存在内存泄漏。若要修复,请在useEffect清理函数中取消所有订阅和异步任务。

原因

组件卸载了,但是状态值(如:setState,useState)依然更改了,或者处于渲染中,直接卸载组件,可能会导致内存泄漏,因此React抛出异常,一般有异步处理的副作用导致的,比如定时器,lottie动画、网络请求副作用。

解决方案

需要在组件卸载的时候,将异步影响副作用处理掉,可以在useEffect中解决。

解决demo

1. 定时器

const [update, setUpdate] = useState(1);
 useEffect(() => {
    const creatInt = setInterval(() => {    //假设这里写了定时器来更新update
      setUpdate(c => c + 1);
    }, 2000);
return () => {
      clearInterval(creatInt);   // (重点)这里清除掉定时器  
    };
  }, []);

2. 网络请求1

 useEffect(() => {
   let isUnmount = false ;      //这里插入isUnmount
    const fetchDetail = async () => {
      const res = await getDetail(detailId);
      if (res.code === 0 && !isUnmount) { //加上判断isUnmount才去更新数据渲染组件
        setDetail(res.data);
      }
    };
    fetchDetail();
    // 或者在这里将 getDetail的ajax发给abort掉,见下面的栗子
    return () => isUnmount = true ;   //最好return一个isUnmount
  }, [detail]);

3. 网络请求2

function Component(props) {
  const [fetched, setFetched] = React.useState(false);
  React.useEffect(() => {
    const ac = new AbortController();
    Promise.all([
      fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
      fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
    ]).then(() => setFetched(true))
      .catch(ex => console.error(ex));
    return () => ac.abort(); // Abort both fetches on unmount
  }, []);
  return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms

4. lottie动画

/* 将apng转成canvas 并提供操作播放的方法以及属性 */
function ApngPlayer({ url, className = '', apngOptions = {}, ready }: IProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  useEffect(() => {
    const canvas = canvasRef.current as HTMLCanvasElement
    let player: Player
    ;(async function () {
      const { width, height } = await getImgSize(url)
      canvas.width = width
      canvas.height = height
      Object.assign(apngOptions, { width, height })
      player = await createApngPlayer(url, canvas, apngOptions)
      if (ready) ready(player)
    })()
    // 防止组件卸载,内存溢出
    return () => player?.stop()
  }, [apngOptions, ready, url])
  return <canvas className={className} ref={canvasRef} />
}

参考