setState
during render, creating an infinite loop. Top 4 causes: (1) onClick={fn()} instead of
onClick={fn}, (2) setState in component body, (3) useEffect
without dependency array, (4) object/array as useEffect dependency.onClick={handleClick()} (calls immediately) vs
onClick={handleClick} (passes reference).componentDidUpdate / shouldComponentUpdate instead. [src1]setState synchronously in the component body (render phase) — always wrap in useEffect, useLayoutEffect, or an event handler. [src1, src2]setState-during-render or missing useEffect dependency arrays. [src7, src8]useEffect cleanup must be returned for subscriptions, intervals, and listeners to prevent memory leaks. [src2]ReactDOM.render() — use createRoot() from react-dom/client. Re-render patterns unchanged. [src1]| Pattern | Causes Infinite Loop? | Fix |
|---|---|---|
onClick={handleClick()} |
YES — calls every render | onClick={handleClick} [src1] |
onClick={() => handleClick(id)} |
No — arrow wraps call | Already correct [src1] |
setState(value) in body |
YES — sets during render | Move to useEffect or handler [src1] |
useEffect(() => { setState() }) (no deps) |
YES — runs every render | Add [] dependency array [src2] |
useEffect(..., [obj]) where obj = {} each render |
YES — new ref each render | useMemo the object [src2, src3] |
setState(prev => prev + 1) in useEffect(..., [count]) |
YES — updates dep, re-runs | Remove dep or functional update [src2] |
useMemo(() => compute(a,b), [a,b]) |
No — memoized correctly | Already correct [src3] |
Conditional useState / useEffect |
YES — breaks Rules of Hooks | Never call hooks conditionally [src4] |
useRef + mutation in render |
No (usually) — refs don't trigger re-render | Correct for mutable values that don't affect UI [src1] |
flushSync(() => setState()) inside useEffect |
YES — forces synchronous re-render | Remove flushSync or move outside effect [src2] |
START
├── Error: "Too many re-renders"
│ ├── Check onClick/onChange handlers
│ │ ├── Found: onClick={fn()} → FIX: onClick={fn} or onClick={() => fn()) [src1]
│ │ └── Handlers look correct → Check component body ↓
│ ├── Check for setState in component body (outside useEffect/handlers)
│ │ ├── Found: setState(x) in body → FIX: Move to useEffect or handler [src1]
│ │ └── No setState in body → Check useEffect ↓
│ ├── Check useEffect dependency arrays
│ │ ├── Missing deps array → FIX: Add [] or [specific deps] [src2]
│ │ ├── Object/array in deps → FIX: useMemo the dep or use primitives [src3]
│ │ └── State var in deps that effect updates → FIX: Remove from deps [src2]
│ ├── Check for conditional hooks
│ │ ├── Found → FIX: Move condition inside hook [src4]
│ │ └── None → Check for flushSync in effects ↓
│ ├── Check for flushSync inside useEffect
│ │ ├── Found → FIX: Remove flushSync or restructure [src2]
│ │ └── None → Use React DevTools Profiler [src6]
│ └── Using React Compiler?
│ ├── YES → Compiler handles memoization but NOT setState-in-render loops [src7]
│ └── NO → Consider enabling for automatic memoization [src8]
├── Error: "Maximum update depth exceeded"
│ └── Same causes — React's bailout kicked in
└── DEFAULT → console.log('render', Date.now()) to find looping component
function MyComponent() {
console.log('MyComponent rendered at', Date.now());
// ... rest of component
}
Or use React DevTools -> Settings -> "Highlight updates when components render". [src6]
Verify: Open DevTools console -> if one component floods the log, that's your culprit.
// ❌ WRONG — calls handleClick on EVERY render (infinite loop) [src1]
<button onClick={handleClick()}>Click</button>
// ✅ CORRECT — passes function reference
<button onClick={handleClick}>Click</button>
// ✅ CORRECT — wraps with arrow for arguments
<button onClick={() => handleClick(id)}>Click</button>
Verify: Search codebase for onClick={ followed by ()} — any match with parentheses is suspect.
// ❌ WRONG — setState during render = infinite loop [src1]
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // BOOM — re-render → setState → re-render → ...
return <p>{count}</p>;
}
// ✅ CORRECT — setState in event handler
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Verify: Search for any set function calls outside of useEffect, useLayoutEffect, or event handlers.
// ❌ WRONG — no dependency array = runs after EVERY render [src2]
useEffect(() => {
setData(fetchedData); // triggers re-render → effect runs again → ...
});
// ✅ CORRECT — empty array = runs once on mount
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
// ❌ WRONG — object reference changes every render [src2, src3]
const options = { page: 1, limit: 10 }; // NEW object each render
useEffect(() => {
fetchData(options);
}, [options]); // options !== prevOptions → re-run → ...
// ✅ CORRECT — memoize the object
const options = useMemo(() => ({ page: 1, limit: 10 }), []);
useEffect(() => {
fetchData(options);
}, [options]);
Verify: Run eslint rule react-hooks/exhaustive-deps to catch missing or incorrect dependencies.
Open React DevTools -> Profiler tab -> Record -> interact with the component -> Stop -> analyze flamegraph. [src6]
Verify: Components should render 1-2 times on mount (2 in StrictMode), not continuously.
Full script: react-common-infinite-loop-patterns-and-fixes.jsx (55 lines)
// Input: Component with "Too many re-renders" error
// Output: Fixed component that renders correctly
import { useState, useEffect, useMemo, useCallback } from 'react';
// ❌ PATTERN 1: setState during render
function BadComponent1() {
const [items, setItems] = useState([]);
const sorted = [...items].sort();
setItems(sorted); // INFINITE LOOP!
return <ul>{sorted.map(i => <li key={i}>{i}</li>)}</ul>;
}
// ✅ FIX: Derive state instead of setting it
function GoodComponent1() {
const [items, setItems] = useState([]);
const sorted = useMemo(() => [...items].sort(), [items]);
return <ul>{sorted.map(i => <li key={i}>{i}</li>)}</ul>;
}
// ❌ BAD — inline function creates new ref each render [src3]
function Parent() {
const [count, setCount] = useState(0);
return <Child onClick={() => console.log('click')} />;
// Child re-renders every time Parent renders
}
// ✅ GOOD — useCallback stabilizes the function reference [src3]
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => console.log('click'), []);
return <Child onClick={handleClick} />;
// Child skips re-render if memoized with React.memo
}
// ✅ BEST (React 19+) — Compiler handles this automatically [src7]
// No manual useCallback needed — compiler inserts memoization
function Parent() {
const [count, setCount] = useState(0);
return <Child onClick={() => console.log('click')} />;
// Compiler auto-memoizes — Child skips re-render
}
// ❌ STILL BROKEN WITH REACT COMPILER [src7, src8]
function BrokenEvenWithCompiler() {
const [count, setCount] = useState(0);
setCount(count + 1); // Compiler cannot fix this — infinite loop
return <p>{count}</p>;
}
// ✅ FIX — same as always: move to effect or handler [src1]
function Fixed() {
const [count, setCount] = useState(0);
useEffect(() => { setCount(c => c + 1); }, []);
return <p>{count}</p>;
}
// ❌ BAD — unnecessary state + infinite loop risk [src1]
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(items.filter(i => i.active));
}, [items]);
// ✅ GOOD — no state needed, no effect, no loop risk [src3, src5]
const filteredItems = useMemo(
() => items.filter(i => i.active),
[items]
);
// ❌ BAD — violates Rules of Hooks [src4]
if (isLoggedIn) {
useEffect(() => { fetchProfile(); }, []);
}
// ✅ GOOD — hook always called, condition inside [src4]
useEffect(() => {
if (isLoggedIn) fetchProfile();
}, [isLoggedIn]);
// ❌ BAD — forces synchronous re-render inside effect [src2]
useEffect(() => {
flushSync(() => {
setCount(c => c + 1);
});
}, []);
// ✅ GOOD — React 18+ batches all setState calls automatically [src2]
useEffect(() => {
setCount(c => c + 1); // Batched with other updates
}, []);
onClick={fn()} calls during render — #1 cause for
beginners. Fix: onClick={fn} or onClick={() => fn(args)}. [src1]useMemo instead. [src3, src5][{a: 1}] !== [{a: 1}] — compared by reference.
Fix: useMemo or destructure to primitives. [src2]setCount(count + 1) needs count
in deps. setCount(c => c + 1) doesn't. [src1, src2]useLayoutEffect runs synchronously after DOM mutations but before paint. Calling setState inside without care can cause flickering or loops. Use useEffect unless you need DOM measurement. [src2]setInterval inside useEffect captures the initial state value. Fix: use functional update setState(prev => prev + 1) or a ref. [src1, src2]# Install React DevTools browser extension
# Chrome: chrome://extensions → search "React Developer Tools"
# Enable "Highlight updates when components render"
# React DevTools → Settings (gear) → Components → check "Highlight updates"
# Profile re-renders
# React DevTools → Profiler tab → Record → interact → Stop → analyze flamegraph
# Check if React Compiler is active
# React DevTools → Components → look for compiler-inserted cache indicators
# Lint for dependency issues
npx eslint --rule 'react-hooks/exhaustive-deps: warn' src/
// Add React.Profiler to measure specific components
<React.Profiler id="MyComponent" onRender={(id, phase, duration) => {
console.log(id, phase, duration);
}}>
<MyComponent />
</React.Profiler>
| React Version | Re-render Behavior | Notes |
|---|---|---|
| React 16.8+ | Hooks introduced (useState, useEffect) | First version where this guide applies [src1] |
| React 17 | Automatic batching for event handlers only | Multiple setState in handlers = 1 re-render |
| React 18 | Automatic batching everywhere (fewer re-renders) | createRoot required; setState in promises/timeouts now batched |
| React 19 | React Compiler 1.0 (auto-memoization) | Reduces manual useMemo/useCallback; core infinite loop patterns unchanged [src7] |
| This Guide Fixes | Look Elsewhere For | Alternative |
|---|---|---|
| "Too many re-renders" error | Slow rendering (no error) | React profiling guide [src5] |
| "Maximum update depth exceeded" | White screen (component crash) | React error boundary guide |
| Component renders endlessly | Component renders but shows wrong data | React state debugging guide |
| useEffect infinite loop | SSR hydration mismatches | React hydration error guide |
useMemo/useCallback. But core patterns (don't setState during render, fix onClick calls, fix useEffect deps) still apply because the Compiler optimizes memoization, not control flow. [src7, src8]useEffect with no dependency array runs after EVERY render — almost never what you want.
Always specify dependencies. [src2]setState calls in the same synchronous block produce only one re-render. This can mask bugs that appear in React 17 but not 18+. [src1]