React: Updated state not reflecting in recursive useCallback

Viewed 1408

I am incrementing foo in a useCallback hook called recursively. But, the incremented foo won't reflect in the callback.

const { useState, useCallback } = React;

const App = () => {
  const [foo, setFoo] = useState(0);

  const recursiveCallback = useCallback(() => {
    // always logs 0
    console.log(foo);

    // incrementing foo
    setFoo(foo + 1);

    setTimeout(() => {
      // recursive call
      recursiveCallback();
    }, 250);
  }, [foo]);

  return <button onClick={recursiveCallback}>Click Me</button>;
}

ReactDOM.render(<App />,document.getElementById('root'));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

3 Answers

You are having a closure on foo===0 value, both in the log and in the setter function, to fix it you need to use functional update and useEffect for logging:

const { useState, useCallback, useEffect } = React;

const App = () => {
  const [foo, setFoo] = useState(0);

  const recursiveCallback = useCallback(() => {
    setFoo(prev => prev + 1);

    setTimeout(() => {
      recursiveCallback();
    }, 250);
  }, [foo]);

  useEffect(() => {
    console.log(foo);
  }, [foo]);

  return <button onClick={recursiveCallback}>Click Me</button>;
}

ReactDOM.render( < App/> , document.getElementById('root'));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

  1. You should use useEffect() with [foo] dependency to see the changes after the state has been set.
  2. The function to update foo should be a Functional update

If the new state is computed using the previous state, you can pass a function to setState The function will receive the previous value, and return an updated value

    // incrementing foo
    setFoo(previousFoo => previousFoo + 1);

Note: what happened if you did not choose functional update?

Well, if you chose setFoo(foo + 1);, the next sate will be 2 only, after that, the state is not changed, useEffect with dependency foo is also not affected as well.

In short, if you need a previous state, you have to use functional update.

const { useState, useCallback, useEffect } = React;

const App = () => {
  const [foo, setFoo] = useState(0);

  const recursiveCallback = useCallback(() => {
    // incrementing foo
    //setFoo(foo + 1);
    setFoo(previousFoo => previousFoo + 1);

    setTimeout(() => {
      // recursive call
      recursiveCallback();
    }, 500);
  }, [foo]);

  useEffect(() => {
    console.log(foo);
  }, [foo]);
  
  return <button onClick={recursiveCallback}>Click Me</button>;
}

ReactDOM.render(<App />,document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

<div id="root"></div>

To access the latest value of foo, try changing this line

setFoo(foo + 1);

to

setFoo((foo) => foo + 1);

Also, passing foo as dependency will update the useCallback hook, so avoid accessing foo from outside and use following syntax

setState((state) => {
  const newState = { ...state }; 
  // Change State here
  return newState; // return new value
})
Related