I love React Hooks even though they're not perfect thanks to some weird rules that sometimes make them feel like magic. Rules such as not being able to use them inside conditionals, and requiring them to be called at the top of the components.
Even with these restrictions, I still enjoy using them when building my React apps and they have helped me write cleaner, intuitive code many times.
However, there was a case recently where I discovered a quirk of how the useEffect
hook works and how it caused me wasted hours of debugging.
Beware of stale state
I had a keydown listener being attached to the DOM inside a useEffect
call that had no dependencies (supposed to run only once per render).
Inside the callback to the listener, I was reading a state variable (useState
) and updating it depending on what the value was whenever a user clicked on their arrow keys.
Now here’s the problem. The callback was always working with the INITIAL state captured during the first render. Even though I was updating the state inside the callback, I kept reading a stale state.
I was stumped as to why this was happening.
After eliminating wrong logic as the cause of the problem, I started suspecting the useEffect call. So I went to Dan Abramov’s "A Complete Guide to useEffect" post to see if there was something I was missing.
It was then I discovered something which explained why I was getting this problem.
useEffect calls close over any variables within them per render
So during the first render, the useEffect call would effectively freeze over the initial state and since there were no dependencies to update the useEffect call, I was always going to be stuck with stale data.
The fix was simply to add in the state variables I was making use of to the useEffect's dependency array. That enabled the useEffect call to “refresh” in response to the state updates being made inside the listener callback.
It's a simple fix that is obvious in hindsight and is even documented on the useEffect
entry in the Hooks documentation..