1 Feb 2023
In this article, we explore a problem/bug that can easily go unnoticed when using objects with hooks.
Hooks have been around for a few years now. They were added in React v16.8.0, and let you use state and other React features without writing a class.
In this article, we won't be going into much detail about what a hook is, its syntax, and so on. For that, you can visit the React documentation page where we think that the React team did a great job explaining it.
What brings us here is a problem/bug we faced when we first started using hooks that can easily go unnoticed.
Let's look at the following example:
const { useState } = React
const Counter = () => {
const [count, setCount] = useState(0)
const [objectCount, setObjectCount] = useState({ count: 0 })
return (
<div>
<h2>Count</h2>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Increase normal count</button>
<h2>Object Count</h2>
<p>You clicked {objectCount.count} times</p>
<button
onClick={() => {
objectCount.count += 1
setObjectCount(objectCount)
}}
>
Broken increase of the object count
</button>
<button
onClick={() =>
setObjectCount({
...objectCount,
count: objectCount.count + 1,
})
}
>
Functioning increase of the object count
</button>
</div>
)
}
ReactDOM.render(<Counter />, document.getElementById('app'))
We prepared this codepen with the example, feel free to visit and play around with it.
In our example, we have:
A count
state hook that stores a plain number;
An objectCount
state hook that stores an object that contains the count
property inside;
An Increase normal count button that updates the count
state. You can validate this by seeing that the counter updates right after pressing the button.
A Broken increase of the object count button that tries to update the objectCount
, but fails miserably. You might be thinking, “Naah, that should work…". Go ahead and try it out on codepen.
A Functioning increase of the object count button that properly updates the objectCount
state.
When a user presses the button, we increase the count
property inside the objectCount
object and then call setObjectCount(objectCount)
.
The problem with this is that the useState
hook uses strict equality comparison to determine if it should trigger a re-render and doesn't check if the properties of the object actually changed.
In other words, the hook compares (===
) the "old" and "new" states and concludes that the object hasn't changed and won't trigger a re-render, causing the object count label to stay the same.
setObjectCount
The Functioning increase of the object count button fixes the issue by creating and passing a shallow copy of the objectCount
to the setter function.
It basically keeps the same object properties but creates a new object reference so that the hook strict equality comparison determines that the state changes, and immediately triggers a re-render.
setObjectCount
Another solution would be to simply not use objects in an useState
hook.
You could use the useState
hook per each property of the object. In theory, this would be the ideal scenario, but doing this might be daunting and time-consuming.
You might have your reasons to directly store an object as state. In our case, we were retrieving data from an API and decided to store the object retrieved.
If you are familiar with Redux you already know how this works as it is very similar.
useReducer
accepts a reducer of type (state, action) => newState
, and returns the current state paired with a dispatch
method.
This is usually preferable to useState
when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
As per the documentation, "Immutable data cannot be changed once created, leading to much simpler application development, no defensive copying, and enabling advanced memoization and change detection techniques with simple logic. Persistent data presents a mutative API which does not update the data in-place, but instead always yields newly updated data."
In practical terms, when using immutable.js, every object change would actually create a new object. In our example, this would cause the state hook to trigger a re-render.
Keep in mind that the same problem and solutions apply to the (optional) list of dependencies of the useEffect hook.
When this problem happened to me and Rui Sousa, we spent, I would say, a couple of hours hunting down the problem. So we felt like sharing this tip in hopes that it saves you debug time.
If you have a suggestion or a different solution than the ones listed, go ahead and drop us a message.
Tiago Duarte
CPO
Tiago has been there, seen it, done it. If he hasn’t, he’s probably read about it. You’ll be struck by his calm demeanour, but luckily for us, that’s probably because whatever you approach him with, he’s already got a solution for it. Tiago is the CPO at Significa.
Nuno Polónia
Front-End Developer
18 October 2024
Master JavaScript web animations with requestAnimationFrame.Significa
Team
30 September 2024
Optimise your e-commerce website for better performance.Ricardo Reis
(Former) Front-End Developer