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