Stale closures and React hooks
Stale closures can be a tricky and hard-to-find issue when using React hooks. In this article we are going to see what stale closures are, how it occurs and different ways to fix them.
Before starting with stale closures, what are closures actually? Quoting MDN docs, ‘A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).’ In other words, a closure is nothing but a function that remembers its outer variables and can access them if needed. Shown below is a simple example of closure.
In this example, function ‘c’ does have a reference to variable ‘b’, which is in the lexical environment of ‘c’, thus creating a closure.
Now that we’ve understood what closures are, let’s see what stale closures are in React hooks. Stale closure is the referencing towards an outdated variable in between renders (from ‘React’ perspective). That means even when the state/props are updated and component re-renders, some callbacks will still be referencing old variables. Let's see this with an example.
Here, we do have a ‘count’ state and two buttons to increment and decrement. Also, there is a useEffect, which attaches a click handler to the ‘h1’ tag. When the user clicks on ‘h1’, it basically checks the count and if it is greater than 0, the count will be set to +100 else -100.
So, using the ‘+’ button we incremented the count to 2. Now, what happens if we click on the ‘h1’ tag? Let’s think for a moment. As the current value of count is 2, which is greater than 0, it will be set to +100?..
..
..
..
..
..
Actually, the count will be set to -100 (If needed, you can try this in the codesandbox above). Why is it so? Why aren't we getting the expected result i.e. +100?..
This is happening because once the user clicks on the ‘h1’ tag, we are doing a check to see if the count is greater than 0. That logic is correct. But the issue is, the ‘count’ variable we are referencing to do the check was created during the initial render. And what was the value of the ‘count’ variable during the initial render? It was 0. So when the check happens, 0 is not greater than 0, and hence the count will be set to -100.
Basically, we are referencing an outdated ‘count’ variable that holds some outdated value. And this is called ‘stale closure’, which is the referencing towards an outdated variable in between renders
So, how can we fix this? Let’s see four different ways
1. useEffect Dependency Array
This issue wouldn’t even have occurred in the first place if we had properly followed the rules of React hooks dependency arrays. Quoting React docs ‘make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders’.
This is the mistake we are making as well. If we look at the code, we are using a state variable ‘count’ inside the useEffect which changes over time. But, we didn’t add the ‘count’ variable into the dependency array of the useEffect. So, it won’t run the callback passed into the useEffect (after running the cleanup function) when the ‘count’ value changes. Thus, our event handlers happen to reference an old ‘count’ variable.
To fix this, simply add ‘count’ to the dependency array of the useEffect
And it starts working!
2. State update using callback functions
Another way in which we can fix this issue is by using a callback function to update the state. The state-updater function which is provided by the useState can receive either a value or a function. If we are passing a function, that callback function will receive the latest (current) value of the state as its first parameter (similar to useReducer). We can then use that latest value to do the comparison. Actually, it is better to use a function to update the state if we are computing a new state value based on the previous state value. To get a better idea regarding ‘Function Updates’, you can go ahead and read this article by Kent C Dodds, which explains how ‘Function Updates’ can be helpful in certain situations.
So, in the above example, we can pass a callback function to the state-updater function, and then using the latest value received, we can do the comparison.
And then we get the expected result!
You can play around with the code below if needed. Just increment the count to 2 and click on the ‘Count’ title. Compare the results with those of the problem statement at the top(first codesandbox).
3. Refs to the Rescue
We can also fix this issue by using useRef. What is special about useRef is that the returned object from useRef will persist for the full lifetime of the component and it will give you the same ref object on every render. So what we can do is, create a ref and point it to the latest value of count on every render.
You can also put this logic inside a useEffect which runs whenever the value of count changes. That will also give the same results.
Since we are having the latest value of ‘count’ in countRef.current let's pass it to the event handler instead of the ‘count’ variable itself. So, whenever a click happens our event handler will compare the value of the countRef.current which holds the latest value of ‘count’.
Also remember, by using Refs we won't get into the issue of creating new variables on every render, as React ensures the object returned from useRef will be the exact same throughout the lifetime of the component. So, by using Refs we will be having one object which is the same throughout the renders and it will hold the latest value of count.
Hence, by using Refs things have started working again!
You can play around with the code below.
4. Combining Event handlers with Refs
This one is a pretty contrived solution. But let's see how it works. We are going to use the same useRef we’ve used in the earlier solution, but in a different way. Instead of pointing the state value to the ‘current’ property of Ref, we are going to point the clickHandler function itself to the ‘current’ property.
We are making a small change such that our event handler is defined with the latest value of count on every render and then we point clickHandlerRef.current to that function. Similar to the previous solution, we can put this logic inside a useEffect so as to make it run only when the ‘count’ value changes.
As we are having the latest value of ‘count’ in the function which clickHandlerRef.current points to, let’s create another function, which in turn executes the clickHandlerRef.current function and then we can pass this newly created function to the event handler.
So, whenever a click happens our event handler will execute the function which clickHandlerRef.current points to. This function will be having the latest value of ‘count’, as this just got created on the last render with the latest value of ‘count’.
With the above changes, we will get the expected result.
If needed, you can play around with the code.
Thus, we’ve now got an idea of four different ways in which we can fix the Stale closure issue in React hooks. Please let me know in the comments if you know of any other solutions.
All four solutions and the problem is uploaded into Github and you can find it here.
Thank you and will see you with another article!