Home > Blockchain >  React setInterval in combination with useEffect creates behavior I don't understand. It require
React setInterval in combination with useEffect creates behavior I don't understand. It require

Time:05-18

I am Following along in a course and I do not understand the logical reason for the different behavior in the two code examples. I start with the working example then I highlight what breaks the code. There are solutions for this problem in similar questions but I haven't found a description or term for what is happening.

Below the setInterval works correctly and the timer function counts down per second as it should. There is a secs useState hook to keep track of seconds, and an inProgress useState.

  React.useEffect(() => {
        if (!inProgress) {
            clearInterval(interval.current)
        } else {
            interval.current = setInterval(() => {
                setSecs((secs) => {
                    const timeLeft = secs - 1;
                    return timeLeft
                })
            }, 1000)
        }
        return () => clearInterval(interval.current);
  }, [inProgress])

Below is the code excerpt that varies and breaks the code.

      interval.current = setInterval(() => {
            setSecs(secs - 1)
            console.log('interval fires')
            console.log(secs)
        }, 1000)

This code does not work, it counts down for one second and then nothing happens. When using the console.logs as seen above, 'interval fires' prints every second as does 'secs', however the secs state is not counting down its stays the same number.

    React.useEffect(() => {
    if (!inProgress) {
        clearInterval(interval.current)
    } else {
        interval.current = setInterval(() => {
            setSecs(secs - 1)
        }, 1000)
    }
    return () => clearInterval(interval.current);
}, [inProgress])

This seems like important behavior to commit to memory, yet I don't know how to categorize it. I'm looking for terms that describe this behavior or a good visual explanation of it.

The last setSec state ran 1000ms ago, and even If I setInterval to 10 seconds the problem persists. So this is not a render, mount, update issue based in time. I don't believe this can be described by closures either. I currently cannot logically understand this behavior.

CodePudding user response:

This is because when you render your component, each render has its own props and states. So states and props are never be changed in the render.
In the useEffect with emtpy dependency array, you know that it will run on the first render of the component. It means that when you setInterval in this situation, you set it with the props and states which is dependent on the first render, and it will not be changed even if setInterval is fired.
In other words, your setInteval function will be the closure and the states will be the free variables. You are referencing this stale variable in the closure.

React.useEffect(() => {
    if (!inProgress) {
        clearInterval(interval.current)
    } else {
        interval.current = setInterval(() => {
            setSecs(secs - 1) // state 'secs' are dependent on this render, free variable in this closure 
        }, 1000)
    }
    return () => clearInterval(interval.current);
}, [inProgress])

So if you want it to be reference recent state, you can use functional update form in setState. offical docs

setSecs(secs => secs - 1) // this form of setState guarantee that your state value is the recent one

But if you need to do something more than setState with state value, you should consider using useRef. Unlike states, ref will always give you the same object reference which is independent of your component. offical docs
You can learn more details in Dan Abramov's post which is really helpful to understand the logic behind the useEffect and states.

  • Related