Dangers of using Objects in useState & useEffect ReactJS Hooks

Dangers of using Objects in useState & useEffect ReactJS Hooks — Navigation

Hooks have been around for almost two years now. They we’re 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 👍🏾 and we couldn’t explain it better.

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

      <br />
      <br />

      <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 “naaaaaahhh, 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 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” state 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 new 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 applies 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!

Fixing bugsFixing bugs

If you have a suggestion or a different solution than the ones listed, go ahead and leave us a comment 💬. We are very friendly, we promise 😇.

References

Original cover illustration Original cover illustration

Thank you for reading!

Thank you so much for reading, it means a lot to us! Also don’t forget to follow Coletiv on Twitter and LinkedIn as we keep posting more and more interesting articles on multiple technologies.

In case you don’t know, Coletiv is a software development studio from Porto specialised in Elixir, Web, and App (iOS & Android) development. But we do all kinds of stuff. We take care of UX/UI design, software development, and even security for you.

So, let’s craft something together?