Understanding referential equality in React's useEffect
Tags: javascript, react, webdev, typescript
Published: 2022-04-05 01:44:20

Hello fellow readers!
In this post I am going to discuss how useEffect handles it's dependencies when there is an object in it.

**Note: there will be assumptions that you know some key concepts about useEffect. So, if you don't really know the basics, I first recommend you to [read the React docs](https://reactjs.org/docs/hooks-effect.html) on this subject.**

## Referential equality

When we talk about comparison in most of programming languages, we deal with two topics: comparison by reference and comparison by value.
In JavaScript world, this is also true. We can compare values using primitive types, like string or numbers, or compare references when dealing with objects.

### Comparison by value

This is the most straightforward concept. If two values are equal, then a boolean comparison returns `true`. Note that this works for the **most common** primitive types of JavaScript (strings, numbers and booleans).

```javascript
const a = 1;
const b = 1;
const c = 2;
console.log(a === b); // true
console.log(b === c); // false

const d = 'hello';
const e = 'hello';
const f = 'bye';
console.log(d === e); // true
console.log(e === f); // false
```

### Comparison by reference

This type of comparison takes in consideration where in the memory an object is located. If two objects point to the same location, they are equal, otherwise they are different. Check out the following schema:

![Drawing showing two objects pointing to two different memory pieces](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bo89st1il3lo9c4hs0lb.png)

Even if two objects have the same properties with the same values, they will not be equal, unless they are located in the same memory position. You can run the following code in your browser's DevTools to prove this:

```javascript
const obj1 = { animal: 'dog' };
const obj2 = { animal: 'dog' };
const obj3 = obj1

console.log(obj1 === obj1) // true
console.log(obj1 === obj2) // false
console.log(obj2 === obj3) // false
console.log(obj1 === obj3) // true
```

## Comparison in React's useEffect

