use object in useEffect 2nd param without having to stringify it to JSON
In JS two objects are not equals.
const a = {}, b = {};
console.log(a === b);
So I can't use an object in useEffect
(React hooks) as a second parameter since it will always be considered as false (so it will re-render):
function MyComponent() {
// ...
useEffect(() => {
// do something
}, [myObject]) // <- this is the object that can change.
}
Doing this (code above), results in running effect everytime the component re-render, because object is considered not equal each time.
I can "hack" this by passing the object as a JSON stringified value, but it's a bit dirty IMO:
function MyComponent() {
// ...
useEffect(() => {
// do something
}, [JSON.stringify(myObject)]) // <- yuck
Is there a better way to do this and avoid unwanted calls of the effect?
Side note: the object has nested properties. The effects has to run on every change inside this object.
Answers
In React's useEffect
hook, the dependency array ([]
) is used to specify when the effect should re-run. React compares each value in this array using strict equality (===
) to determine if the effect needs to be re-executed.
Problem Understanding
When you have an object myObject
as a dependency in useEffect
:
function MyComponent() {
useEffect(() => {
// do something
}, [myObject]);
}
React will check if myObject
has changed using strict equality. However, if myObject
is an object and its reference changes (even if its properties remain the same), React will treat it as a different object and re-run the effect.
Solution 1: Use useRef
for Object Reference
To avoid unnecessary re-renders when the object's properties change but not its reference, you can use the useRef
hook. useRef
returns a mutable ref object whose .current
property is initialized to the passed argument (myObject
). This ensures that React doesn't treat it as a new dependency on every render:
function MyComponent() {
const myObjectRef = useRef(myObject);
useEffect(() => {
// do something with myObjectRef.current
// this effect runs only when myObjectRef.current changes
}, [myObjectRef.current]);
// Update myObjectRef.current when myObject changes
useEffect(() => {
myObjectRef.current = myObject;
}, [myObject]);
// rest of your component
}
In this approach:
myObjectRef
is initialized withuseRef(myObject)
, ensuringmyObjectRef.current
always holds the latest reference tomyObject
.- The first
useEffect
watchesmyObjectRef.current
for changes, ensuring the effect runs only when the object's reference changes. - The second
useEffect
updatesmyObjectRef.current
whenevermyObject
changes, so subsequent renders use the updated object reference.
Solution 2: Use a Deep Comparison Library
If your object has deeply nested properties and you want to compare them deeply without manually managing updates, you can use a library like lodash.isEqual
or deep-equal
to perform deep comparisons:
import isEqual from 'lodash/isEqual';
function MyComponent() {
useEffect(() => {
// do something
}, [isEqual(myObject, previousMyObject)]);
}
In this approach:
isEqual
comparesmyObject
andpreviousMyObject
deeply to determine if they are equal.- You need to manage
previousMyObject
to store the previous state ofmyObject
and update it whenmyObject
changes.
Conclusion
Using useRef
is a common and straightforward approach to avoid re-running effects based on object reference changes. It ensures that React considers the object's properties rather than its reference when determining if the effect should run. Alternatively, if you need deep comparison, you can use libraries like lodash.isEqual
for more complex object structures. Each approach has its use case depending on your specific scenario and performance considerations.