Development · February 2023
Dangers of using Objects in useState & useEffect ReactJS Hooks
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 and so forth. For that, you can visit the React documentation page where we think that the React team did a great job explaining it.
(Un)known problem of using objects in useState/useEffect hooks
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:
1const { useState } = React23const Counter = () => {4 const [count, setCount] = useState(0)5 const [objectCount, setObjectCount] = useState({ count: 0 })67 return (8 <div>9 <h2>Count</h2>10 <p>You clicked {count} times</p>11 <button onClick={() => setCount(count + 1)}>Increase normal count</button>1213 <h2>Object Count</h2>14 <p>You clicked {objectCount.count} times</p>15 <button16 onClick={() => {17 objectCount.count += 118 setObjectCount(objectCount)19 }}20 >21 Broken increase of the object count22 </button>2324 <button25 onClick={() =>26 setObjectCount({27 ...objectCount,28 count: objectCount.count + 1,29 })30 }31 >32 Functioning increase of the object count33 </button>34 </div>35 )36}3738ReactDOM.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
Why does pressing the Broken increase of the object count button doesn't immediately increase the object count?
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.
Possible solutions
Create and pass a shallow copy to 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.
Do not use an object as state
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.
Use the useReducer hook
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.
Use immutable.js
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.
Time saver
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.