SetInterval is not showing updated state
Asked Answered
V

4

6

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?

Vertebral answered 13/8, 2019 at 11:19 Comment(4)
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.Belief
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.Belief
@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! :)Vertebral
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.Belief
H
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.

Hardset answered 13/8, 2019 at 11:34 Comment(5)
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?Vertebral
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.Hardset
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.Vertebral
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.Hardset
userInterval is perfect, it even have cleanupRevanche
B
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.

Belief answered 13/8, 2019 at 11:43 Comment(1)
Upvote for the article by Dan! But it doesn't explain why it does occur though but gives a great workaround to it.Vertebral
C
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.

Curr answered 13/8, 2019 at 11:30 Comment(0)
F
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)
Fsh answered 13/8, 2019 at 11:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.