useEffect清理函数未按预期顺序更新状态问题排查
问题与解决方案:子组件卸载时的状态计数异常
问题场景
希望子组件从DOM卸载时,通过useContext钩子更新父组件的状态值,但无法理解useEffect钩子返回的清理函数的调用顺序。
需求明确:每个开关组件能修改处于"开启"状态的开关计数,确保计数准确;点击"移除所有开关"按钮将它们从DOM移除时,也能正确更新计数。但实际移除DOM时的状态更新未按预期顺序执行,最终计数不准确。
操作步骤:添加多个开关,开启其中几个,点击移除所有开关,会发现父组件中的计数未被正确设置。在useEffect返回的清理函数中添加console.log后,日志显示计数似乎被正确设置,但顺序存在问题。
原示例代码:
import React, { useState, createContext, useContext, useEffect } from 'react'; import './style.css'; const SwitchContext = createContext(); const ChildComponent = () => { const [switchOn, setSwitchOn] = useState(false); const { numberOfSwitchesOn, setNumberOfSwitchesOn } = useContext(SwitchContext); useEffect(() => { return () => { if (switchOn) { console.log( `Setting number of switches on to: ${numberOfSwitchesOn - 1}` ); setNumberOfSwitchesOn(numberOfSwitchesOn - 1); } }; }, [switchOn]); const toggleSwitch = () => { if (switchOn) { // We are turning off this switch... setNumberOfSwitchesOn(numberOfSwitchesOn - 1); } else { // We are turning it on... setNumberOfSwitchesOn(numberOfSwitchesOn + 1); } setSwitchOn(!switchOn); }; return ( <button onClick={() => toggleSwitch()}>{switchOn ? 'on' : 'off'}</button> ); }; export default function App() { const [numberOfSwitches, setNumberOfSwitches] = useState(1); const [numberOfSwitchesOn, setNumberOfSwitchesOn] = useState(0); let switches = []; for (let i = 0; i < numberOfSwitches; i++) { switches.push(<ChildComponent key={`switch-${i}`} />); } return ( <SwitchContext.Provider value={{ numberOfSwitchesOn, setNumberOfSwitchesOn, }} > <div> <button onClick={() => setNumberOfSwitches(numberOfSwitches + 1)}> Add switch </button> <button onClick={() => setNumberOfSwitches(0)}> Remove all switches </button> <p>Number of switches on: {numberOfSwitchesOn}</p> {switches} </div> </SwitchContext.Provider> ); }
原因分析
问题核心是闭包陷阱:
- useEffect的清理函数会捕获组件渲染时的
numberOfSwitchesOn值,而非卸载时的最新值。 - 当多个子组件卸载时,每个清理函数都持有卸载前某次渲染的旧状态值,导致更新时互相覆盖。比如当前计数是3,两个开启的子组件卸载时,各自用旧值3计算出2,最终计数变成2而非预期的1。
修复方案
改用函数式状态更新,让React自动传入最新的状态值,避开闭包陷阱:
- 将所有
setNumberOfSwitchesOn的调用改为函数式形式:setNumberOfSwitchesOn(prev => prev ± 1),确保每次更新基于最新状态。 - 调整useEffect的依赖数组,显式声明所有用到的外部变量(符合React Hooks规范)。
修复后的完整代码
import React, { useState, createContext, useContext, useEffect } from 'react'; import './style.css'; const SwitchContext = createContext(); const ChildComponent = () => { const [switchOn, setSwitchOn] = useState(false); const { setNumberOfSwitchesOn } = useContext(SwitchContext); useEffect(() => { return () => { if (switchOn) { setNumberOfSwitchesOn(prev => prev - 1); } }; }, [switchOn, setNumberOfSwitchesOn]); const toggleSwitch = () => { // 函数式更新,基于最新状态值修改计数 setNumberOfSwitchesOn(prev => switchOn ? prev - 1 : prev + 1); setSwitchOn(!switchOn); }; return ( <button onClick={toggleSwitch}>{switchOn ? 'on' : 'off'}</button> ); }; export default function App() { const [numberOfSwitches, setNumberOfSwitches] = useState(1); const [numberOfSwitchesOn, setNumberOfSwitchesOn] = useState(0); // 简化组件数组生成方式 const switches = Array.from({ length: numberOfSwitches }, (_, i) => ( <ChildComponent key={`switch-${i}`} /> )); return ( <SwitchContext.Provider value={{ numberOfSwitchesOn, setNumberOfSwitchesOn, }} > <div> <button onClick={() => setNumberOfSwitches(prev => prev + 1)}> Add switch </button> <button onClick={() => setNumberOfSwitches(0)}> Remove all switches </button> <p>Number of switches on: {numberOfSwitchesOn}</p> {switches} </div> </SwitchContext.Provider> ); }
关键修复点说明
- 函数式状态更新:彻底解决闭包导致的旧值引用问题,每次更新都基于React维护的最新状态值。
- 依赖数组规范:显式声明useEffect依赖的所有变量,避免潜在的不一致问题(
setNumberOfSwitchesOn是React稳定的函数,不会触发不必要的重渲染)。 - 简化代码:用
Array.from替代for循环生成组件数组,代码更简洁可读。
内容的提问来源于stack exchange,提问作者Gus Murphy
相关产品推荐
相关产品推荐

