How to Pass a Dynamic Array to the React useEffect Dependency Array Without Infinite Loops
To pass a dynamic array to useEffect without triggering the effect on every render, you must stabilize the array's reference using useMemo or useRef so that React's shallow equality check only detects changes when the actual contents differ.
When working with the facebook/react codebase, understanding how the dependency array comparison works is essential for avoiding unnecessary effect executions. The useEffect hook relies on a shallow equality algorithm to determine whether its callback should run, which means the reference identity of an array matters just as much as its contents.
Why Array References Trigger useEffect Re-runs
React determines whether to execute an effect by comparing the current dependency array with the previous one using a function called areHookInputsEqual in packages/react-reconciler/src/ReactFiberHooks.js (lines 53-99). This function iterates over both arrays and compares each entry using Object.is equality.
If you create a new array literal on every render—such as [item1, item2]—the reference changes even if the contents are identical. Because areHookInputsEqual first checks if the arrays are the same reference (lines 53-60), and then performs shallow comparison, a new array reference forces React to re-run the effect.
Stabilizing Dynamic Arrays with useMemo
The recommended approach for passing a dynamic array to useEffect is to memoize the array using useMemo. This ensures the array reference remains stable unless the underlying values actually change.
In packages/react/src/ReactHooks.js (lines 87-101), the useEffect implementation forwards the dependency array to the reconciler. By ensuring your array is memoized before it reaches this point, you satisfy the shallow equality check in areHookInputsEqual.
import { useEffect, useMemo } from 'react';
function DataProcessor({ items }) {
// Stabilize the array: only recalculate when items change
const processedIds = useMemo(() =>
items.map(item => item.id),
[items]
);
useEffect(() => {
// This only runs when the actual ids change, not on every render
console.log('Fetching data for ids:', processedIds);
fetchData(processedIds);
}, [processedIds]); // Stable reference thanks to useMemo
}
Using useRef for Imperative Array Management
For scenarios where you need to mutate the array imperatively or avoid the overhead of memoization calculations, useRef provides a stable container. The .current property of a ref maintains the same reference across renders, allowing you to manually control when the array changes.
import { useEffect, useRef } from 'react';
function ManualUpdater({ data }) {
const depsRef = useRef([]);
// Manually check if contents changed before updating ref
if (data.length !== depsRef.current.length ||
data.some((v, i) => v !== depsRef.current[i])) {
depsRef.current = data.slice(); // Create new array only when needed
}
useEffect(() => {
console.log('Effect triggered by data change');
processData(depsRef.current);
}, [depsRef.current]); // Stable until manually updated
}
Avoiding the JSON.stringify Anti-pattern
While you may encounter suggestions to use JSON.stringify(array) as a dependency, this approach is not recommended for production code. Serialization is computationally expensive and can produce false positives for non-primitive values like functions or class instances.
// ❌ Avoid this pattern
useEffect(() => {
// Effect logic
}, [JSON.stringify(myArray)]); // Expensive and unreliable
Instead, rely on useMemo to create a stable primitive dependency array derived from your dynamic content.
Key Implementation Files in React
Understanding the internal mechanics helps explain why reference stability matters:
packages/react-reconciler/src/ReactFiberHooks.js(lines 53-99): ContainsareHookInputsEqual, the shallow equality function that compares dependency arrays usingObject.is.packages/react/src/ReactHooks.js(lines 87-101): Defines theuseEffectexport that forwards the callback and dependencies to the reconciler's dispatcher.
These implementations confirm that React performs reference and shallow equality checks before deciding to execute an effect.
Summary
- React compares
useEffectdependency arrays using shallow equality (areHookInputsEqual), checking each entry withObject.is. - Creating new array literals on every render changes the reference and triggers the effect unnecessarily.
- Use
useMemoto stabilize the array reference, recalculating only when underlying values change. - Use
useRefwhen you need imperative control over the array contents without triggering re-renders. - Avoid
JSON.stringifyfor dependency arrays due to performance costs and reliability issues.
Frequently Asked Questions
Why does useEffect run infinitely when I pass a new array literal?
When you write useEffect(() => {...}, [newArray]) where newArray is defined inline like [item1, item2], JavaScript creates a new array object on every render. React's areHookInputsEqual function detects this reference change and assumes the dependencies changed, triggering the effect. The effect then likely updates state, causing another render and creating another new array, resulting in an infinite loop.
Is useMemo the best way to stabilize an array for useEffect?
Yes, useMemo is the idiomatic React solution for this problem. It aligns with React's declarative paradigm and hook rules. By placing the individual values that determine your array's contents in useMemo's dependency list, you ensure the array reference only changes when those underlying values actually change. This satisfies React's shallow equality check in areHookInputsEqual without manual reference management.
Can I use useRef instead of useMemo for dynamic arrays?
You can use useRef as an alternative, particularly when you need imperative control or want to avoid the overhead of memoization calculations. However, useRef requires manual synchronization—you must explicitly check if the contents changed before updating ref.current. Unlike useMemo, useRef does not automatically respond to prop or state changes, making it more error-prone for declarative data flows. Use useRef only when useMemo is insufficient for your specific use case.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →