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.
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.
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:
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:
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(0) const timeOutInSeconds: number = 5 const [startTimeout, setStartTimeout] = useState (false) const [timeoutInProgress, setTimeoutInProgress] = useState (false) const [logs, setLogs] = useState >([]) 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) => {log} ); return{listItems}
; } function updateCounter(value: number) { setCounter(value) setLogs([...logs, `The value of counter is now ${value}`]) } function reset() { setCounter(0) setLogs(["reset done!"]) } return (); } export default App;Closure demo
Counter value: {counter}
Follow the istructions to create a closure of the state variable counter
- Set the counter to preferred value
- Start a timeout and wait for {timeOutInSeconds} to increment the counter (current value is {counter})
- Increment by 10 the counter before the timeout
{ renderLogs() }
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.
seCounter(c => c + 1)
The above is the detailed content of React: stale closure. For more information, please follow other related articles on the PHP Chinese website!