Problem Solved

React · infinite loop

Home › Courses › React › Problems › Infinite loop
⚛️ Problem · 5 min read

"Maximum update depth exceeded."
Three causes, three fixes.

React stops after 50 nested updates and throws. The browser freezes for a second, then the console fills with warnings. This error means your component is causing renders faster than React can complete them — an infinite loop. There are exactly three causes, and each has a distinct fix. This page covers all three.

Short answer: Find which component is looping (React's stack trace tells you). Then check for three patterns: (1) setState in render — move to an event handler or useEffect. (2) unstable dep in useEffect — wrap functions in useCallback, objects in useMemo. (3) effect sets state that's in its own deps — use functional setState, add a guard, or move the value to a ref. Once you fix the cause, the loop stops.

✅ Solution 1 — setState in render

The most obvious cause — and the easiest to fix.

If you call setState during render, React schedules an update before the render finishes, which triggers a new render, which calls setState again — infinite loop. The fix: move the setState out of render, into an event handler or useEffect. Or, better, remove the state entirely and compute the value.

setState-in-render.tsx
import { useState } from 'react';

// ─── BAD: setState in render ─────────────────
function BadCounter() {
const [count, setCount] = useState(0);

setCount(count + 1);
// ⚠️ Maximum update depth exceeded
// Render → setState → re-render → setState → ...

return 

{count}

; } // ─── GOOD: setState in an event handler ────── function GoodCounter() { const [count, setCount] = useState(0); function increment() { setCount(c => c + 1); // ✅ called from a click } return ; } // ─── GOOD: derived value, no state ─────────── function DerivedExample() { const [count, setCount] = useState(0); // If a value is derived, compute it in render. // Don't store it in state — that causes the loop. const doubled = count * 2; // ✅ derived, no loop return (

Count: {count}, Doubled: {doubled}

); } // ─── BAD variant: setState inside .map() ───── function BadItems({ items }: { items: string[] }) { const [valid, setValid] = useState<string[]>([]); items.forEach(item => { if (item.length > 0) { setValid(v => [...v, item]); // ⚠️ setState during render } }); return
    {valid.map(v =>
  • {v}
  • )}
; } // ─── GOOD: compute during render ───────────── function GoodItems({ items }: { items: string[] }) { // ✅ Filter during render — no state, no loop const valid = items.filter(item => item.length > 0); return
    {valid.map(v =>
  • {v}
  • )}
; } console.log(BadCounter, GoodCounter, DerivedExample, BadItems, GoodItems);
import { useState } from 'react';

function BadCounter() {
const [count, setCount] = useState(0);
setCount(count + 1);
return React.createElement('p', null, count);
}

function GoodCounter() {
const [count, setCount] = useState(0);
function increment() {
setCount(c => c + 1);
}
return React.createElement('button', { onClick: increment }, 'Count: ', count);
}

function DerivedExample() {
const [count, setCount] = useState(0);
const doubled = count * 2;
return React.createElement('div', null,
React.createElement('p', null, 'Count: ', count, ', Doubled: ', doubled),
React.createElement('button', { onClick: () => setCount(c => c + 1) }, '+1')
);
}

function BadItems({ items }) {
const [valid, setValid] = useState([]);
items.forEach(item => {
if (item.length > 0) {
setValid(v => [...v, item]);
}
});
return React.createElement('ul', null, valid.map(v => React.createElement('li', { key: v }, v)));
}

function GoodItems({ items }) {
const valid = items.filter(item => item.length > 0);
return React.createElement('ul', null, valid.map(v => React.createElement('li', { key: v }, v)));
}

console.log(BadCounter, GoodCounter, DerivedExample, BadItems, GoodItems);

setState ≠ render

Render is for computing JSX. setState schedules a new render. Calling it during render creates a loop.

Event handlers are safe

onClick, onChange — these run after render. setState there schedules one render.

Derived values

If a value comes from other state, compute it in render. const doubled = count * 2. Don't store it.

Filtering/mapping

Do it during render. items.filter(...) in render is fine — it's not state, it's a computed value.

✅ Solution 2 — Unstable dependency in useEffect

The most common cause in real codebases.

When a useEffect has a dependency whose identity changes on every render, the effect re-runs on every render. If the effect also updates state, you get a loop: render → effect → setState → render → effect → setState → ... The fix: stabilize the dependency with useCallback, useMemo, or destructuring.

unstable-dep.tsx
import { useState, useEffect, useCallback, useMemo } from 'react';

