You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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自动传入最新的状态值,避开闭包陷阱:

  1. 将所有setNumberOfSwitchesOn的调用改为函数式形式:setNumberOfSwitchesOn(prev => prev ± 1),确保每次更新基于最新状态。
  2. 调整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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 01:05:18