已卸载组件的 React 状态更新
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
最初发布在我的个人博客debugger.io上。
如果你是一名 React 开发者,那么你很可能至少遇到过一次这样的警告:
警告:无法对已卸载的组件执行 React 状态更新。此操作不会产生任何实际效果,但表明您的应用程序存在内存泄漏。要解决此问题,请在 useEffect 的清理函数中取消所有订阅和异步任务。
为了了解如何修复此警告,我们需要了解其发生的原因。我们需要以一致的方式重现此问题。
⚠️ 请注意,本文中使用了 React Hooks,如果您使用的是 React 类组件,您可能会在警告中看到对 componentWillUnmount 的引用,而不是 useEffect 清理函数。
重现该警告
👀 我已经在 GitHub 上上传了一个入门仓库,这样你就不用复制粘贴代码了。
你可以克隆并在本地运行,或者使用codesandbox.io的导入功能。
如果我们再仔细看一下这个警告,就会发现其中主要有两个部分在起作用:
- React 状态更新
- 未安装的组件
为了实现这些功能,我们将构建这个简单的下拉菜单,并采用异步数据获取方式。
州政府最新动态
function Pets() {
const [pets, dispatch] = useReducer(petsReducer, initialState);
const onChange = ({ target }) => {
dispatch({ type: "PET_SELECTED", payload: target.value });
};
useEffect(() => {
if (pets.selectedPet) {
dispatch({ type: "FETCH_PET" });
getPet(pets.selectedPet).then(data => {
dispatch({ type: "FETCH_PET_SUCCESS", payload: data });
});
} else {
dispatch({ type: "RESET" });
}
}, [pets.selectedPet]);
return (
<div>
<select value={pets.selectedPet} onChange={onChange}>
<option value="">Select a pet</option>
<option value="cats">Cats</option>
<option value="dogs">Dogs</option>
</select>
{pets.loading && <div>Loading...</div>}
{pets.petData && <Pet {...pets.petData} />}
</div>
);
}
这里是Pets组件,它使用useReducerhook 来存储一些状态。
让我们看看petsReducer初始状态:
const initialState = { loading: false, selectedPet: "", petData: null }
function petsReducer(state, action) {
switch (action.type) {
case "PET_SELECTED": {
return {
...state,
selectedPet: action.payload
};
}
case "FETCH_PET": {
return {
...state,
loading: true,
petData: null
};
}
case "FETCH_PET_SUCCESS": {
return {
...state,
loading: false,
petData: action.payload
};
}
case "RESET": {
return initialState;
}
default:
throw new Error( `Not supported action ${action.type}` );
}
}
如你所见,这里没有什么特别之处,只是一个简单的 reducer 来管理我们的状态。
该Pets组件还使用useEffecthook 来实现一些副作用,例如获取我们所选宠物的数据。我们调用getPet返回 a 的函数Promise,并将FETCH_PET_SUCCESS返回的数据作为有效负载分发 action 来更新我们的状态。
请注意,这getPet实际上并没有访问服务器端点,它只是一个模拟服务器调用的函数。它的代码如下所示:
const petsDB = {
dogs: { name: "Dogs", voice: "Woof!", avatar: "🐶" },
cats: { name: "Cats", voice: "Miauuu", avatar: "🐱" }
};
export function getPet(type) {
return new Promise(resolve => {
// simulate a fetch call
setTimeout(() => {
resolve(petsDB[type]);
}, 1000);
});
}
如你所见,它只不过是一个setTimeout内部Promise。
我们的App代码基本上就是渲染这个Pets组件:
function App() {
return (
<div>
<Pets />
</div>
);
}
好的,我们问题的第一部分已经完成,React state update现在我们需要创建第二部分An unmounted component。
卸载组件
使用状态和条件渲染可以相对容易地实现这一点,我们将在级别存储一个布尔标志,并使用切换按钮根据该标志App渲染组件。<Pets />
function App() {
const [showPets, setShowPets] = useState(true);
const toggle = () => {
setShowPets(state => !state);
};
return (
<div>
<button onClick={toggle}>{showPets ? "hide" : "show"}</button>
{showPets && <Pets />}
</div>
);
}
生殖
好的,现在我们已经满足了出现警告的两个条件,让我们来试一试。我们再看一下这个警告:
警告:无法对已卸载的组件执行 React 状态更新。此操作不会产生任何实际效果,但表明您的应用程序存在内存泄漏。要解决此问题,请在 useEffect 的清理函数中取消所有订阅和异步任务。
我们重点来看这一行:
已卸载组件的 React 状态更新
如果我们选择一只宠物,我们知道至少需要1 秒钟才能收到数据。数据返回后,我们会更新状态。如果在 1 秒钟之前(数据到达之前)getPet卸载组件,就会触发已卸载组件的更新。Pet
方法如下:
*如果 1 秒的延迟无法实现,请尝试增加函数timeOut中的延迟getPet。
好的,这是我们任务的第一部分,现在我们需要修复它。
修复
你可能会感到惊讶,但解决这个问题其实很简单。React 会提供清晰且非常有用的信息,并指导你找到解决方案:
要解决此问题,请在 useEffect 清理函数中取消所有订阅和异步任务。
嗯,我们可能并没有真正订阅任何东西,但我们确实有一个asynchronous tasks,还记得getPet异步函数吗?
function Pets() {
const [pets, dispatch] = useReducer(petsReducer, initialState);
const onChange = ({ target }) => {
dispatch({ type: "PET_SELECTED", payload: target.value });
};
useEffect(() => {
if (pets.selectedPet) {
dispatch({ type: "FETCH_PET" });
getPet(pets.selectedPet).then(data => {
dispatch({ type: "FETCH_PET_SUCCESS", payload: data });
});
} else {
dispatch({ type: "RESET" });
}
}, [pets.selectedPet]);
return (
<div>
<select value={pets.selectedPet} onChange={onChange}>
<option value="">Select a pet</option>
<option value="cats">Cats</option>
<option value="dogs">Dogs</option>
</select>
{pets.loading && <div>Loading...</div>}
{pets.petData && <Pet {...pets.petData} />}
</div>
);
}
所以基本上,如果组件尚未挂载,我们就需要在回调函数中不要更新状态。
function Pets() {
const [pets, dispatch] = useReducer(petsReducer, initialState);
const onChange = ({ target }) => {
dispatch({ type: "PET_SELECTED", payload: target.value });
};
useEffect(() => {
let mounted = true;
if (pets.selectedPet) {
dispatch({ type: "FETCH_PET" });
getPet(pets.selectedPet).then(data => {
if(mounted){
dispatch({ type: "FETCH_PET_SUCCESS", payload: data });
}
});
} else {
dispatch({ type: "RESET" });
}
return () => mounted = false;
}, [pets.selectedPet]);
return (
<div>
<select value={pets.selectedPet} onChange={onChange}>
<option value="">Select a pet</option>
<option value="cats">Cats</option>
<option value="dogs">Dogs</option>
</select>
{pets.loading && <div>Loading...</div>}
{pets.petData && <Pet {...pets.petData} />}
</div>
);
}
每次执行 effect 时,我们都会设置一个局部变量,mounted并true在 effect 的清理函数中将其设置为 false(正如 React 建议的那样)。最重要的是,只有当该变量的值为 false 时true,我们才会更新 state。也就是说,如果组件已卸载(即变量已设置为 false),则不会进入该if代码块。
额外提示
我们在作用域内设置了一个局部变量useEffect,如果想在另一个作用域内重用此变量,useEffect可以使用 ` useRefis_preRendering_state`,这是一种组件的非渲染状态。
例如:
function Pets() {
const [pets, dispatch] = useReducer(petsReducer, initialState);
const isMountedRef = useRef(null);
const onChange = ({ target }) => {
dispatch({ type: "PET_SELECTED", payload: target.value });
};
useEffect(() => {
isMountedRef.current = true;
if (pets.selectedPet) {
dispatch({ type: "FETCH_PET" });
getPet(pets.selectedPet).then(data => {
if(isMountedRef.current){
dispatch({ type: "FETCH_PET_SUCCESS", payload: data });
}
});
} else {
dispatch({ type: "RESET" });
}
return () => isMountedRef.current = false;
}, [pets.selectedPet]);
useEffect(() => {
// we can access isMountedRef.current here as well
})
return (
<div>
<select value={pets.selectedPet} onChange={onChange}>
<option value="">Select a pet</option>
<option value="cats">Cats</option>
<option value="dogs">Dogs</option>
</select>
{pets.loading && <div>Loading...</div>}
{pets.petData && <Pet {...pets.petData} />}
</div>
);
}
Hooks 的妙处在于,我们可以将这部分逻辑提取到一个自定义 Hook 中,并在组件间复用。一种可能的实现方式如下:
function useIsMountedRef(){
const isMountedRef = useRef(null);
useEffect(() => {
isMountedRef.current = true;
return () => isMountedRef.current = false;
});
return isMountedRef;
}
function Pets() {
const [pets, dispatch] = useReducer(petsReducer, initialState);
const isMountedRef = useIsMountedRef();
const onChange = ({ target }) => {
dispatch({ type: "PET_SELECTED", payload: target.value });
};
useEffect(() => {
if (pets.selectedPet) {
dispatch({ type: "FETCH_PET" });
getPet(pets.selectedPet).then(data => {
if(isMountedRef.current){
dispatch({ type: "FETCH_PET_SUCCESS", payload: data });
}
});
} else {
dispatch({ type: "RESET" });
}
}, [pets.selectedPet, isMountedRef]);
return (
<div>
<select value={pets.selectedPet} onChange={onChange}>
<option value="">Select a pet</option>
<option value="cats">Cats</option>
<option value="dogs">Dogs</option>
</select>
{pets.loading && <div>Loading...</div>}
{pets.petData && <Pet {...pets.petData} />}
</div>
);
}
自定义使用效果
如果我们想让我们的钩子更加疯狂,我们可以创建自己的自定义钩子useEffect(或钩子useLayoutEffect),它将为我们提供效果的“当前状态”:
function useAbortableEffect(effect, dependencies) {
const status = {}; // mutable status object
useEffect(() => {
status.aborted = false;
// pass the mutable object to the effect callback
// store the returned value for cleanup
const cleanUpFn = effect(status);
return () => {
// mutate the object to signal the consumer
// this effect is cleaning up
status.aborted = true;
if (typeof cleanUpFn === "function") {
// run the cleanup function
cleanUpFn();
}
};
}, [...dependencies]);
}
我们将在Pet组件中这样使用它:
useAbortableEffect((status) => {
if (pets.selectedPet) {
dispatch({ type: "FETCH_PET" });
getPet(pets.selectedPet).then(data => {
if(!status.aborted){
dispatch({ type: "FETCH_PET_SUCCESS", payload: data });
}
});
} else {
dispatch({ type: "RESET" });
}
}, [pets.selectedPet]);
请注意,我们的自定义 effect 回调现在接受一个status参数,该参数是一个包含aborted布尔属性的对象。如果该属性设置为 true true,则表示我们的 effect 已被清理并重新运行(这意味着我们的依赖项已更改或组件已卸载)。
我挺喜欢这种模式的,希望 ReactuseEffect能默认支持这种行为。我已经为此在 React 代码库里创建了一个 RFC,欢迎大家评论或改进。
总结
我们已经看到,一个简单的组件如果执行异步状态更新,就可能导致这种常见的警告。想想你所有类似的组件。务必在执行状态更新之前检查组件是否已成功挂载。
希望这篇文章对你有帮助。如果你有不同的方法或建议,欢迎告诉我,你可以在推特上或私信我@ sag1v。🤓
更多文章请访问debuggr.io
文章来源:https://dev.to/sag1v/react-state-update-on-an-unmounted-component-1bi1



