63

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?

isherwood
  • 58,414
  • 16
  • 114
  • 157
mmason33
  • 878
  • 1
  • 6
  • 10
  • 3
    Maybe I am late for the party. But wanted to know why someone would be passing function in dependency of `useEffect` ? – Saurabh Bayani Aug 08 '22 at 08:39
  • @SaurabhBayani, see https://stackoverflow.com/questions/71814755/use-case-for-passing-function-as-an-dependency-in-useeffect-in-react – dan Nov 24 '22 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 Dec 05 '22 at 12:29
  • 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 Dec 09 '22 at 17:43

2 Answers2

97

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
    );
  },
  [count, /* and any other 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(blocks => []);
    }
}, [sections, markup]);
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 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 Jun 26 '20 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 Nov 18 '20 at 11:54
  • 2
    @kuzdogan If you read the [useCallback](https://reactjs.org/docs/hooks-reference.html#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 Nov 18 '20 at 16:24
  • I tried memoizing a wrapped `dispatch` from useReducer passed through context and this solution does not work. It continues to loop. – Barry G Jun 02 '21 at 11:13
  • Hi @BarryG, this might be from how you've defined the context provider and how you pass the context value. Feel free to ask a new question on SO and ping me here with a link to it and I can take a look. – Drew Reese Jun 02 '21 at 15:23
  • 2
    @DrewReese I found this solution helpful. Basically had to memoize where the function was being create in context rather than the component. https://stackoverflow.com/questions/64104731/usecallback-and-useeffect-infinite-loop – Barry G Jun 02 '21 at 19:05
  • 1
    @DrewReese This answer probably saved me from many upcoming errors. – batatop Jan 28 '22 at 17:40
  • Just don't add functions to the dependency list. No one has ever given me a good reason for why we do this, and I think its dumb. I've never run into a problem with not including functions. Like ever. When I do include, my code inevitably breaks. And I don't like using useMemo or useCallback on every function I write. That is so hard to read. Don't listen to every rule out there. Some are just wrong, and no one wants to say they are because they don't want to seem dumb. Functions aren't state! Period. – Jordan Nov 29 '22 at 04:57
  • @Jordan When you *are* adding dependencies that are functions and it's breaking "stuff" it's usually because the functions ***are not*** stable references. Get used to use cases where `useMemo` and `useCallback` are helpful, just like with any other React hook. The rules exist generally for a reason, to save you from yourself, i.e. to keep you from writing bad code. Only memoize what you need to, when there's an actual problem to solve by using it. – Drew Reese Nov 29 '22 at 05:02
  • @DrewReese, I'm just not going to include functions in the decency list. I'm not going to have useMemo and useCallback hooks all over the place making my code that much more unreadable. You do not have to add functions to the decency list. Functions aren't state and they do not change. I have never seen a good reason to add them. – Jordan Dec 01 '22 at 18:10
  • @Jordan Sure, you are completely free to disregard the Rules of Hooks and established patterns *if you want*. Just FYI, React hook dependencies have nothing to do with React state, but rather any external reference. If you redeclare a function each render cycle then yes, the reference ***does*** actually change. Best of luck to you. Cheers. – Drew Reese Dec 02 '22 at 03:30
  • @DrewReese, but what would be the benefit of rerunning useEffect when that reference changes? That's what I mean by state. The only reason that useEffect would ever have to rerun is if state changes. Not some reference somewhere. How the heck are people using useEffect? It's for resources and side-effects to the otherwise functional flow of the app. I _really want to know why_, but no one has ever given me a real answer. Everyone says to do this, and I just can't see why. – Jordan Dec 09 '22 at 04:32
  • @Jordan React hooks run each and every render cycle, unconditionally. If you are talking specifically about a `useEffect` hook's *callback function* being invoked because a dependency changed, that's a different thing. The reason you include functions in the dependency array is to close over the new function reference that is closing over any new references of its own. In other words, it addresses the issue of stale enclosures. Again, the point of using the `useCallback` hook is to provide a stable function reference so it triggers the effect callback only when it needs to. – Drew Reese Dec 09 '22 at 05:05
  • @DrewReese Yeah, I meant the callback function inside `useEffect`. Either there is some pattern in JavaScript or React that I just don't us or people getting this error are doing something really peculiar. This just doesn't smell right. Even when I use closures, they are coeval with my function. You run into problems and memory leaks when your closures are not at least internally atomic. I'll just keep on, I guess. – Jordan Dec 09 '22 at 17:36
8

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.

Cory Robinson
  • 4,616
  • 4
  • 36
  • 53
  • but what if you're storing functions in state (https://stackoverflow.com/questions/55621212/is-it-possible-to-react-usestate-in-react)? You can store a useCallback hook inside of state? – ANimator120 Feb 11 '23 at 05:07