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.
Leave a Reply