Passing array to useEffect dependency list

ReactjsReact Hooks

Reactjs Problem Overview


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)
)

Reactjs Solutions


Solution 1 - Reactjs

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

Read more here

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

Solution 2 - Reactjs

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

Solution 3 - Reactjs

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 ])

Solution 4 - Reactjs

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]);

Solution 5 - Reactjs

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser0101View Question on Stackoverflow
Solution 1 - ReactjsLoi Nguyen HuynhView Answer on Stackoverflow
Solution 2 - ReactjsNearHuscarlView Answer on Stackoverflow
Solution 3 - ReactjsiPzardView Answer on Stackoverflow
Solution 4 - ReactjsStephen PaulView Answer on Stackoverflow
Solution 5 - ReactjsdreadwailView Answer on Stackoverflow