React `useEffect` Infinite Loops Usually Mean You Keep Changing the Thing You Told React to Watch and Then Acting Shocked When It Keeps Running
A practical guide to fixing runaway React `useEffect` rerenders by tracing dependency changes, stabilizing values, and separating fetch/setup logic from state that the effect itself mutates.
Why this issue matters: a
useEffectloop is rarely mysterious once you inspect the dependency array honestly. React is usually doing exactly what you told it to do, just more consistently than you expected.
Common symptoms include:
- endless network requests
- repeated console logs
- “Maximum update depth exceeded”
- components that feel like they are constantly reinitializing
The usual pattern is simple:
- the effect depends on a value
- the effect updates that value or something recreated from it
- the dependency changes again
- the effect reruns forever
The classic bad example
useEffect(() => {
setFilters({ ...filters, loaded: true });
}, [filters]);This effect depends on filters, then creates a new filters object, which changes the dependency, which reruns the effect.
That is not a React bug. That is a loop.
The first debugging move
Log the actual dependency values:
useEffect(() => {
console.log('effect deps', filters, userId, query);
}, [filters, userId, query]);You want to know:
- which dependency keeps changing
- whether it is a primitive or a recreated object/function
- whether the effect itself is causing the change
Common fixes
1. Depend on stable primitives instead of whole objects
Instead of:
[filters]use:
[filters.sort, filters.page]when only those fields matter.
2. Do not set state from the same dependency unless guarded
If the update is one-time initialization, use a condition:
useEffect(() => {
if (!loaded) {
setLoaded(true);
}
}, [loaded]);3. Stabilize recreated callbacks or objects
If a dependency is recreated every render:
const options = { limit: 10 };then the effect sees a “new” object every time. Move that object outside, derive it differently, or reduce the dependency to the primitive values that matter.
Data-fetching example
Bad:
useEffect(() => {
fetchData(filters).then(setData);
}, [filters]);if filters is recreated on every render.
Better:
useEffect(() => {
fetchData({ page, sort }).then(setData);
}, [page, sort]);Now the effect tracks the actual variables that should trigger it.
Final recommendation
When useEffect loops, stop asking why React rerenders so much and start asking which dependency changes every time. Most fixes come from stabilizing dependencies, narrowing what the effect watches, or stopping the effect from rewriting its own trigger.