118

There's some data coming from long polling every 5 seconds and I would like my component to dispatch an action every time one item of an array (or the array length itself) changes. How do I prevent useEffect from getting into infinity loop when passing an array as dependency to useEffect but still manage to dispatch some action if any value changes?

useEffect(() => {
  console.log(outcomes)
}, [outcomes])

where outcomes is an array of IDs, like [123, 234, 3212]. The items in array might be replaced or deleted, so the total length of the array might - but don't have to - stay the same, so passing outcomes.length as dependency is not the case.

outcomes comes from reselect's custom selector:

const getOutcomes = createSelector(
  someData,
  data => data.map(({ outcomeId }) => outcomeId)
)
3
  • 1
    Not enough context, please include the code that is actually causing the infinite loop Commented Dec 24, 2019 at 11:16
  • 9
    Spreading the array is no good. App starts with an empty array as default, so useEffect will throw error about different number of dependencies between rerenders. Commented Dec 24, 2019 at 11:32
  • Disappointing that none of these answers work with without warnings from react-hooks/exhaustive-deps Commented Mar 13, 2023 at 19:02

7 Answers 7

193

You can pass JSON.stringify(outcomes) as the dependency list:

Read more here

useEffect(() => {
  console.log(outcomes)
}, [JSON.stringify(outcomes)])
Sign up to request clarification or add additional context in comments.

17 Comments

I'm quite sure this is the answer the OP wants. The idea also isn't made up by me. It's from Dan Abramov (React core team member). If anyone sees there's a problem in my answer, please tell me what it is so that I can improve that answer to be better.
Yeah, I think I will actually give it a go. What's holding me back is 'react-hooks/exhaustive-deps' warning about oucomes being missing from deps. I know it makes no sense to add it since we added JSON.stringify(outcomes) but still
@LoiNguyenHuynh: I know why the answer works and I think it's a good answer, but with all the respect that I have for the React team because React is an amazing framework to build web apps with... this thing of passing arrays or objects to useEffect/useMemo/useCallback's dependencies really keeps coming back to me and I'm starting to get tired of using work-arounds. Is there any way we can discuss improvements somewhere? I don't know where to start.
This causes ESLint to warn both about a missing dependency and a complex expression - both under the react-hooks/exhaustive-deps rule, which I really don't want to ignore. Maybe a custom deeply comparing hook would solve this? I don't know if ESLint would recognize a custom hook.
shouldnt we use .sort() and then stringify? what if the items change but length stayed the same?
|
23

Using JSON.stringify() or any deep comparison methods may be inefficient, if you know ahead the shape of the object, you can write your own effect hook that triggers the callback based on the result of your custom equality function.

useEffect works by checking if each value in the dependency array is the same instance with the one in the previous render and executes the callback if one of them is not. So we just need to keep the instance of the data we're interested in using useRef and only assign a new one if the custom equality check return false to trigger the effect.

function arrayEqual(a1: any[], a2: any[]) {
  if (a1.length !== a2.length) return false;
  for (let i = 0; i < a1.length; i++) {
    if (a1[i] !== a2[i]) {
      return false;
    }
  }
  return true;
}

type MaybeCleanUpFn = void | (() => void);

function useNumberArrayEffect(cb: () => MaybeCleanUpFn, deps: number[]) {
  const ref = useRef<number[]>(deps);

  if (!arrayEqual(deps, ref.current)) {
    ref.current = deps;
  }

  useEffect(cb, [ref.current]);
}

Usage

function Child({ arr }: { arr: number[] }) {
  useNumberArrayEffect(() => {
    console.log("run effect", JSON.stringify(arr));
  }, arr);

  return <pre>{JSON.stringify(arr)}</pre>;
}

Taking one step further, we can also reuse the hook by creating an effect hook that accepts a custom equality function.

type MaybeCleanUpFn = void | (() => void);
type EqualityFn = (a: DependencyList, b: DependencyList) => boolean;

