designdebt.club
Menu
Design Debt Club

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>
    </>

[[social]]

})

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.