use object in useEffect 2nd param without having to stringify it

ghz 6months ago ⋅ 86 views

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 with useRef(myObject), ensuring myObjectRef.current always holds the latest reference to myObject.
  • The first useEffect watches myObjectRef.current for changes, ensuring the effect runs only when the object's reference changes.
  • The second useEffect updates myObjectRef.current whenever myObject 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 compares myObject and previousMyObject deeply to determine if they are equal.
  • You need to manage previousMyObject to store the previous state of myObject and update it when myObject 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.