skip to Main Content

I understand that setState is asynchronous, but does it explicitly wait for all other code to be executed before it completes (even setTimeout with delay)?

I’ve been trying to increment a counter using a simple recursive function in React, but setState never executes. I added a setTimeout with a one-second delay, just to give setState enough time to execute, but it just keeps waiting infinitely and never executes.

Is replacing useState with useRef the best solution, or are there more elegant solutions?

import { useState } from 'react';

const TestComponent = () => {

const [counter, setCounter] = useState(1);

const setStateTest = () => {        
    setCounter(prev => prev + 1); // this line of code never executes
    setTimeout(() => {
        if (counter < 10) {
            setStateTest(); // this recursion happens infinitely because counter is never incremented
            console.log(counter); // the logged value is always 1, no incrementing
        }
    }, 1000)
}

return <button onClick={setStateTest}>Set State Test</button>

}

2

Answers


  1. Don’t use recursion for this.

    I suspect the problem is a closure around the original counter value. State itself may be getting updated, but you’re not logging the current state to the console. You’re logging the captured value of that state when the function was first called. (Click the button again and observe updated values being logged to the console, alongside the ongoing original value.)

    Instead of recursing, rely on useEffect. For example, consider this approach:

    useEffect(() => {
      console.log(counter);
      const x = setTimeout(() => {
        setCounter(prev => prev + 1);
      }, 1000);
      return () => clearTimeout(x);
    }, [counter]);
    

    Any time counter changes, this effect will execute. And all this effect does is:

    1. Log the value of counter to the console
    2. Set a timeout to update the value of counter in 1 second
    3. Return a function to clear the timeout (this is important so that effects do not leak resources)

    Then to initially trigger it, just update state once:

    <button onClick={() => setCounter(prev => prev + 1)}>Set State Test</button>
    

    Once this changes the state, the effect triggers and it perpetually re-triggers itself every second (by updating the state value in its dependency array) as long as the component is loaded.

    Login or Signup to reply.
  2. States in React are asynchronous and the value will only update after the function has finished running. Since your function never stops running (as it loops the setTimeout), the asynchronous call never gets a chance to finish and update the state value, thus it remains at 1. For this reason, state is not the best way to do what you are currently trying to do. Define counter using let outside of the scope of TestComponent instead.

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