29

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.

7
  • 1
    Does your object have nested properties, or are there just simple key value pairs in it? Commented Apr 23, 2019 at 10:04
  • 1
    @Tholle it has nested properties. (I will edit my question) Commented Apr 23, 2019 at 10:05
  • You could avoid generating new object (modifying the reference) / check any identifier / check a hash / check the size of the object Commented Apr 23, 2019 at 10:11
  • where is this object coming from? Why is it recreated every time? I think those are the crux of your problem, not useEffect. Commented Apr 23, 2019 at 10:21
  • 1
    on v16.8+ useEffect(() => {// do something}, [myObject]) is acctually doing something on myObject changes Commented Jul 19, 2019 at 2:59

5 Answers 5

23

You could create a custom hook that keeps track of the previous dependency array in a ref and compares the objects with e.g. Lodash isEqual and only runs the provided function if they are not equal.

Example

const { useState, useEffect, useRef } = React;
const { isEqual } = _;

function useDeepEffect(fn, deps) {
  const isFirst = useRef(true);
  const prevDeps = useRef(deps);

  useEffect(() => {
    const isFirstEffect = isFirst.current;
    const isSame = prevDeps.current.every((obj, index) =>
      isEqual(obj, deps[index])
    );

    isFirst.current = false;
    prevDeps.current = deps;

    if (isFirstEffect || !isSame) {
      return fn();
    }
  }, deps);
}

function App() {
  const [state, setState] = useState({ foo: "foo" });

  useEffect(() => {
    setTimeout(() => setState({ foo: "foo" }), 1000);
    setTimeout(() => setState({ foo: "bar" }), 2000);
  }, []);

  useDeepEffect(() => {
    console.log("State changed!");
  }, [state]);

  return <div>{JSON.stringify(state)}</div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

Sign up to request clarification or add additional context in comments.

5 Comments

This is a very cool answer thanks for sharing. Question though why not use useState instead of useRef.
@ThomasValadez By using the function returned from useState the component will be re-rendered, which is not desired in this case.
Hi @Tholle, really good example. Just to ask one thing. This example which you show, I think it would work out of the box, without any checking with _isEqual method, because object is not nested. I think we have this issue, only with nested object. Please correct me if I'm wrong?
This implementation can be improved. I think there's a bug in how it unsubscribes using the cleanup function but doesn't resubscribe if the deps are deep equal but reference unequal. Copy this instead: github.com/streamich/react-use/blob/master/src/…
Is this method faster than using JSON.stringify() in the second param of the useEffect hook?
4

The above answer by @Tholle is absolutely correct. I wrote a post regarding the same on dev.to

In React, side effects can be handled in functional components using useEffect hook. In this post, I'm going to talk about the dependency array which holds our props/state and specifically what happens in case there's an object in the dependency array.

The useEffect hook runs even if one element in the dependency array changes. React does this for optimisation purposes. On the other hand, if you pass an empty array then it never re-runs.

However, things become complicated if an object is present in this array. Then even if the object is modified, the hook won't re-run because it doesn't do deep object comparison between these dependency changes for that object. There are couple of ways to solve this problem.

  1. Use lodash's isEqual method and usePrevious hook. This hook internally uses a ref object that holds a mutable current property that can hold values.

    It’s possible that in the future React will provide a usePrevious Hook out of the box since it is a relatively common use case.

    const prevDeeplyNestedObject = usePrevious(deeplyNestedObject)
    useEffect(()=>{
        if (
            !_.isEqual(
                prevDeeplyNestedObject,
                deeplyNestedObject,
            )
        ) {
            // ...execute your code
        }
    },[deeplyNestedObject, prevDeeplyNestedObject])
    
  2. Use useDeepCompareEffect hook as a drop-in replacement for useEffect hook for objects

    import useDeepCompareEffect from 'use-deep-compare-effect'
    ...
    useDeepCompareEffect(()=>{
        // ...execute your code
    }, [deeplyNestedObject])
    
  3. Use useCustomCompareEffect hook which is similar to solution #2

I prepared a CodeSandbox example related to this post. Fork it and check it yourself.

Comments

3

Your best bet is to use useDeepCompareEffect from react-use. It's a drop-in replacement for useEffect.

const {useDeepCompareEffect} from "react-use";

const App = () => {
    useDeepCompareEffect(() => {
        // ...
    }, [someObject]);

    return (<>...</>);
};

export default App;

Comments

0

Plain (not nested) object in dependency array

I just want to challenge these two answers and to ask what happen if object in dependency array is not nested. If that is plain object without properties deeper then one level.

In my opinion in that case, useEffect functionality works without any additional checks.

I just want to write this, to learn and to explain better to myself if I'm wrong. Any suggestions, explanation is very welcome.

Here is maybe easier to check and play with example: https://codesandbox.io/s/usehooks-bt9j5?file=/src/App.js

const {useState, useEffect} = React;

function ChildApp({ person }) {
  useEffect(() => {
    console.log("useEffect ");
  }, [person]);

  console.log("Child");
  return (
    <div>
      <hr />
      <h2>Inside child</h2>
      <div>{person.name}</div>
      <div>{person.age}</div>
    </div>
  );
}
function App() {
  const [person, setPerson] = useState({ name: "Bobi", age: 29 });
  const [car, setCar] = useState("Volvo");

  function handleChange(e) {
    const variable = e.target.name;
    setPerson({ ...person, [variable]: e.target.value });
  }

  function handleCarChange(e) {
    setCar(e.target.value);
  }

  return (
    <div className="App">
      Name:
      <input
        name="name"
        onChange={(e) => handleChange(e)}
        value={person.name}
      />
      <br />
      Age:
      <input name="age" onChange={(e) => handleChange(e)} value={person.age} />
      <br />
      Car: <input name="car" onChange={(e) => handleCarChange(e)} value={car} />
      <ChildApp person={person} />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-
dom.development.js"></script>
<div id="root"></div>

Comments

0

You can just expand the properties in the useEffect array:

var obj = {a: 1, b: 2};

useEffect(
  () => {
    //do something when any property inside "a" changes
  },
  Object.entries(obj).flat()
);

Object.entries(obj) returns an array of pairs ([["a", 1], ["b", 2]]) and .flat() flattens the array into: ["a", 1, "b", 2]

Note that the number of properties in the object must remain constant because the length of the array cannot change or else useEffect will throw an error.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.