In this post, I'll show how to create a closure in a useState hook React app.
I'll not explain what a closure is, because there are many resources about this topic and I don't want to be repetitive. I advise the reading of this article by @imranabdulmalik.
In short, a closure is (from Mozilla):
...the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
Just in case you're not familiar with the term lexical environment, you can read this article by @soumyadey or alternatively this one.
The problem
In a React application, you can create accidentally a closure of a variable belonging to the component state created with useState hook. When this happens, you're facing a stale closure problem, that is to say, when you refer to an old value of the state that in the meantime it's changed, and so it's not more relevant.
POC
I've created a Demo React application which the main goal is to increment a counter (belonging to the state) that can be closed in a closure in the callback of setTimeout method.
In short, this app can:
- Show the value of the counter
- Increment by 1 the counter
- Start a timer to increment the counter by 1 after five seconds.
- Increment by 10 the counter
In the following picture, it's shown the initial UI state of the app, with counter to zero.
We'll simulate the closure of the counter in three steps:
- Incrementing by 1 the counter
- Starting the timer to increment by 1 after five seconds
- Incrementing by 10 before the timeout triggers
After 5 seconds, the value of the counter is 2.
The expected value of the counter should be 12, but we get 2.
The reason why this happens it's because we've created a closure of the counter in the callback passed to setTimeout and when the timeout is triggered we set the counter starting from its old value (that was 1).
setTimeout(() => {
setLogs((l) => [...l, `You closed counter with value: ${counter}\n and now I'll increment by one. Check the state`])
setTimeoutInProgress(false)
setStartTimeout(false)
setCounter(counter + 1)
setLogs((l) => [...l, `Did you create a closure of counter?`])
}, timeOutInSeconds * 1000);
Following the full code of the app component.
function App() {
const [counter, setCounter] = useState<number>(0)
const timeOutInSeconds: number = 5
const [startTimeout, setStartTimeout] = useState<boolean>(false)
const [timeoutInProgress, setTimeoutInProgress] = useState<boolean>(false)
const [logs, setLogs] = useState<Array<string>>([])
useEffect(() => {
if (startTimeout && !timeoutInProgress) {
setTimeoutInProgress(true)
setLogs((l) => [...l, `Timeout scheduled in ${timeOutInSeconds} seconds`])
setTimeout(() => {
setLogs((l) => [...l, `You closed counter with value: ${counter}\n and now I'll increment by one. Check the state`])
setTimeoutInProgress(false)
setStartTimeout(false)
setCounter(counter + 1)
setLogs((l) => [...l, `Did you create a closure of counter?`])
}, timeOutInSeconds * 1000);
}
}, [counter, startTimeout, timeoutInProgress])
function renderLogs(): React.ReactNode {
const listItems = logs.map((log, index) =>
<li key={index}>{log}</li>
);
return <ul>{listItems}</ul>;
}
function updateCounter(value: number) {
setCounter(value)
setLogs([...logs, `The value of counter is now ${value}`])
}
function reset() {
setCounter(0)
setLogs(["reset done!"])
}
return (
<div className="App">
<h1>Closure demo</h1>
<hr />
<h3>Counter value: {counter}</h3><button onClick={reset}>reset</button>
<hr />
<h3>Follow the istructions to create a <i>closure</i> of the state variable counter</h3>
<ol type='1' className=''>
<li>Set the counter to preferred value <button onClick={() => updateCounter(counter + 1)}>+1</button> </li>
<li>Start a timeout and wait for {timeOutInSeconds} to increment the counter (current value is {counter}) <button onClick={() => setStartTimeout(true)}>START</button> </li>
<li>Increment by 10 the counter before the timeout <button onClick={() => updateCounter(counter + 10)}>+10</button> </li>
</ol>
<hr />
{
renderLogs()
}
</div >
);
}
export default App;
Solution
The solution is based on the use of useRef hook that lets you reference a value thatβs not needed for rendering.
So we add to the App component:
const currentCounter = useRef(counter)
Then we'll modify the callback of setTimeout like shown below:
setTimeout(() => {
setLogs((l) => [...l, `You closed counter with value: ${currentCounter.current}\n and now I'll increment by one. Check the state`])
setTimeoutInProgress(false)
setStartTimeout(false)
setCounter(currentCounter.current + 1)
setLogs((l) => [...l, `Did you create a closure of counter?`])
}, timeOutInSeconds * 1000);
Our callback needs to read the counter value because we log the current value before to increment it.
In case, you don't need to read the value, you can avoid the closure of the counter just using the functional notation to update the counter.
setCounter(c => c + 1)
Credits
- Dmitri Pavlutin Be Aware of Stale Closures when Using React Hooks
- Imran Abdulmalik Mastering Closures in JavaScript: A Comprehensive Guide
- Keyur Paralkar Lexical Scope in JavaScript β Beginner's Guide
- Souvik Paul Stale Closures in React
- Soumya Dey Understanding Lexical Scope & Closures in JavaScript
- Subash Mahapatra stackoverflow
Top comments (0)