Home > Software design >  React maximum update depth exceeded | useEffect dependencies
React maximum update depth exceeded | useEffect dependencies

Time:09-23

I have a certain piece of code that I want to run when any of the dependencies change. One of those dependencies is not being used inside the effect

  const { contractParams, serviceInstance } = entityStore;
  useEffect(() => {
        const [first, second, third, fourth] = getParamDetails(contractParams);
        setFirst(first);
        setSecond(second);
        setThird(third);
        setFourth(fourth);
    }, [contractParams, serviceInstance]);

I want to run this code or trigger a re-render when any of them changes. However, i understand since I am not referring serviceInstance inside this useEffect, its not exactly a dependency per se. However, when I run it, I get this error

Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

If I remove serviceInstance, it goes away.Both of the dependencies are array. Is it treating serviceInstance a diff value on every update? What am I missing? Any suggestions are appreciated

CodePudding user response:

There are several points to be covered here:

Array and object equality testing

Arrays & Objects will often (re-)trigger a useEffect even if they have not changed. The reason for this is that Javascript is not capable of assessing equality on such structures. Consider these two:

["a", "b"] === ["a", "b"] // <-- false
{a: "a", b: "b"} === {a: "a", b: "b"} // <-- also false

If the nesting in your serviceInstance is relatively shallow, you could manually decide which values should in fact trigger a refresh.

You mention serviceInstance is an array, so perhaps something like:

const [prop1, _, prop3] = serviceInstance;

useEffect(() => { 
  // code
}, [prop1, prop3])

Another solution is to use a deep compare useEffect variant, something like: https://github.com/kentcdodds/use-deep-compare-effect

Please note though, as Dan Abramov himself states, there's a reason why useEffect doesn't work that way. Depending on how your data is structured, this can really create a bottleneck in the performance of your app.

Side effects

As Alia pointed out in the comments, if one of the setters in your effect actually ends up changing the contents of serviceInstance it will create a loop.

You should consider separating concerns:

When the contractParams change, one useEffect will set the params. You can then use a second useEffect that only looks at serviceInstance (or as seen above, parts of it) to then trigger a refresh.

  import uuid from "uuid";

  const [componentKey, setComponentKey] = useState(uuid())
  const { contractParams, serviceInstance } = entityStore;
  const [first, second, third, fourth] = getParamDetails(contractParams);
  useEffect(() => {
        setFirst(first);
        setSecond(second);
        setThird(third);
        setFourth(fourth);
    }, [first, second, third, fourth]);

  const [prop1, _, prop3] = serviceInstance;
  useEffect(() => {
     // refresh code
    setComponentKey(uuid());

  },[prop1, prop3]);

  return <YourComponent key={componentKey}/>

using something like https://www.npmjs.com/package/uuid to manage your uuid

Please note the above code is purely theoretical, as I do not know how you intend to use these values. If serviceInstance is being used in the render part of your component, then it should trigger a refresh on its own. You may need to rethink the structure of said component.

Edit: Dependencies on hooks

As a side note, depending on the strictly your react linter is set to enforce (eg: react-hooks/exhaustive-deps), using variables that are not used within the hook will throw a warning. This is usually a good sign that something is structurally wrong with the code.

CodePudding user response:

Well, from what I've gathered so far you want to trigger an useEffect hook by some additional dependency, that is an array, and that by including it triggers render looping.

I'm assuming that something in the effect callback is causing this array to be updated and returned back to the component as a new array reference.

To skirt this array reference equality check you can stringify the array and instead uses this as the additional triggering dependency. Only when the array serializes to a new string will it trigger the effect.

Exchange between Kent Dodds and Dan Abramov regarding the "fast-deep-equal"

Dan suggests if the deep equality is shallow enough to just use JSON.stringify(obj).

Example:

const a = ["a", "b"];
const b = ["a", "b"];

console.log(a === b); // false, different array reference
console.log(JSON.stringify(a) === JSON.stringify(b)); // true, serialized equality

Code:

Note, as @ltroller points out, that if you've the React-hooks linter installed as part of your project that it should complain about the extraneous dependencies. If this is the case you can disable the linter rule for that line, but use caution as this will disable all the dependency checks should the hook logic ever be updated and your dependencies do actually change.

const { contractParams, serviceInstance } = entityStore;

useEffect(() => {
  const [first, second, third, fourth] = getParamDetails(contractParams);
  setFirst(first);
  setSecond(second);
  setThird(third);
  setFourth(fourth);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [contractParams, JSON.stringify(serviceInstance)]);

Naturally, if you need more customized equality like wanting ["a", "b"] and ["b", "a"] to be considered equal, then you can write a custom serializer to, for example, sort then stringify the result for equality comparison.

  • Related