68

Why is an infinite loop created when I pass a function expression into the useEffect dependency array? The function expression does not alter the component state, it only references it.

// component has one prop called => sections

const markup = (count) => {
    const stringCountCorrection = count + 1;
    return (
        // Some markup that references the sections prop
    );
};

// Creates infinite loop
useEffect(() => {
    if (sections.length) {
        const sectionsWithMarkup = sections.map((section, index)=> markup(index));
        setSectionBlocks(blocks => [...blocks, ...sectionsWithMarkup]);
    } else {
        setSectionBlocks(blocks => []);
    }
}, [sections, markup]);

If markup altered state I could understand why it would create an infinite loop but it does not it simply references the sections prop.

So I'm not looking for a code related answer to this question. If possible I'm looking for a detailed explanation as to why this happens.

I'm more interested in the why then just simply finding the answer or correct way to solve the problem.

Why does passing a function in the useEffect dependency array that is declared outside of useEffect cause a re-render when both state and props aren't changed in said function?

4
  • 4
    Maybe I am late for the party. But wanted to know why someone would be passing function in dependency of useEffect ? Commented Aug 8, 2022 at 8:39
  • @SaurabhBayani, see stackoverflow.com/questions/71814755/…
    – dan
    Commented Nov 24, 2022 at 15:02
  • I learned why when I learned the useCallback hook. But I don't understand what is any potential benefit that people would like to use a function within the dependency array, (especially when you think it's supposed to be immutable)
    – kakacii
    Commented Dec 5, 2022 at 12:29
  • 1
    Why do you need markup to be a dependency in this case? You know it is going to change reference on every render. So, if you need to depend on it, don't use useEffect. If you don't need to depend on it, don't include it. Putting in a useCallback in this case is equivellent to not including it in the dependency list. Even if you have something in the useCallback dependency list, just move that to the useEffect dependencies. IMO, there is no reason to include it in the dependency list and there is no reason to use useCallback.
    – Jordan
    Commented Dec 9, 2022 at 17:43

3 Answers 3

105

The issue is that upon each render cycle, markup is redefined. React uses shallow object comparison to determine if a value updated or not. Each render cycle markup has a different reference. You can use useCallback to memoize the function though so the reference is stable. Do you have the react hook rules enabled for your linter? If you did then it would likely flag it, tell you why, and make this suggestion to resolve the reference issue.

const markup = useCallback(
  (count) => {
    const stringCountCorrection = count + 1;
    return (
      // Some markup that references the sections prop
    );
  },
  [/* any dependencies the react linter suggests */]
);

// No infinite looping, markup reference is stable/memoized
useEffect(() => {
  if (sections.length) {
    const sectionsWithMarkup = sections.map((section, index) => markup(index));
    setSectionBlocks(blocks => [...blocks, ...sectionsWithMarkup]);
  } else {
    setSectionBlocks([]);
  }
}, [sections, markup]);

Alternatively if the markup function is only used in the useEffect hook you can move it directly into the hook callback to remove it as an external dependency for the hook.

Example:

useEffect(() => {
  const markup = (count) => {
    const stringCountCorrection = count + 1;
    return (
      // Some markup that references the sections prop
    );
  };

  if (sections.length) {
    const sectionsWithMarkup = sections.map((section, index) => markup(index));
    setSectionBlocks(blocks => [...blocks, ...sectionsWithMarkup]);
  } else {
    setSectionBlocks([]);
  }
}, [sections, /* any other dependencies the react linter suggests */]);

Additionally, if the markup function has absolutely no external dependencies, i.e. it is a pure function, then it could/should be declared outside any React component.

14
  • That makes complete sense! Thank you so much. I was definitely thinking about the fact that the function expression is re-declared every time the component re-renders.
    – mmason33
    Commented Jun 26, 2020 at 19:35
  • In no tutorial that just tells to pass the function I could find why infinite loops are happening. Thanks a lot for the simple demonstration. How about passing a function prop from the parent. Should the function be memoized at the parent?
    – kuzdogan
    Commented Nov 18, 2020 at 11:54
  • 3
    @kuzdogan If you read the useCallback docs they explain that one of primary uses of the hook is to "return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders." You should write and pass your callbacks as normal and only reach for the memoization of them when necessary. The useMemo hook documentation explains the reason for this in a bit more detail.
    – Drew Reese
    Commented Nov 18, 2020 at 16:24
  • 2
    @DrewReese I found this solution helpful. Basically had to memoize where the function was being create in context rather than the component. stackoverflow.com/questions/64104731/…
    – Barry G
    Commented Jun 2, 2021 at 19:05
  • 1
    @DrewReese This answer probably saved me from many upcoming errors.
    – batatop
    Commented Jan 28, 2022 at 17:40
9

Why is an infinite loop created when I pass a function expression

The "infinite loop" is the component re-rendering over and over because the markup function is a NEW function reference (pointer in memory) each time the component renders and useEffect triggers the re-render because it's a dependency.

The solution is as @drew-reese pointed out, use the useCallback hook to define your markup function.

1
1

I faced issues and bugs when I used to add functions into the dependency array of useEffect. I created a simple custom hook that solves the problem of javascript closures (function in useEffect needs point to fresh objects), I separated the use of reactive elements and elements that need to be fresh to have in useEffect dependency array only things that I wanted to react to and still use other state/functions from component without adding it to useEffect dependency array.

hooks.ts

export const useObjectRef = <T extends any[]>(...objects: T): React.MutableRefObject<T> => {
    const objectsRef = useRef<T>(objects);
    objectsRef.current = objects;
    return objectsRef;
};

ExampleComponent.tsx

export const ExampleComponent: FC = () => {
    const [stateA, setStateA] = useState()
    const [stateB, setStateB] = useState()

    const doSomething = () => {
        // Do something
    }
    
    // Run only on stateB change
    // But still have access to fresh stateA and doSomething function
    const objRef = useObjectRef(stateA, doSomething)
    useEffect(() => {
        const [stateA, doSomething] = objRef.current
        doSomething();
        console.log(stateA);
    }, [objRef, stateB])

    return (
        ... JSX ...
    )
}

Maybe this will help someone :)

Not the answer you're looking for? Browse other questions tagged or ask your own question.