写过 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
中订阅某些状态或者执行某些异步任务,一定要注意是否需要在组件被卸载的时候清除这些操作。