function useCustomEffect(
  cb: () => MaybeCleanUpFn,
  deps: DependencyList,
  equal?: EqualityFn
) {
  const ref = useRef<DependencyList>(deps);

  if (!equal || !equal(deps, ref.current)) {
    ref.current = deps;
  }

  useEffect(cb, [ref.current]);
}

Usage

useCustomEffect(
  () => {
    console.log("run custom effect", JSON.stringify(arr));
  },
  [arr],
  (a, b) => arrayEqual(a[0], b[0])
);

Live Demo

Edit 59467758/passing-array-to-useeffect-dependency-list

7 Comments

This is the correct way to solve the problem (using useRef).
This answer is wrong, useRef().current can not be used in a dependency array: medium.com/welldone-software/…
@flyingsheep It's your argument that is wrong. Did you read the code and try running the demo? ref.current is a middle man variable that is used to store the new value which invokes the effect callback depend on the equality check. The one that triggers a re-render is the dependency value from the outside, not ref.current.
The answer contains useEffect(cb, [ref.current]), which is equivalent to useEffect(cb).
Thank you for the exhaustive code and explanation @NearHuscarl, but I can't help being disgusted and refuse to write so much code for something that should be shipped out of the box. AFAIC I will JSON.stringify it and silence the warning.
|
14

Another ES6 option would be to use template literals to make it a string. Similar to JSON.stringify(), except the result won't be wrapped in []

useEffect(() => {
  console.log(outcomes)
}, [`${outcomes}`])

Another option, if the array size doesn't change, would be to spread it in:

useEffect(() => {
  console.log(outcomes)
}, [ ...outcomes ])

4 Comments

Be aware that if you mix string with number in the array, the check may fail because `${[1,2,3]}` is the same as `${[1,2,'3']}`. But storing values with different types in the same array is a bad idea anyway..
Unfortunately spreading returns a warning to me: React Hook useEffect has a spread element in its dependency array. This means we can't statically verify whether you've passed the correct dependencies
The first one is the same as writing outcomes.join()
I am using string literals approach for my array of object- and when I tried to console this` ${outcomes} ` it returned something like- ` '[object Object]' `, I just want to confirm that this still is the deep comparison right? or is it considering just length of array?
6

As an addendum to loi-nguyen-huynh's answer, for anyone encountering the eslint exhaustive-deps warning, this can be resolved by first breaking the stringified JSON out into a variable:

const depsString = JSON.stringify(deps);
React.useEffect(() => {
    ...
}, [depsString]);

1 Comment

You can't use deps inside the hook without getting a different exhaustive-deps warning, however. You also get a warning for not using the depsString in the hook. Is your answer to have to JSON.parse it within the effect? That seems ridiculous, honestly.
4

I would recommend looking into this OSS package which was created to address the exact issue you describe (deeply comparing the values in the dependency array instead of shallow):

https://github.com/kentcdodds/use-deep-compare-effect

The usage/API is exactly the same as useEffect but it will compare deeply.

I would caution you however to not use it where you don't need it because it has the potential to result in a performance degredation due to unnecessary deep comparisons where a shallow one would do.

Comments

0

The solution I am suggesting here will match some scenarios. What I am doing for these kinds of issues is to memoize the variable and then useEffect will not fire indefinitely.

Please note this is not 100% match to the question asked above but I guess this will help someone.

const roles = useMemo(() => [EntityRole.Client, EntityRole.Agency], []);

useEffect(() => {
  console.log(roles)
}, [roles])

1 Comment

what is EntityRole.Client, EntityRole.Agency here?
0

To fix linting issue, you can parse stingify variable within useEffect

  const getNames = (names) => {
      // stingify names list first
      const nameStr = JSON.stringify(names);

      useEffect(() => {
          // use stingify variable so useEffect don't have dependency on names
          const nameList = JSON.parse(nameStr);
          console.log(nameList);
      }, [nameStr]);
  };

1 Comment

I hate it, but it does seem to be the simplest approach.

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.