useState values not updating in React

Recently, I ran into a React problem I’d read about many times. But I didn’t recognise it at first because I’d never consciously run into it myself.

It’s what’s referred to as Stale Closures, which means, in my rudimentary understanding, you’ve used a variable within a function that’s pulled it into a different context that won’t see any updates React makes to it.

…Meaning when the inner function runs later, it could be using an old value.

The problem, simplified

Here’s a very simply React button component that has a label that counts up each time it’s clicked.

const ClickCountButton = () => {
    const [count, setCount] = React.useState(0);

    const handleClick = (e) => {
        setCount( count+1 );
    }

    return <>
        <button
            onClick={handleClick}
        >   
            {count}
        </button>
    </>

})

Here’s the same function with a new enclosure that can’t access updates to the original context because it will run asynchronously. This means that it will only know the value that the variable count was at the time that it was set, and won’t know about any changes after that.

const ClickCountButton = () => {
    const [count, setCount] = React.useState(0);
    const handleClick = (e) => {
        setCount( count+1 );
    }


    // New code: Add an event listener on component mount
    React.useEffect( () => {
        // Introducing a problem with stale closures
        app.on('incremented-externally', () => {
            setCount( count+1 );
        }));
    }, [])
    //////



    return <>
        <button
            onClick={handleClick}
        >   
            {count}
        </button>
    </>

})

In the above code, there’s now an asynchronous event listener that’s running to respond to some other interaction outside of this component, and it should also increment the count.

In order to increment correctly, however, it needs to know the current count at the time the interaction occurs — Which it doesn’t. It only knows the count that was applied at the time the event listener was set.

Note: The app object and event app.on('incremented-externally',...) is just something I made up to simplify the code, it’s not real and won’t work.

A solution

A solution I found somewhere online (I don’t remember where unfortunately), is to add a Ref into the mix.

const ClickCountButton = () => {
    const [count, setCount] = React.useState(0);
    const handleClick = (e) => {
        setCount( count+1 );
    }

    // New code:
    // Creates a ref and updates it every time count changes
    const countRef = React.useRef(count);
    React.useEffect( () => {
        countRef.current = count;
    }, [count]);
    //////


    React.useEffect( () => {
        app.on('incremented-externally', () => {
            setCount( countRef.current+1 );   // Note the update here
        }));
    }, [])

    return <>
        <button
            onClick={handleClick}
        >   
            {count}
        </button>
    </>

})

We still need the count state and it’s setCount function, because those are what tells the react component to update. But we’ve added the Ref in as a persistent variable that the asynchronous code will always have the correct reference to.

Why?

My goal here was to provide a quick solution for anyone that hits this issue too, so I don’t claim to have the deepest understand of what’s going under the hood. That being said, let me take a stab at explaining my best guess…

Count is a primitive variable?

Count is a primitive variable, this means that when passed into other functions, it’s value is passed in — Not a reference to the variable. This means that if the variable changes, the function doesn’t know the new value.

That came to mind first, but I don’t think it’s quite the only story…

React state is immutable

If count was an object (Which is passed by reference), would we still have this issue? I believe so (though I haven’t tested it). That’s because React state is immutable. Even if it was an object, the objects properties wouldn’t be changed with updates and leave the original object reference valid, but rather, the whole object would be replaced — Making any past references to it wrong, and thus, the values stale.

I’m going to leave it there rather than digging in deeper for now, but now you know what you’re experiencing is a stale closures problem, go ahead an google that to learn more.

And if you know more, feel free to add clarifications/corrections in the comments.

Thanks…

I also dissect and speculate on design and development.
Digging into subtle details and implications, and exploring broad perspectives and potential paradigm shifts.
Check out my conceptual articles on Substack or find my latest below.


You can also find me on Threads, Bluesky, Mastodon, or X for more diverse posts about ongoing projects.

My latest articles

Keep doing your crappy little drawings (Staging XR scenes)

Some people create beautiful perspective illustrations to visualise and storyboard their virtual reality designs And it’s tempting to think you’re not a strong designer if you’re not doing that too…

Focal point blocking for XR media

Planning out a linear VR experience requires thinking about where the viewers attention might be. Thinking about the focal points…

Designing immersive experiences

In traditional cinema, TV, or even the more modern phone screen, there’s limited screen real-estate. But removing that limitation creates a design problem…

The future is not prompt engineered

Let’s not pretend the importance of prompt engineering is ubiquitous. The most prevalent power of generative AI is in the way it adapts to us, not the other way around…

The typography of dates, times, & filenames

A deep dive into carefully considered date formatting, line length and general typography attributes of filenames…

Loosening the Shackles of Rapid Authoring Tools

Rapid authoring tools like Articulate Storyline and Evolve Authoring make sharing projects possible across a team of non-programmers, but your design must often adapted to the limited range of possibilities the tool allows…
Bluesky
Threads
Twitter / X
Mastodon
Instagram


Author:

Date:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.