Diego Betto's Blog
Photo by Andrew Wulf on Unsplash

Diego Betto · September 14, 2026 · 5 min di lettura

React 19: "Too many re-renders", the infinite loop error explained

Why React throws 'Too many re-renders. React limits the number of renders...' and the two most common causes: setState in the render body and unstable effect dependencies.

Condividi:XLinkedInFacebookWhatsApp

You change a couple of lines, save, and the browser freezes for a second before dumping this in the console:

⛔ Error

Too many re-renders. React limits the number of renders to prevent an infinite loop.

The message tells you React caught itself in a loop before your tab actually crashed — that’s the good news. The bad news is it doesn’t tell you which line caused it. Almost always, the culprit is one of two patterns, both easy to write by accident.

Cause #1: calling setState directly in the render body

This is the classic case, and it looks deceptively reasonable at first glance:

function Counter() {
  const [count, setCount] = useState(0);

  // Runs on every render — including the one it just triggered
  setCount(count + 1);

  return <p>{count}</p>;
}

Every call to setCount schedules a re-render. That re-render runs the component body again, which calls setCount again, which schedules another re-render — forever. React’s render function must be pure: it can read state, but it can’t write it unconditionally as part of rendering.

The fix is almost always to move the state update into an event handler or an effect that runs only when something specific changes:

function Counter() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

⚠ A sneakier variant

This also happens indirectly, when you call setState conditionally during render to “sync” props into state: if (value !== prevValue) setState(value). It looks harmless because of the if, but if the condition is ever true on every render — for example because value is a new object literal each time — you’re back to an infinite loop. See cause #2 below for exactly why that happens.

Cause #2: an object or array recreated on every render as a dependency

This one is sneakier because the setState call is safely tucked inside a useEffect, which feels correct:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  // New object on every render!
  const options = { limit: 10, query };

  useEffect(() => {
    fetchResults(options).then(setResults);
  }, [options]); // options is never === to the previous options

  return (
    <ul>
      {results.map((r) => (
        <li key={r.id}>{r.text}</li>
      ))}
    </ul>
  );
}

options is a brand-new object literal on every render, so it never equals the previous one by reference. React re-runs the effect every time, the effect calls setResults, which triggers a re-render, which creates a new options object — an infinite loop, just one hop removed from cause #1.

Two ways to fix it:

// Option A: depend on the primitive values, not the object
useEffect(() => {
  fetchResults({ limit: 10, query }).then(setResults);
}, [query]);

// Option B: memoize the object itself
const options = useMemo(() => ({ limit: 10, query }), [query]);
useEffect(() => {
  fetchResults(options).then(setResults);
}, [options]);

Option A is usually simpler and is what I’d reach for first — depend on the raw values that actually change, and construct the object inside the effect where its identity doesn’t matter.

💡 Consiglio

If you’re on the React Compiler, a lot of this class of bug gets easier to avoid because the Compiler memoizes objects and functions for you automatically — but it doesn’t rewrite a setState call sitting directly in your render body, so cause #1 is still entirely on you. I go into where the Compiler’s memoization actually helps in my React Compiler article.

How to actually find the offending line

The error message alone won’t point at your code, but two tools will:

  • React’s own overlay in development usually includes a component stack right below the error — read it top to bottom, the first component you wrote (not a library wrapper) is your best starting point.
  • Comment out state updates one at a time. Sounds crude, but for a loop with multiple useEffect/useState pairs in the same component, temporarily disabling each setX call until the loop stops is often faster than reading generated stack traces.

FAQ

❓ Is 'Maximum update depth exceeded' the same error?

Yes — that’s the underlying Error message React throws; “Too many re-renders” is the friendlier text shown by React’s error overlay in development. Both point at the same problem.

❓ Can useMemo or React.memo cause this?

Not by themselves — they only affect whether a component skips a render, not whether it schedules one. The loop always originates from a setState call that fires on (effectively) every render; useMemo/React.memo can hide or reveal the pattern, but they’re not the root cause.

❓ Does this only happen with hooks?

It’s most common with useState/useEffect, but the same shape of bug exists in class components too — calling this.setState inside render(), or inside componentDidUpdate without a proper condition, produces the identical infinite loop.

Conclusion

“Too many re-renders” always traces back to a state update that fires unconditionally, whether that’s a setState call sitting directly in the render body, or one hidden inside an effect whose dependency is recreated on every render. Once you know to look for those two shapes specifically, the fix is usually a one-line change — the hard part is just spotting which of your useState/useEffect pairs is the one doing it.

References

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.