skip to Main Content
"use client"
import React, { useEffect } from 'react'
import { useState } from 'react';
export default function page() {
    const [count, setCount] = useState(0);
    const handleClick = () => {
        setCount((count)=>count + 1);
    };
    function checkVal(){
        console.log(count)
    }
    useEffect(()=>{
        setInterval(checkVal,1000)
    }
    ,[])
    return (
    <div>
        <button onClick={handleClick}>{count}</button>
        <button onClick={checkVal}>clickmetocheck</button>
    </div>
  )
}

I have a following code which checks the value of count when

  1. checkVal is called through setInterval
  2. checkVal is called through onClick event

Since these two condition is calling the same function checkVal(), I was expecting console to print the same number. However, checkVal through setInterval was only printing the default value even when the value was updated through handleClick(). And checkVal through onclick event was correctly printing out count.

Same code, different behaviour. Why does this happen?

2

Answers


  1. Same code, different behaviour. Why does this happen?

    The same code when called from different contexts may behave differently.

    Context 1 : onClcik event

    <button onClick={checkVal}>clickmetocheck</button>
    

    Context 2 : useEffect event

    useEffect(()=>{
            setInterval(checkVal,1000)
        }
    

    The most significant similarity in the above two contexts is, both are events, and events will have visibility of state with respect to latest render. Therefore the value of the state count visible in both contexts is the same.

    However, Context 2 has a nested context which is a setInterval. setInterval when invoked does only one thing : It just schedules the given code for execution. The below code will be scheduled to execute on every second.

    function checkVal(){
            console.log(count)
        }
    

    Now let us examine the above code.

    It is a function nested inside the higher order function page. Since it is a nested function, it can access the variables from its enclosing function.

    Now comes the most important point : When the nested function had been created while rendering the component page, its binding to the variables in its enclosing function has also been created. Therefore it is not only a function object created but a scope as well. The variables referenced inside the nested function are resolved from this scope. This mechanism – a function object along with its scope is generally called closure.

    Now the point of failure is this:

    1. useEffect will fire only once since its dependency array is empty.
    2. Therefore setInterval will be invoked exactly only once.
    3. setInterval when called initially and only once, the nested function created will get the scope which has the state count with the value 0.

    Since no further changes are happening on any of the three steps above, when the following code will run on its due time, its variable count will always be resolved from the same scope with the same value 0. Therefore it prints 0 always.

    function checkVal(){
            console.log(count)
        }
    

    You can run the below code to test and confirm this understanding in another way.

    Try the below code – adding count as its dependency.

    Though the below code will work infinite, it will still print the latest count based on the button clicking. The above three points will change on every mouse click. UseEffect will fire again, setInterval will be invoked again, and the nested function will be re-created with a new scope every time. Therefore the variable reference will be get the latest value while printing in console.

      useEffect(() => {
        setInterval(checkVal, 1000);
      }, [count]);
    
    Login or Signup to reply.
  2. The issue here is that you never update the useEffect, so the checkVal function is using an old count. To solve it you need to review how effects dependencies work in React.

    For this particular example, you could update the useEffect as follows:

    useEffect(()=>{
      const int = setInterval(checkVal,1000);
      return () => {
        clearInterval(int);
      };
    }, [checkVal]);
    
    • Add checkVal as dependency of the effect
    • Clean up the interval every time your checkVal function changes to avoid creating new intervals on every effect change.

    That will work, however, it is not a good idea to add a dependency on your effect that is changing on every render. That is because your checkVal function is not memoized and it will be re-created on every single render.

    You could improve your code as follows:

    function App() {
      const [count, setCount] = useState(0);
      const handleClick = () => {
        setCount((count)=>count + 1);
      };
      const checkValRef = useRef();
      checkValRef.current = () => {
        console.log(count);
      };
    
      useEffect(()=>{
          const int = setInterval(() => {
            checkValRef.current();
          },1000);
          return () => {
            clearInterval(int);
          };
      },[]);
    
      return (
        <div>
          <button onClick={handleClick}>{count}</button>
          <button onClick={checkValRef.current}>clickmetocheck</button>
        </div>
      )
    }
    
    • Create a react reference to store your function and keep it up-to-date.
    • Create a function in the setInterval, do not bind the reference.
    • Your effect does not need any dependency because you are using a reference.

    This creates a single function that is bind to the setInterval. However, on every call, it will trigger the latest function stored in the reference that will be always updated.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search