javascriptreactjssleepstaleobjectstate

React useEffect stale value inside function


How would one update the value of variable simulationOn inside of function executeSimulation in the following context:

App this.state.simulationOn changes via external code --> ... --> React stateless component (Robot) rerendered --> useEffect hook called with new values --> executeSimulation IS NOT UPDATED with new value of 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]);

    }

In the example above, simulationOn never changes to false, even though useEffect is called with the updated value (I check with console.log). I suspect this is because the new value of simulationOn is never passed to the scope of function executeSimulation, but I don't know how to pass new hook values inside of function executeSimulation.


Solution

  • The executeSimulation function has a stale closure simulationOn will never be true, here is code demonstrating stale closure:

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

    Note that Robot is called every time it renders but executeSimulation runs from a previous render having it's previous simulationOn value in it's closure (see stale closure example above)

    Instead of checking simulationOn in executeSimulation you should just start executeSimulation when simulationOn is true and clearTimeout in the cleanup function of the useEffect:

    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>