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:

These implementations confirm that React performs reference and shallow equality checks before deciding to execute an effect.

Summary

  • React compares useEffect dependency arrays using shallow equality (areHookInputsEqual), checking each entry with Object.is.
  • Creating new array literals on every render changes the reference and triggers the effect unnecessarily.
  • Use useMemo to stabilize the array reference, recalculating only when underlying values change.
  • Use useRef when you need imperative control over the array contents without triggering re-renders.
  • Avoid JSON.stringify for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →