Too many re-renders: infinite loop caused by useEffect dependencies

Filed under Next.js & React errors

The fix, fast

An effect that updates a piece of state listed in its own dependency array will run, update the state, and immediately run again — forever. Break the loop by removing that state from the dependencies and using the functional update form, like setCount(c => c + 1), or by computing the value during rendering instead of in an effect at all. If a value is calculated from other values, you usually do not need an effect for it.

Why this happens

In React, rendering means drawing your component — the function that returns what appears on screen — based on its current state, the values it remembers between draws. A useEffect is a block of code React runs after a draw, whenever the values in its dependency array (the list in square brackets at the end) have changed since the last draw.

Now connect those two facts and the loop builds itself:

  1. The effect runs and calls a state setter, like setItems(...).
  2. Changing state tells React to draw the component again.
  3. The effect’s dependencies include that same state, so React sees a change and runs the effect again. Return to step 1.

A subtler version involves objects and lists. A line like const options = { sort: "new" } creates a brand-new object on every draw, even if its contents are identical — to React, a new object is a changed value. Put that object in a dependency array and the effect fires after every draw, and if that effect also sets state, you have the same infinite loop with no obvious culprit. React eventually gives up and warns “Too many re-renders” rather than freezing the tab forever.

The fix, step by step

  1. Read the warning and find the effect above it. The “Too many re-renders” warning points at a component, not a line. In that component, look for a useEffect that calls a set-something function. That pairing — an effect and a state setter inside it — is your suspect list.
  2. Check whether the effect sets state that is in its own dependencies. This is the classic broken shape:
    // BROKEN: effect depends on count, and changes count
    useEffect(() => {
      setCount(count + 1);
    }, [count]);
    If the setter and the dependency match, you have found the loop.
  3. Use a functional update and drop the state from the dependencies. The functional form — passing a function to the setter — tells React “take the previous value and change it,” so the effect no longer needs to read the current value and no longer depends on it:
    // FIXED: no dependency on count, previous value supplied by React
    useEffect(() => {
      const id = setInterval(() => setCount(c => c + 1), 1000);
      return () => clearInterval(id);
    }, []);
    Only use an empty dependency array when the effect genuinely should run once, on first appearance — and keep the cleanup line that stops timers or listeners.
  4. Move derived values out of effects entirely. A derived value is one you can calculate from values you already have, like a total from a list of prices. The React documentation’s rule of thumb is “you might not need an effect”: calculate it during rendering instead of storing it in state and syncing it with an effect.
    // BROKEN: state + effect just to keep a total in sync
    // const [total, setTotal] = useState(0);
    // useEffect(() => { setTotal(items.reduce(...)) }, [items]);
    
    // FIXED: compute it while rendering — always current, no loop
    const total = items.reduce((sum, item) => sum + item.price, 0);
  5. Stabilise objects and lists with useMemo when a dependency truly needs one. useMemo tells React “keep this exact object until its inputs change,” which stops a freshly recreated object from retriggering effects:
    const options = useMemo(() => ({ sort: "new", limit: 20 }), []);
    Use this to fix identity — the object being “new” every draw — not to paper over an effect that should not exist.
  6. For fetch-once effects, keep empty dependencies and guard the result. Loading data when a component first appears legitimately uses an empty array. Make sure nothing inside sets a dependency, ignore stale responses if the component disappears early, and — in development — remember React’s StrictMode deliberately runs effects twice to expose exactly these bugs, so a double request in development alone is not an infinite loop.

Give your AI this prompt

My React component is hitting "Too many re-renders" and looping. Here is the component:
[paste the component code here]

And the exact warning from the console:
[paste the warning here]

Diagnose it in this order:
1. List every useEffect in the component, what state each one sets, and what is in its dependency array. Flag any effect that sets a state value that also appears in its own dependencies.
2. Check for objects or arrays created during rendering that are used as dependencies — explain if a new identity every render is retriggering an effect.
3. For any value that is calculated from other state or props, recommend removing the effect and computing it during rendering instead.
4. Where an update only needs the previous value, rewrite it in functional form, e.g. setCount(c => c + 1).

Constraints: make ONE fix at a time and explain in plain English why it breaks the loop before showing code. Do not rewrite unrelated files or components, do not remove effects that sync with something outside React (timers, subscriptions, fetching) without explaining the replacement, and do not just delete the dependency array to silence the warning.

One fix, every Friday.

One real error, explained in plain English, with the fix — in your inbox every Friday. No spam, no jargon, unsubscribe anytime.