Closures in React
Why your state is stale
September 14, 2023 • 2 min read
A closure is a function that remembers the variables around it, even after the outer function has finished running. This is a normal JavaScript behaviour, but in React it shows up in a way that confuses a lot of people, usually inside useEffect or inside an event handler that was created a few renders ago.
We will see how closures work in a simple function first, and then look at the classic stale state problem in a React component and how to fix it.
How it Works
Every time you call the outer function, the inner function keeps a reference to the variables that existed at that moment. Those variables do not disappear when the outer function returns.
function counter() {
let count = 0
return function increment() {
count += 1
return count
}
}
const next = counter()
console.log(next()) // 1
console.log(next()) // 2In the code above, counter() runs once and returns the increment function. The count variable stays alive because increment still points to it. Each call keeps working on the same value instead of starting from zero again.
Stale state
In React, every render creates new functions, and each one closes over the props and state of that specific render. If you keep an old function around, it keeps the old values too.
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(id)
}, [])
return <h1>{count}</h1>
}This looks correct but the counter stops at 1. The effect runs only once because the dependency array is empty, so the callback inside setInterval closed over count when it was still 0. Every second it calculates 0 + 1 again.
The fix
Use the updater form of the setter. React passes the current value to your function, so you do not need to read count from the closure at all.
useEffect(() => {
const id = setInterval(() => {
setCount(previous => previous + 1)
}, 1000)
return () => clearInterval(id)
}, [])Now the interval does not care which render created it. The other option is to add count to the dependency array, but that recreates the interval on every tick, and usually the updater form is what you want.
Conclusion
Closures are not a React feature, they are how JavaScript works. React just makes them visible because it runs your component function again and again. When a value looks outdated, ask yourself which render created that function and what it could see at the time.
Have a nice code!