Having objects as dependencies in React useEffect hook

Having objects as dependencies in React useEffect hook

Overview of what useEffect hook is.

The useEffect hook enables us to run side effects in functional React components. These effects can include sending an asynchronous request to a third party API, logging a value to the console, manually changing the DOM among others.

The signature of the useEffect is shown below:

useEffect( fn , dep )

It takes a function ( fn ) and an optional second argument, 'dependencies' array ( dep ) . The function runs only when any of the values in the dependencies array changes and if this array is absent, it runs on every render of the react component in question. If this array is present but empty , the function runs only once ( i.e. on initial render ) .

Let’s take a trivial counter example :

import { useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("count is : " , count)
  })

  return (
    <div >
      <p>count: {count}</p>
     <button onClick={e => setCount(count => count + 1)}>increment</button>
    </div>
  );
}

In above code, function in useEffect hooks runs everytime the App component renders. If the useEffect hook is passed an extra parameter ( empty array ) as shown below , the function passed to it runs only once .

  useEffect(() => {
    console.log("count is : " , count)
  },[])

If this array is passed a value ( say count here ) as below , the function runs only when the count value changes .

Now let’s say the button click handler sets the exact same value of count i.e there is no increment in count value . The only change to code 1 to depict this is to replace the button element line with this :

<button onClick={() => setCount(count)}>increment</button>

Due to the above change , It is observed that asides the initial render of the component , there are no subsequent renders because the value of count in the dependencies array doesn’t change i.e count’s value keeps being 0. As expected , the function passed to useEffect hook doesn’t run 👍🏿

However , there’s a gotcha in this useEffect hook when the count value in the dependencies array is an object .

Problem with having objects as dependencies.

Let’s say , the count’s value is stored as a property in an object . We can have the following :

import { useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState({value: 0});

  useEffect(() => {
    console.log("count is : " , count.value)
  },[count])

  return (
    <div >
      <p>count: {count.value}</p>
    <button onClick={() => setCount({value: count.value})}>increment</button>
    </div>
  );
}
export default App;

As it appears above, the object stored in the count variable keeps being the same even when the button is clicked . But there’s a kind of problem here, I would say 😶

Let’s quickly review how useEffect deals with values in the dependencies array .

What it does is to check for changes in any of the values that’s in the dependencies array and based on this , cause a rerender or not . So in our very first counter example where the count is stored as a primitive value and not an object, React checks if count is same between the current and previous time the useEffect hook ran . A little showcase of how the check could be is below:

  • If count’s value is same, the callback effect function ( first argument of useEffect hook ) is not called
let prevCount = 0
let currentCount = 0
console.log( prevCount === currentCount) // true

// effect function will be called
  • else it’s not called,
let prevCount = 2
let currentCount = 3
console.log( prevCount === currentCount ) // false

//effect function won't be called
  • But when count is a non-primitive value ( an object ) , a different behavior is observed
let prevCount = { value: 0 }
let currentCount = { value: 0 }
console.log(prevCount === currentCount) // false

//or let's use a more 'objecty' way to compare objects
console.log( Object.is( prevCount , currentCount ) ) // still false :(

//effect function won't be called

The reason for this behavior is because unlike primitives where comparisons are based on their values, comparing objects is based on references / identities of this object. Seem confused ?

Here’s a link that gives more detail on this : primitive-vs-reference (non-primitive) values

As you may now have perceived , from what’s illustrated above in the counter example where count is an object and passed as a dependency in useEffect, the callback function is called even when the value property remains the same and there is no apparent difference between the previous count object and current count object.

Ways to go about this problem.

To correct this undesired behavior , there are ways to go about this. I will give a couple of them here.

  • Using JSON.stringify

This is one way to go about getting a desired behavior which i personally see as pretty much straightforward . What’s required to be done is just to have JSON.stringify(<objectInQuestion>) in the dependencies array instead of just the object in question so we have something as what’s below:

 useEffect(() => {
    console.log("count is : " , count.value)
  },[JSON.stringify(count)])

Now , the function runs only when there is an apparent change in count object ( even when count object has nested object(s) in it). But , ermmmm..yh it works fine trust me at least for trivial examples as this but it can be a bad idea when dealing with large/deeply nested objects.

Actually , stringifying an object is an expensive operation ( even worse when the object passed to it is large in size ) and the fact that JSON.stringify() runs during every render of a React component can pose some performance issues.

  • Depending on object's property

Here, instead of having useEffect depend on the whole object, it only depends on the properties used within the callback function ( useEffect’s first parameter ).

In our counter example ( where count is an object ) , we an make useEffect depend on the value property in place of the count object since that is what’s used really in the callback function.

So even if the count object holds another property ( say age ) , as long as the age property isn’t used within the useEffect callback function , it is much better and less redundant to make useEffect depend on value property ( in count object ) explicitly.

The useEffect hook will look like this :

  useEffect(() => {
    console.log("count is : " , count.value)
  },[count.value])

This may start to look tricky when the count’s property ( value ) is an object too.

Let’s say in counter example , our count state is as below:

const [count, setCount] = useState({
    first: {
      second: {
        third: {
          value: 0,
        },
      },
    },
  });

Our useEffect hook will look like this :

//using the current option
useEffect(() => {
    console.log("count is : ", count.first.second.third.value);
  }, [count.first.second.third.value]);
  • Using useDeepCompareEffect hook

Here , a custom hook ( useDeepCompareEffect ) from the use-deep-compare-effect package is used as a drop-in replacement for useEffect hook. It is important to note that this is unnecessary when we are dealing with primitives .

On installation in our react app, all that’s needed is to import the hook into our code and use it in place of useEffect hook.

So for our counter example, we get the desired behavior with the following lines of code.


import { useEffect, useState } from "react";
import useDeepCompareEffect from "use-deep-compare-effect";

function App() {
  const [count, setCount] = useState({
    first: {
      second: {
        third: {
          value: 0,
        },
      },
    },
  });


  useDeepCompareEffect(() => {
    console.log("count is : ", count.first.second.third.value);
  }, [count]);

  return (
    <div>
      <p>count: { count.first.second.third.value}</p>
      <button
        onClick={() =>
          setCount({
            first: { second: { third: { value: count.first.second.third.value } } },
          })
        }
      >
        increment
      </button>
    </div>
  );
}
export default App;

Only change is just addition of an import statement in our code , and ‘embedding’ Deep somewhere inside our component function . 😁 Yh, that’s just it.

This last option seems to appease me and personally , I find it considerably necessary when dealing with deeply nested/large objects.

Wrapping it up.

In this article , I have been able to give an overview of the useEffect hook in React from my viewpoint, point out a problem in using the dependencies array and give some of the many ways to go about this problem.

Just as everything else within the React landscape, there are always many ways to solve a problem. I was able to give three of the possible ways to solve the problem associated with having objects as dependencies but I’m really curious if you’ve found other ways to solve this. I’m also curious if this has even been a problem for you.

Looking forward to your comments below.

Feel free to reach out to me on Linkedin and Twitter. See you in my next article ;)