Passing a function in the useEffect dependency array causes infinite loop
Asked Answered
M

3

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?

Mossman answered 26/6, 2020 at 19:21 Comment(4)
Maybe I am late for the party. But wanted to know why someone would be passing function in dependency of useEffect ?Headdress
@SaurabhBayani, see #71815255Hives
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)Schrock
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.Depreciation
C
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.

Conch answered 26/6, 2020 at 19:29 Comment(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.Mossman
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?Octoroon
@Octoroon 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.Conch
I tried memoizing a wrapped dispatch from useReducer passed through context and this solution does not work. It continues to loop.Robbynrobe
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.Conch
@DrewReese I found this solution helpful. Basically had to memoize where the function was being create in context rather than the component. #64105231Robbynrobe
@DrewReese This answer probably saved me from many upcoming errors.Curnin
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.Depreciation
@Depreciation 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.Conch
@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.Depreciation
@Depreciation 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.Conch
@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.Depreciation
@Depreciation 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.Conch
@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.Depreciation
A
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.

Amberjack answered 18/11, 2020 at 16:16 Comment(1)
but what if you're storing functions in state (#55621712)? You can store a useCallback hook inside of state?Chough
E
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 :)

Endlong answered 1/12, 2023 at 20:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.