How to Fix "Too Many Re-renders" in React

Type: Software Reference Confidence: 0.93 Sources: 8 Verified: 2026-02-23 Freshness: quarterly

TL;DR

Constraints

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

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

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

Related Units