5

I have set the state to true before calling the setInterval function. But even though the useEffect hook is being triggered with the new value of the state, it's not being reflected in the setInterval function.

enter image description here Code sandbox here: https://jsfiddle.net/6e05tc2L/3/

let interval;
const Component = () => {
  React.useEffect(() => {
    console.log('State updated to', state);
  });
  const [state, setState] = React.useState(false);
  const on = () => {
    setState(true);
    interval = setInterval(() => {
      console.log(state);
    }, 1000);
    }
  const off = () => {
    setState(false);
    clearInterval(interval);
  }
  const toggle = () => state ? off() : on()

  return (<div>
    <button onClick={toggle}>Toggle State</button>
   </div>);
}

ReactDOM.render(
  <Component />,
  document.getElementById('container')
);

Shouldn't it be using the newer value of state once it's updated?

Ulysse BN
  • 10,116
  • 7
  • 54
  • 82
acesmndr
  • 8,137
  • 3
  • 23
  • 28
  • 2
    The function passed to `setInterval` is created just 1 time when `on` is called, and it closes over the value of `state` at the time it is created. Future renders call `React.useState` again and "see" a new `state`, but that function created when `on` was called is essentially stuck in the past: it closed over `state` when it was created and will never get a new value of `state`. – Ross Allen Aug 13 '19 at 17:16
  • 2
    So if you check out Abramov's `useInterval`, it calls `useEffect` on each render and updates a ref's `.current` with a new version of the callback that closed over the value of `state` for that render call. The function passed to the native `setInterval` never changes, but all that function does is call *another* function whose reference actually *is* updating on every render. – Ross Allen Aug 13 '19 at 17:28
  • @RossAllen So the callback function changes due to the use of useRef but the tick function remains the same. That's awesome but feels quite complex. Thanks! :) – acesmndr Aug 14 '19 at 04:08
  • 1
    Yup, exactly. That `tick` never changes, but in your own call of `useInterval` you are passing in a new function on every render. That function gets set to `savedCallback.current` and called by the permanent `tick`. – Ross Allen Aug 14 '19 at 12:09

4 Answers4

16

The values inside the function which you pass to useEffect are refreshed on every render, because useEffect uses a new definition of the function you pass to it.

But the function passed to setInterval is defined once and it closes over the old stale value of state. Which has not yet updated.

Closures are tricky with hooks, but the thing to realize is that useEffect creates a new function for each render and hence each time the function closes over a fresh state value.

The trick then is to call your setInterval related code inside a useEffect itself, which itself depends on the changing value of state

React.useEffect(() => {
  if(state) {

    interval = setInterval(() => {
      console.log(state);
    }, 1000);
  } else {
    clearInterval(interval);
  }

}, [state]);

Or, better, use a useInterval hook which takes care of these details for you.

Mukesh Soni
  • 6,646
  • 3
  • 30
  • 37
  • If we use local variables then it does use the updated value of the variable even if the variable is outside the scope of the function. How and why is closure different with hooked state than a variable? – acesmndr Aug 13 '19 at 14:57
  • 3
    Closure means closing over values outside the scope of the function. This discrepancy is not because of scope. It's because once a function closes over a value, it always references that variable. Which is what happened with the setInterval callback. It got defined once and got attached to whatever the value of state was. But the functions you pass to useEffect are thrown away on every render and a new one is created which then closes over the latest value in the variables. – Mukesh Soni Aug 13 '19 at 15:01
  • Accepted this answer because it shows a simple workaround to the issue. What I meant by variables is suppose we have a seperate counter variable declared globally then if we use setInterval to increment it then it does increment it without it being defined under useEffect hook. – acesmndr Aug 13 '19 at 15:11
  • 1
    But that's global. And stays in scope until the module exits or your program exits. `state` in your case get out of scope once the render function is done running. Only the functions defined inside that function have access to that variable through closure. – Mukesh Soni Aug 13 '19 at 15:14
  • userInterval is perfect, it even have cleanup – James Tan Dec 05 '20 at 07:03
5

setInterval always has access to the value of your component's first render because the function passed to setInterval closes around that value and is never redeclared. You can use a custom hook to fix this:

function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

That implementation and a thorough explanation of the mismatch between React Hooks and setInterval is from Making setInterval Declarative with React Hooks by Dan Abramov, one of the React contributors.

Ross Allen
  • 43,772
  • 14
  • 97
  • 95
  • Upvote for the article by Dan! But it doesn't explain why it does occur though but gives a great workaround to it. – acesmndr Aug 13 '19 at 15:03
0

If you want to reaload your component whenever your state change you should create your useEffect like this.

React.useEffect(() => {
    console.log('State updated to', state);
}, [state]);

The way you created is the same as componentDidMount() with an array as the second parameter it's like componentDidUpdate() with it's dependencies. So, your component will re-render whenever your state change.

To solve the infinity call of setTimeout you can do this where you create the function

React.useCallback(() => {
    setInterval(() => {
        console.log(state);
    }, 1000);
})

with this React will know that you want to create this function just once.

0

I'm not a ReactJS expert, but I guess the state you are logging is not refreshed since it is declared once and never refreshed. If React.useState(false) is the method that is giving you your state, you should use it in your interval function.

Here is an example of what I'm trying to explain:

const object = { value: false }


const notRefreshed = object.value // here we are copying the value

const interval = setInterval(() => {
  const refreshed = object.value // here we are using the reference to copy the latest value
  console.log("refreshed", refreshed)
  console.log("notRefreshed", notRefreshed)
}, 500)


setTimeout(() => object.value = true, 1600)

setTimeout(() => clearInterval(interval), 2600)
Ulysse BN
  • 10,116
  • 7
  • 54
  • 82