在函数内反应 useEffect 陈旧值

Posted

技术标签:

【中文标题】在函数内反应 useEffect 陈旧值【英文标题】:React useEffect stale value inside function 【发布时间】:2020-07-02 07:30:12 【问题描述】:

如何在以下上下文中更新函数executeSimulation 内的变量simulationOn 的值:

App this.state.simulationOn 通过外部代码更改 --> ... --> React 无状态组件 (Robot) 重新渲染 --> useEffect 使用新值调用钩子 --> executeSimulation IS未更新为 simulationOn 的新值。

    function Robot( simulationOn, alreadyActivated, robotCommands ) 

        useEffect(() => 
            function executeSimulation(index, givenCommmands) 
                index += 1;
                if (index > givenCommmands.length || !simulationOn) 
                    return;
                
                setTimeout(executeSimulation.bind(, index, givenCommmands), 1050);
            
            if (simulationOn && !alreadyActivated) 
                executeSimulation(1, robotCommands);
            
        , [simulationOn, alreadyActivated, robotCommands]);

    

在上面的示例中,simulationOn 永远不会更改为 false,即使使用更新的值调用 useEffect(我检查了 console.log)。我怀疑这是因为simulationOn 的新值从未传递到函数executeSimulation 的范围内,但我不知道如何在函数executeSimulation 内传递新的钩子值

【问题讨论】:

simulationOn 改变在哪里? 对不起,我只是把问题说得更清楚了! this.state.simulationOn 通过外部代码更改 App 类。 我真的不明白是哪一部分的问题?如果您想手动更改simulationOn,则必须将setSimulationOn 回调一直传递到要更改它的组件。如果即使 SimulationOn=false,模拟仍然出现问题,那么问题可能是因为您没有清除 setTimeout。 我不想手动更改simulationOn,这已经发生了。我试图让函数executeSimulation 在运行时知道对钩子变量(特别是simulationOn)所做的更改。编辑:尝试清除 setTimeout 但这又被忽略了。 为什么executeSimulation必须在useEffect的范围内?如果您希望它在每次超时时引用simulationOn 的当前值,您应该在组件的范围内声明它并直接引用该道具。 useEffect 有效地为该渲染创建了一个实例,并且单独渲染,不可能在其中传递任何新道具 - 它们将始终传递给一个新实例。 【参考方案1】:

executeSimulation 函数有一个陈旧的闭包,simulationOn 永远不会为真,这里是演示陈旧闭包的代码:

var component = test => 
  console.log('called Component with',test);
  setTimeout(
    () => console.log('test in callback:', test),
    20
  );

component(true);
coponent(false)

请注意,Robot 每次渲染时都会被调用,但 executeSimulation 是从先前的渲染运行的,它的闭包中有之前的 simulationOn 值(请参阅上面的陈旧闭包示例)

不要在executeSimulation 中检查simulationOn,而应该只在simulationOn 为true 时启动executeSimulation,并且在useEffect 的清理函数中使用clearTimeout:

const Component = ( simulation, steps, reset ) => 
  const [current, setCurrent] = React.useState(0);
  const continueRunning =
    current < steps.length - 1 && simulation;
  //if reset or steps changes then set current index to 0
  React.useEffect(() => setCurrent(0), [reset, steps]);
  React.useEffect(() => 
    let timer;
    function executeSimulation() 
      setCurrent(current => current + 1);
      //set timer for the cleanup to cancel it when simulation changes
      timer = setTimeout(executeSimulation, 1200);
    
    if (continueRunning) 
      timer = setTimeout(executeSimulation, 1200);
    
    return () => 
      clearTimeout(timer);
    ;
  , [continueRunning]);
  return (
    <React.Fragment>
      <h1>Step: steps[current]</h1>
      <h1>Simulation: simulation ? 'on' : 'off'</h1>
      <h1>Current index: current</h1>
    </React.Fragment>
  );
;
const App = () => 
  const randomArray = (length = 3, min = 1, max = 100) =>
    [...new Array(length)].map(
      () => Math.floor(Math.random() * (max - min)) + min
    );
  const [simulation, setSimulation] = React.useState(false);
  const [reset, setReset] = React.useState();
  const [steps, setSteps] = React.useState(randomArray());
  return (
    <div>
      <button onClick=() => setSimulation(s => !s)>
        simulation ? 'Pause' : 'Start' simulation
      </button>
      <button onClick=() => setReset()>reset</button>
      <button onClick=() => setSteps(randomArray())>
        new steps
      </button>
      <Component
        simulation=simulation
        reset=reset
        steps=steps
      />
      <div>Steps: JSON.stringify(steps)</div>
    </div>
  );
;
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

【讨论】:

是的,感谢您指出问题并提供代码!这不会解决问题,因为每次道具更改时都会触发清理功能,而不是仅在 simulationOn 更改时触发。 (我的依赖数组不仅仅是[simulationOn],而是更大)。我现在认为答案可能与***.com/questions/57481573/… 有关 @TheodorAmariucai 我已经更新了答案,App 组件能够启动和暂停模拟(通过模拟)、重置(通过重置)并创建新的模拟步骤(通过步骤)。您可以运行代码,看看它是否完成了您需要它做的事情(点击整页链接查看所有内容)【参考方案2】:

simulationOn 永远不会改变,因为父组件必须改变它。它通过一个属性传递给这个 Robot 组件。 我创建了一个沙箱示例来显示,如果您在父级中正确更改它,它将向下传播。 https://codesandbox.io/s/robot-i85lf.

这个机器人存在一些设计问题。似乎假设机器人可以通过将索引值作为实例变量来“记住”索引值。 React 不是这样工作的。此外,它假设 useEffect 将在一个参数更改时仅调用一次,这是不正确的。我们不知道 useEffect 会被调用多少次。 React 仅保证在其中一个依赖项发生更改时将调用它。但它可以被更频繁地调用。

我的例子显示了一个命令列表必须由父级保存,并且需要发送完整列表,所以哑机器人可以显示它执行的所有命令。

【讨论】:

以上是关于在函数内反应 useEffect 陈旧值的主要内容,如果未能解决你的问题,请参考以下文章

对 useEffect 挂钩内的多个 setState() 调用做出反应批量更新

无效的挂钩调用。钩子只能在反应函数组件内部使用...... useEffect,redux

使用自定义 Hook 事件处理反应陈旧状态

反应 useEffect 警告以放置缺少的依赖项。但是钩子中的依赖值发生了变化

为啥 PanResponder 不能使用 useEffect Hook?

反应useState不更新值