How to Fix "Too Many Re-renders" in React
How do I fix "Too many re-renders" in React?
TL;DR
- Bottom line: "Too many re-renders" means your component calls
setStateduring render, creating an infinite loop. Top 4 causes: (1)onClick={fn()}instead ofonClick={fn}, (2)setStatein component body, (3)useEffectwithout dependency array, (4) object/array asuseEffectdependency. - Key tool: React DevTools Profiler -> "Highlight updates" to see which components re-render.
- Watch out for:
onClick={handleClick()}(calls immediately) vsonClick={handleClick}(passes reference). - Works with: React 16.8+ (hooks), React 18+, React 19+ with Compiler. React Compiler 1.0 auto-memoizes but does NOT prevent these infinite loop patterns.
Constraints
- React hooks require React 16.8+ — class components use
componentDidUpdate/shouldComponentUpdateinstead. [src1] - Never call
setStatesynchronously in the component body (render phase) — always wrap inuseEffect,useLayoutEffect, or an event handler. [src1, src2] - React Compiler 1.0 (stable Oct 2025) handles auto-memoization but does NOT fix
setState-during-render or missinguseEffectdependency arrays. [src7, src8] useEffectcleanup must be returned for subscriptions, intervals, and listeners to prevent memory leaks. [src2]- Never call hooks conditionally or inside loops — this breaks React's internal hook ordering. [src4]
- React 19 removed
ReactDOM.render()— usecreateRoot()fromreact-dom/client. Re-render patterns unchanged. [src1]
Quick Reference
| 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] |
Decision Tree
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
Decision Logic
If error message is "Too many re-renders" and you have onClick/onChange handlers
Search for onClick={fn()} patterns first — accidental invocation is the #1 cause (~40% of cases). Replace with onClick={fn} or onClick={() => fn(args)}. [src1]
If error persists after fixing handlers, and you see setState calls outside hooks/handlers
The setter is being called during render. Move it into useEffect, useLayoutEffect, or an event handler. Never call setters synchronously in the component body. [src1, src2]
If error fires on mount with useEffect and no dependency array
Add a dependency array. Use [] for mount-only side effects, [dep1, dep2] for re-runs on specific changes. Run eslint with react-hooks/exhaustive-deps to catch this automatically. [src2, src4]
If useEffect runs forever and dependency is an object or array literal
Object/array references change every render. Wrap in useMemo, destructure to primitives, or move the literal outside the component. [{a: 1}] !== [{a: 1}] by reference. [src2, src3]
If the loop is setState(prev + 1) inside useEffect(..., [count])
Either remove count from the dependency array and use the functional form setState(c => c + 1), or rethink whether the effect should depend on the same state it updates. [src2]
If hooks are called conditionally (inside if, loops, or after early returns)
Move the condition INSIDE the hook callback. Hooks must be called in the same order on every render to preserve React's internal hook index. [src4]
If you are on React 19 with the Compiler enabled and still hit this error
The Compiler auto-memoizes useMemo/useCallback but cannot fix setState-during-render or missing dependency arrays. All rules above still apply. [src7, src8]
DEFAULT
Add console.log('render', Date.now()) at the top of the suspect component to confirm the loop, then walk the Decision Tree above. Use React DevTools Profiler to see render counts and which props/state changed. [src6]
Step-by-Step Guide
1. Identify the offending 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.
2. Check event handlers for accidental function calls
// ❌ 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.
3. Check for setState in the component body
// ❌ 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.
4. Fix useEffect dependency arrays
// ❌ 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.
5. Verify with React DevTools Profiler
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.
Code Examples
React: Common infinite loop patterns and fixes
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>;
}
React: useCallback to prevent child re-renders
// ❌ 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
}
React 19+: Compiler does NOT fix setState-in-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>;
}
Anti-Patterns
Wrong: Calling setState to "derive" state
// ❌ BAD — unnecessary state + infinite loop risk [src1]
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(items.filter(i => i.active));
}, [items]);
Correct: Compute derived values during render
// ✅ GOOD — no state needed, no effect, no loop risk [src3, src5]
const filteredItems = useMemo(
() => items.filter(i => i.active),
[items]
);
Wrong: Conditional hooks
// ❌ BAD — violates Rules of Hooks [src4]
if (isLoggedIn) {
useEffect(() => { fetchProfile(); }, []);
}
Correct: Condition inside the hook
// ✅ GOOD — hook always called, condition inside [src4]
useEffect(() => {
if (isLoggedIn) fetchProfile();
}, [isLoggedIn]);
Wrong: Using flushSync inside useEffect
// ❌ BAD — forces synchronous re-render inside effect [src2]
useEffect(() => {
flushSync(() => {
setCount(c => c + 1);
});
}, []);
Correct: Let React batch naturally
// ✅ GOOD — React 18+ batches all setState calls automatically [src2]
useEffect(() => {
setCount(c => c + 1); // Batched with other updates
}, []);
Common Pitfalls
- Parentheses in onClick:
onClick={fn()}calls during render — #1 cause for beginners. Fix:onClick={fn}oronClick={() => fn(args)}. [src1] - Deriving state with setState + useEffect: If you can compute from existing state/props,
use
useMemoinstead. [src3, src5] - Object/array dependencies:
[{a: 1}] !== [{a: 1}]— compared by reference. Fix:useMemoor destructure to primitives. [src2] - Functional updates forgotten:
setCount(count + 1)needscountin deps.setCount(c => c + 1)doesn't. [src1, src2] - StrictMode double-invoke: Dev mode calls effects twice, surfacing hidden issues. Fix: ensure effects are idempotent. Don't disable StrictMode. [src2]
- React Compiler false confidence: Developers on React 19+ with Compiler may assume all re-render issues are auto-fixed. Compiler handles memoization but cannot fix setState-in-render or missing dependency arrays. [src7, src8]
- useLayoutEffect vs useEffect confusion:
useLayoutEffectruns synchronously after DOM mutations but before paint. Calling setState inside without care can cause flickering or loops. UseuseEffectunless you need DOM measurement. [src2] - Stale closure in intervals:
setIntervalinsideuseEffectcaptures the initial state value. Fix: use functional updatesetState(prev => prev + 1)or a ref. [src1, src2]
Diagnostic Commands
# 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>
Version History & Compatibility
| 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] |
When to Use / When Not to Use
| 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 |
Important Caveats
- React 19's Compiler (stable Oct 2025) auto-memoizes at build time, eliminating most manual
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] useEffectwith no dependency array runs after EVERY render — almost never what you want. Always specify dependencies. [src2]- React's "Too many re-renders" error is a safety bailout (~50 renders). The real issue is the infinite loop pattern, not the error count. [src1]
- React 18+ automatic batching means multiple
setStatecalls in the same synchronous block produce only one re-render. This can mask bugs that appear in React 17 but not 18+. [src1]