React "interval" has always the same state

Viewed 1077

I am using react 16.10 with typescript. I have this code:

  const [state, setState] = useState<State>({
        test: 1
    });  

    //On component mount we start our interval
    useEffect(() => {
        const timerID = setInterval(timer, 5000); //every 5 seconds

        return function cleanup() {
            //When we leave component we stop the timer
            clearInterval(timerID);
        };
    }, []); 

   function timer() {
        if(state.test === 3){
            //HE WILL NEVER ENTER THIS CODE FUNCTION
        }

        setState({...state, test: 3}); // Next time we should have the value 3, BUT IT HAS NEVER THIS VALUE?!?!
    }

   return (
        <>
     <span>The output is {state.test}</span> //First it is showing 1, after 5 seconds 3. Working great
        </>
    );

I am changing the value of test to the number 3 in the interval "timer". setState is working fine. I can see the value in my component, seeing the number switching from 1 to 3.

But in the timer function the value is never changed. It has every time the default value of 1.

What I am doing wrong?

3 Answers

You need to add dependency to useEffect

 //On component mount we start our interval
  useEffect(() => {
      const timerID = setInterval(timer, 5000); //every 5 seconds

      return function cleanup() {
          //When we leave component we stop the timer
          clearInterval(timerID);
      };
  }, [state.test]); // <- here add dependency

Reason

Your effect function is called only once when component is mounted and it stored timer functions reference. now when you state changes your timer function is also updated outside but not inside of useEffect.

useEffect still uses old reference when state was 1 so inside it State always going to be 1 for that referred timer function

Now when you pass state.test as dependency. when state get changed your effect will updated and it now start using new timer function which has new state.

So now, you can have updated state in your timer function. and your condition can evaluate correctly.

if any doubts please comment.

You are not doing anything wrong, your useEffect() has a completely different value in memory and without knowing this behavior about useEffect() you have nothing in there telling useEffect() to stop looking at that old value and start looking at the new value. As Hardik wrote, your useEffect() is called only once, so you still have that old value that was originally called in there and useEffect has no idea that your timer has changed since. It will be referencing that original value forever.

What you can do is completely remove the empty array as the second argument and you will notice the difference in behavior.

Using a direct reference to the variable you are using in your state as suggested by Hardik seems to be the way to go.

So again, useEffect() is not being called a second time and as a result, nothing inside it is being ran again so it all in stale reference.

One of the tips the facebook team gives to mitigate this bug:

  • When you have a useEffect function that references a state, props, or context values, add them to your dependency list. In other words, if you have a props called trackId, you would want to do something like this:

     useEffect(() => {
       trackId
     }, [trackId]);
    

I see a couple of potential issues, first of all you need to be calling this.state and this.setState. I'd guess state is undefined but this.state won't be. You also don't need to spread your state in your setState function, this.setState({ test: 3}); is good enough, the setState function does this for you.

Secondly you need to update state for every change, it looks like you're only updating if the test value is 3, I'm surprised it's ever 3 with this implementation

Related