写过 React 的一定会有遇到过这个错误:

错误信息

准确来说这是一个警告,因为它看起来并不会影响程序的正常运行,如果不去处理的话也不会有什么严重的后果,所以大多数人就会选择视而不见... 😂

但是要真问题来为啥会这样,不仔细研究一下不是很好想明白,下面就来探讨下这个错误提示的根本原因在哪。

首先看一下错误的字面意思:

不能更新 React 的 state 在一个还没有挂载的组件上。

这是一个空操作,但它表明您的应用程序中存在内存泄漏。

要修复,请取消 useEffect 清理函数中的所有订阅和异步任务。

意思大概是说问题出在了 useEffect 上,首先我们先尝试着复现一下这个错误。

我们构建一个简单的应用,通过一个 select 组件选择不同的动物名称,然后根据选择的名字读取相应的数据并显示在下方。

Pets 组件:

const Pets: React.FC = () => {
  const [pets, setPets] = useState('');
  const [petsData, setPetsData] = useState({});
  const [loading, setLoading] = useState(false);

  const handleChange = ({ target }) => {
    setLoading(true);
    setPets(target.value);
  };

  useEffect(() => {
    if (pets) {
      getPets(pets).then(res => {
        setLoading(false);
        setPetsData(res);
      });
    }
  }, [pets]);

  return (
    <div>
      <select name="pets" id="pets" onChange={handleChange}>
        <option value="">Select a pet</option>
        <option value="cats">Cats</option>
        <option value="dogs">Dogs</option>
      </select>
      {loading && <Loading />}
      {petsData && <PetsCard petsData={petsData} />}
    </div>
  );
};

我们使用了 select 组件选择 cats 或是 dogs,当选择改变的时候我们设置当前的 pets 状态为选择的值,然后会触发 useEffect 更新对应的内容并显示。

其中 PetsCard 组件接受动物的数据并渲染:

const PetsCard = ({ petsData }) => {
  console.log(petsData);
  return (
    <div>
      <p>{petsData.name}</p>
      <p>{petsData.avatar}</p>
    </div>
  );
};

使用 Loading 组件展示加载中的状态:

const Loading: React.FC = () => {
  return <p>Loading...</p>;
};

数据部分我们选择本地模拟:

const petsData = {
  dogs: {
    name: 'dogs',
    avatar: '🐶'
  },
  cats: {
    name: 'cats',
    avatar: '🐱'
  }
};

export const getPets = (type: 'dogs' | 'cats') => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(petsData[type]);
    }, 1000);
  });
};

使用 setTimeout 模拟较慢的网络请求。

最后将他们挂载起来:

const App: React.FC = () => {
  return (
    <div>
      <Pets />
    </div>
  );
};

render(<App />, document.getElementById('root'));

运行起来后一切正常,我们选择不同的名字展示相应的数据。

重点来了,我们添加一个按钮控制这个组件的显示与隐藏,修改后:

const App: React.FC = () => {
  const [show, setShow] = useState(true);
  return (
    <div>
      <button onClick={() => setShow(!show)}>switch</button>
      {show && <Pets />}
    </div>
  );
};

render(<App />, document.getElementById('root'));

下面执行操作:更换一个名字,在 Loading 状态的时候点击按钮,组件消失,console 弹出警告

至此我们已经复现出了这个问题。

经过模拟我们发现了大概的问题所在,当我们更换名字的时候,组件开始请求对应的数据,然后我们点击了按钮,这个组件被卸载掉了,但是一秒后数据请求到了并 setState 更新数据,此时组件确实处于没有挂载的状态。

按照这个思路,要修复这个问题,只需要在这个组件被卸载的时候,取消掉所有的可能在未来更改状态的操作,而在 useEffect 这个 hooks 中,允许我们 return 一个函数,在这个函数中我们可以放入一些清理的逻辑,React 会在组件被卸载的时候执行这个函数,而在当前应用中我们只需要保证 组件卸载后不会再有 setState

我们按照这个思路修改下 Pets 组件:

修改一下 useEffect 方法:

useEffect(() => {
    let mount = true;
    if (pets) {
      getPets(pets).then(res => {
        if (mount) {
          setLoading(false);
          setPetsData(res);
        }
      });
    }
    return () => {
      console.log('hahahhaha');
      mount = false;
    };
  }, [pets]);

定一个一个 mount 变量控制组件是否被挂载,并有条件的进行更新状态。

这下那个警告就消失了😁。

总结,在 useEffect 中订阅某些状态或者执行某些异步任务,一定要注意是否需要在组件被卸载的时候清除这些操作。