// ─── BAD: object dep causes loop ─────────────
function BadObjectDep() {
const [data, setData] = useState(0);
const options = { retries: 3 }; // new object every render

useEffect(() => {
setData(d => d + 1);
}, [options]);
// ⚠️ Effect runs every render (options changed)
// Effect calls setData → renders again → options changes → loop

return 

{data}

; } // ─── GOOD: useMemo stabilizes object ───────── function GoodObjectDep() { const [data, setData] = useState(0); const options = useMemo(() => ({ retries: 3 }), []); // ✅ stable useEffect(() => { setData(d => d + 1); }, [options]); // ✅ Effect runs once (options stable) // Effect calls setData → renders again → options unchanged → no loop return

{data}

; } // ─── BAD: function dep causes loop ─────────── function BadFunctionDep() { const [data, setData] = useState(0); const fetchData = () => { setData(d => d + 1); }; useEffect(() => { fetchData(); }, [fetchData]); // ⚠️ Loop — fetchData is new on every render return

{data}

; } // ─── GOOD: useCallback stabilizes function ─── function GoodFunctionDep() { const [data, setData] = useState(0); const fetchData = useCallback(() => { setData(d => d + 1); }, []); // ✅ stable useEffect(() => { fetchData(); }, [fetchData]); // ✅ Effect runs once return

{data}

; } // ─── GOOD: inline the function instead ─────── function InlineFix() { const [data, setData] = useState(0); useEffect(() => { // No external function, no dependency setData(d => d + 1); }, []); // ✅ runs once on mount return

{data}

; } console.log(BadObjectDep, GoodObjectDep, BadFunctionDep, GoodFunctionDep, InlineFix);
import { useState, useEffect, useCallback, useMemo } from 'react';

function BadObjectDep() {
const [data, setData] = useState(0);
const options = { retries: 3 };
useEffect(() => { setData(d => d + 1); }, [options]);
return React.createElement('p', null, data);
}

function GoodObjectDep() {
const [data, setData] = useState(0);
const options = useMemo(() => ({ retries: 3 }), []);
useEffect(() => { setData(d => d + 1); }, [options]);
return React.createElement('p', null, data);
}

function BadFunctionDep() {
const [data, setData] = useState(0);
const fetchData = () => { setData(d => d + 1); };
useEffect(() => { fetchData(); }, [fetchData]);
return React.createElement('p', null, data);
}

function GoodFunctionDep() {
const [data, setData] = useState(0);
const fetchData = useCallback(() => { setData(d => d + 1); }, []);
useEffect(() => { fetchData(); }, [fetchData]);
return React.createElement('p', null, data);
}

function InlineFix() {
const [data, setData] = useState(0);
useEffect(() => { setData(d => d + 1); }, []);
return React.createElement('p', null, data);
}

console.log(BadObjectDep, GoodObjectDep, BadFunctionDep, GoodFunctionDep, InlineFix);

Objects have identity

{ retries: 3 } creates a new object each render — different reference. useMemo returns the same reference until deps change.

Functions have identity

Same for functions. useCallback(fn, []) returns the same function until deps change.

Inline vs stabilize

If the function is only used in the effect, move it inside — no dependency. useCallback is for functions that must be passed to other components.

Primitives don't need memo

Strings, numbers, booleans — count, id, enabled — are compared by value. Adding them to deps is fine.

✅ Solution 3 — Effect sets state that's in its own deps

The subtlest cause — and the most important to know.

The pattern: useEffect(() => setX(...), [x]). The effect reads x, sets x, which changes x, which re-runs the effect — infinite loop. The fix depends on what the effect is trying to do. Often, the state doesn't need to be in useState at all — it's a derived value.

self-dep-effect.tsx
import { useState, useEffect, useRef } from 'react';

// ─── BAD: effect depends on state it sets ────
function BadSelfDep() {
const [items, setItems] = useState<string[]>([]);

useEffect(() => {
// Reads items, sets items — infinite loop
setItems(prev => [...prev, 'new item']);
}, [items]);
// ⚠️ Maximum update depth exceeded

return 
    {items.map(i =>
  • {i}
  • )}
; } // ─── GOOD 1: derive, don't store ───────────── function DerivedFix({ source }: { source: string[] }) { // No state needed — the "derived" list IS source return
    {source.map(i =>
  • {i}
  • )}
; } // ─── GOOD 2: functional setState + guard ───── function GuardedFix() { const [items, setItems] = useState<string[]>([]); useEffect(() => { if (items.length >= 3) return; // stop at 3 const timer = setTimeout(() => { setItems(prev => [...prev, item-${prev.length}`]); }, 1000); return () => clearTimeout(timer); }, [items]); // ✅ guard prevents infinite loop return
    {items.map(i =>
  • {i}
  • )}
; } // ─── GOOD 3: use a ref to track "latest" ───── function RefFix({ value }: { value: number }) { const [history, setHistory] = useState<number[]>([]); const lastValue = useRef(value); useEffect(() => { // Only record when value actually changed if (lastValue.current !== value) { lastValue.current = value; setHistory(h => [...h, value]); } }, [value]); // ✅ depends on prop, guards with ref return