With the previous introduction on types of comparison on mind, let's bring that concept into React's hook useEffect.
Accordingly to [React's docs](https://reactjs.org/docs/hooks-effect.html), we can define this hook as:

> The Effect Hook lets you perform side effects in function components. Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects. Whether or not you’re used to calling these operations “side effects” (or just “effects”), you’ve likely performed them in your components before.

If we need to run an effect after a specific change, we must use hook's second argument, which is an **array of dependencies**:

```javascript
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
```

Every time any of the dependencies change, the callback inside useEffect is run, and in this process it is important to know how the comparison is made.

If there are only primitive values such as `string` or `number`, there will be a comparison by value, otherwise there will be a comparison by reference.

I've seen plenty of times errors regarding the functionality of useEffect when it comes to dependencies. You may trap yourself in an infinite loop or multiple calls to an API, which may incur in money loss if, for example, your back-end is hosted in a cloud service. To mitigate these problems, it is important to keep these dependencies as stable as possible.

So, let's see some examples.

- **useEffect + value comparison:** this example show a simple count component that render in screen a new text every time the `count` state changes. As it is a number, React simply compares if the previous number and the new number are different, if this is true, then `useEffect` is called.

```tsx
const ValueComparison = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.body.append(`Whoa! My count is now: ${count}`);
    var br = document.createElement('br');
    document.body.appendChild(br);
  }, [count]);

  return <button onClick={() => setCount(count + 1)}>Click me to count</button>;
};
```

![Gif showing a button that when clicked increments a counter and renders in screen a text with the current count](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2mzmqxk3jn6ipgj3vrx.gif)

- **useEffect + reference comparison (1):** the following example shows a common problem. It shows a object state that is directly changed, but nothing is rendered. Check it out:

```tsx
const ReferenceComparison1 = () => {
  const [animalObj, setAnimalObj] = useState({ animal: 'dog' });

  const handleChange = () => {
    animalObj.animal = animalObj.animal === 'cat' ? 'dog' : 'cat';
    setAnimalObj(animalObj);
  };

  useEffect(() => {
    document.body.append(`I am this animal: ${animalObj.animal}`);
    var br = document.createElement('br');
    document.body.appendChild(br);
  }, [animalObj]);

  return <button onClick={handleChange}>Click me to change the animal</button>;
};
```

![Gif showing a non working button that when clicked does nothing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lhpbm09zln3688dguxn5.gif)
 
You may be asking yourself, baffled: _but the state did change! Now the animal should be a cat!_
Well... not quite. We are changing an object property, not the object per se. See, remember that an object comparison is made by reference? **So, the reference of the object in the memory stays the same even if some property changes, thereby useEffect dependency will not recognize any change.**

To fix this, we simply need to pass a new object to `setAnimalObj`, meaning that this new object will point to a new memory location, so the dependency will change and `useEffect` will fire:

```tsx
const ReferenceComparison1 = () => {
  const [animalObj, setAnimalObj] = useState({ animal: 'dog' });

  const handleChange = () => {
    setAnimalObj({
      ...animalObj,
      animal: animalObj.animal === 'cat' ? 'dog' : 'cat',
    });
  };

  useEffect(() => {
    document.body.append(`I am this animal: ${animalObj.animal}`);
    var br = document.createElement('br');
    document.body.appendChild(br);
  }, [animalObj]);

  return <button onClick={handleChange}>Click me to change the animal</button>;
};
```

![Gif showing a button that when clicked renders in screen wether the current animal is a dog or a cat](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nrwxrqowfq22opsj1dsu.gif)

- **useEffect + reference comparison (2):** now let's see an example with a parent-child component relationship:

```tsx
// Here is the parent component that renders an animal list and a button that increments a counter
const ReferenceComparison2 = () => {
  const [count, setCount] = useState(0);
  const animalList = [
    { animal: 'dog' },
    { animal: 'cat' },
    { animal: 'turtle' },
  ];

  return (
    <React.Fragment>
      <ChildComponent data={animalList} />
      <span>Count: {count}</span>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
    </React.Fragment>
  );
};

// Here is the child component, responsible for rendering the list used by parent component
const ChildComponent = ({ data }: ChildComponent1Props) => {
  useEffect(() => {
    document.body.append(`Child rendered! Data has changed!`);
    var br = document.createElement('br');
    document.body.appendChild(br);
  }, [data]);

  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item.animal}</li>
      ))}
    </ul>
  );
};
```

If we run the above code, we can see that the child component is rerendered every time the button is clicked, although the counter and the list are independent (also animalList hasn't changed any property at all):

![Gif showing an animal list with a button. Below these, there is a text showing how many times the child component was rendered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9sn9e7rgo38wiv0lk3k2.gif)
 
This happens because every time the counter is updated, the parent component is rerendered, therefore the function will be called again, generating a new reference for the object in `animalList` variable. Finally, the child component acknowledge this change and runs `useEffect`.

![Drawing representing that the animalList variable points to different memory blocks on each render](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wyux25nmtmulz4139m53.png)

It is possible to solve this in many ways, let's see two of them. The first solution below simply moves the array data outside the component function, therefore the object reference will never change:

```tsx
const animalList = [{ animal: 'dog' }, { animal: 'cat' }, { animal: 'turtle' }];

const ReferenceComparison2 = () => {
  const [count, setCount] = useState(0);

  return (
    <React.Fragment>
      <ChildComponent data={animalList} />
      <span>Count: {count}</span>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
    </React.Fragment>
  );
};
```

The second possible solution is to use `useMemo`. This hook keeps the same reference of a value unless its dependencies change:

```tsx
const ReferenceComparison2 = () => {
  const [count, setCount] = useState(0);
  const animalList = useMemo(
    () => [{ animal: 'dog' }, { animal: 'cat' }, { animal: 'turtle' }],
    []
  );

  return (
    <React.Fragment>
      <ChildComponent data={animalList} />
      <span>Count: {count}</span>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
    </React.Fragment>
  );
};
```

Now our child component will not run `useEffect`, because the data dependency has a stable reference:

![Gif showing that now, when the increment counter button is clicked, the child component does not rerender](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1j758ab5gr1zqumchow.gif)


## Wrapping up

We've seen how referential equality works when using `useEffect`. **It is always important to keep an eye on the dependencies, specially if they rely on objects, arrays or functions.**
You may find yourself sometimes in trouble when the same effect runs many times. If this happens, remember to checkout the dependencies and if they are stable.
Feel free to use the comments section to expose your opinion or ask me anything! Thanks!