1 Feb 2023

Dangers of using Objects in useState and useEffect ReactJS Hooks.

In this article, we explore a problem/bug that can easily go unnoticed when using objects with hooks.

Dangers of using objects in useState illustration cover.

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.


(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:

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:

  • 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.

  • 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.

  • 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.

Create and pass a shallow copy to 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.

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.


References

Tiago Duarte

CPO

Author page

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.

We build and launch functional digital products.

Get a quote

Related articles