History: {history.join(', ')}

; } // ─── GOOD 4: run once, use setInterval ─────── function IntervalFix() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); // functional — no count dep }, 1000); return () => clearInterval(id); }, []); // ✅ runs once on mount return

{count}

; } console.log(BadSelfDep, DerivedFix, GuardedFix, RefFix, IntervalFix);
import { useState, useEffect, useRef } from 'react';

function BadSelfDep() {
const [items, setItems] = useState([]);
useEffect(() => { setItems(prev => [...prev, 'new item']); }, [items]);
return React.createElement('ul', null, items.map(i => React.createElement('li', { key: i }, i)));
}

function DerivedFix({ source }) {
return React.createElement('ul', null, source.map(i => React.createElement('li', { key: i }, i)));
}

function GuardedFix() {
const [items, setItems] = useState([]);
useEffect(() => {
if (items.length >= 3) return;
const timer = setTimeout(() => {
setItems(prev => [...prev, item-${prev.length}`]);
}, 1000);
return () => clearTimeout(timer);
}, [items]);
return React.createElement('ul', null, items.map(i => React.createElement('li', { key: i }, i)));
}

function RefFix({ value }) {
const [history, setHistory] = useState([]);
const lastValue = useRef(value);
useEffect(() => {
if (lastValue.current !== value) {
lastValue.current = value;
setHistory(h => [...h, value]);
}
}, [value]);
return React.createElement('p', null, 'History: ', history.join(', '));
}

function IntervalFix() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => { setCount(c => c + 1); }, 1000);
return () => clearInterval(id);
}, []);
return React.createElement('p', null, count);
}

console.log(BadSelfDep, DerivedFix, GuardedFix, RefFix, IntervalFix);

Don't store derived

If x comes from other state, don't put it in useState. Compute during render. Eliminates the need for the effect entirely.

Functional setState + guard

setX(prev => ...) avoids stale closure. The guard (if (x.length >= 3) return) stops the loop conditionally.

Ref for "last value"

useRef stores the previous value without triggering renders. if (ref.current !== val) guards the setState.

Intervals and timers

setInterval + functional setState + empty deps = no loop, cleanup on unmount. The idiomatic pattern.

Don't silence the error by removing the dependency array. Removing deps means the effect runs every render — which is exactly the infinite loop, just without React catching it. Adding // eslint-disable-next-line hides the warning but keeps the loop. The fix is always to address the cause: don't set state in render, stabilize the dependency, or use functional setState with a guard. Every workaround trades a clear error for a silent bug.

⚠️ Common mistakes

The loop patterns that cost real time.

1. setState in render

Function body, inside .map(), inside .forEach(). Fix: move to event handler or effect, or compute during render.

2. Object/array in deps

New identity every render → effect re-runs. Fix: useMemo, or destructure primitives from it.

3. Function in deps

Same — new function every render. Fix: useCallback, or inline inside the effect.

4. Effect sets its own dep

useEffect(() => setX(x + 1), [x]) — infinite loop. Fix: functional setState + guard, or derive instead.

5. setState without a condition

setX(newValue) where newValue !== x — always triggers. Fix: if (x !== newValue) setX(newValue), or use functional setState.

6. Store "last value" in state

const [prev, setPrev] = useState(...) — same problem. Fix: use useRef for previous values.

7. Removing deps as a "fix"

Effect runs every render — same loop, no error message. Fix: address the dependency properly.

8. Calling setState in a store subscription

Custom stores that notify on every change — the component re-renders, the store notifies, loop. Fix: useSyncExternalStore or guarded subscriptions.

❓ Frequently asked

What does "Maximum update depth exceeded" mean in React?

A component is triggering setState during render, in a useEffect with unstable dependencies, or in an event that re-fires itself, causing React to schedule renders faster than it can complete them. React stops after 50 nested updates and throws.

How do I find which useEffect is causing the loop?

React's error message includes a component stack trace in the console — search upward to find the effect. Alternatively, add console.log to each useEffect and see which one fires repeatedly. Or use React DevTools Profiler — the flame graph will show the same component rendering dozens of times.

Why does adding a function to useEffect dependencies cause a loop?

A function defined in a component body is a new object on every render — its reference changes each time the component renders. If it's in the dependency array, the effect thinks the dependency changed, so it re-runs. If the effect calls setState, that triggers another render, which creates a new function, which triggers the effect again — infinite loop.

How do I stop an infinite loop in useEffect?

Identify the cause: (1) setState in render — move it. (2) Function in deps — wrap with useCallback. (3) Object/array in deps — wrap with useMemo or destructure primitives. (4) Effect calls setState with a value that changes — use functional setState or add a guard. (5) setState unconditionally in an effect with the state in its own dependency array — add a condition or move to a ref.

🔗 Related

← All React problems
Problem solved · React