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 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).
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:
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:
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, 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:
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, thenuseEffect
is called.
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>;
};
- 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:
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>;
};
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:
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>;
};
- useEffect + reference comparison (2): now let's see an example with a parent-child component relationship:
// 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):
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
.
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:
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:
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:
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!
Top comments (2)
In your second example i,e counter, moving the animalList array outside component resolved the issue , why ?
As we know, If a parent component is updated, React always update all the direct children within that component. So, even after moving animalList outside, ChildComponent is still child of App and setCount will trigger App re-render which should re-render ChildComponent too. But it's not. Can you explain ? @vicnovais
Hey, @aakashsr!
When we move the animalList to outside, it's reference remains the same for ever, i.e. it's in the same piece of memory regardless of what the component does.
The ChildComponent indeed will be called and rendered again, but the
useEffect
handler will not be called, because it's reference remained the same.