Engineering

26 November, 2020

Dangers of using Objects in useState & useEffect ReactJS Hooks

What we talk about in this article is a problem/bug that can easily go unnoticed when using objects with hooks.

Tiago Duarte

Software Engineer

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

Software Development

Web

Hooks

ReactJS

Join our newsletter

Be part of our community and stay up to date with the latest blog posts.

Subscribe

Join our newsletter

Be part of our community and stay up to date with the latest blog posts.

Subscribe

You might also like...

Go back to blogNext
How to support a list of uploads as input with Absinthe GraphQL

Engineering

26 July, 2022

How to support a list of uploads as input with Absinthe GraphQL

As you might guess, in our day-to-day, we write GraphQL queries and mutations for Phoenix applications using Absinthe to be able to create, read, update and delete records.

Nuno Marinho

Software Engineer

Flutter Navigator 2.0 Made Easy with Auto Router - Coletiv Blog

Engineering

04 January, 2022

Flutter Navigator 2.0 Made Easy with Auto Router

If you are a Flutter developer you might have heard about or even tried the “new” way of navigating with Navigator 2.0, which might be one of the most controversial APIs I have seen.

António Valente

Software Engineer

Enabling PostgreSQL cron jobs on AWS RDS - Coletiv Blog

Engineering

04 November, 2021

Enabling PostgreSQL cron jobs on AWS RDS

A database cron job is a process for scheduling a procedure or command on your database to automate repetitive tasks. By default, cron jobs are disabled on PostgreSQL instances. Here is how you can enable them on Amazon Web Services (AWS) RDS console.

Nuno Marinho

Software Engineer

Go back to blogNext