๐ŸŒ Detecting your locationโ€ฆ

How to Fix ‘Too Many Re-renders’ Error in React: Complete Solution

โฑ๏ธ6 min read  ยท  1,138 words

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. React is telling you that rendering caused a state update, which caused a render, which caused an update. The cause is almost always one of four patterns, and each has a clear fix.

What Is Actually Happening

Rendering must be a pure calculation of UI from props and state. If it also sets state, you create a cycle. React detects the runaway and stops it rather than letting the tab freeze.

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

  setCount(count + 1);   // runs during render -> triggers render -> runs again

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

The error message is unusually accurate here: something in your render path is updating state every time it runs.

Cause 1: Calling the Handler Instead of Passing It

By far the most common version, and the one that catches everyone at least once.

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

  // โŒ setCount(count + 1) is CALLED during render.
  // Its return value (undefined) becomes the onClick handler.
  return <button onClick={setCount(count + 1)}>Increment</button>;
}

onClick needs a function to call later. Writing setCount(...) executes it immediately. Wrap it in an arrow function so a function is passed rather than a result.

// โœ… A function is passed; it runs on click.
<button onClick={() => setCount(count + 1)}>Increment</button>

// โœ… Or reference an existing function without calling it.
<button onClick={handleClick}>Increment</button>      // no parentheses

// โŒ This calls handleClick during render.
<button onClick={handleClick()}>Increment</button>

The rule: parentheses after a handler name in JSX means it runs during render. If you need to pass arguments, wrap it.

<button onClick={() => handleDelete(item.id)}>Delete</button>

Cause 2: useEffect Without a Dependency Array

An effect with no second argument runs after every render. If it sets state, that state change triggers another render, which runs the effect again.

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  // โŒ No dependency array โ€” runs after every render, forever.
  useEffect(() => {
    fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
  });
}
// โœ… Re-runs only when userId changes.
useEffect(() => {
  fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
}, [userId]);

Omitting the array is different from passing an empty one. No array means “every render”; [] means “once on mount”.

Cause 3: A Dependency That Changes Every Render

Objects, arrays, and functions are compared by reference. A new one each render means the dependency always looks changed.

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

  // New object identity on every render.
  const options = { query, limit: 20 };

  useEffect(() => {
    search(options).then(setResults);
  }, [options]);   // โŒ always "changed" -> loop
}

Depend on primitives, which compare by value.

useEffect(() => {
  search({ query, limit: 20 }).then(setResults);
}, [query]);   // โœ… string compares by value

When the object must exist outside the effect, memoise it.

const options = useMemo(() => ({ query, limit: 20 }), [query]);

useEffect(() => {
  search(options).then(setResults);
}, [options]);   // โœ… identity is stable until query changes

Cause 4: Setting State to Derive a Value

Using state plus an effect to compute something from existing values creates an unnecessary cycle and an extra render.

function Cart({ items }) {
  const [total, setTotal] = useState(0);

  // โŒ Unnecessary, and loops if the dependency is wrong.
  useEffect(() => {
    setTotal(items.reduce((sum, i) => sum + i.price, 0));
  });

  return <p>Total: {total}</p>;
}

Anything computable from props or state should be computed during render.

function Cart({ items }) {
  // โœ… No state, no effect, no possible loop.
  const total = items.reduce((sum, i) => sum + i.price, 0);
  return <p>Total: {total}</p>;
}

If the calculation is genuinely expensive, wrap it in useMemo โ€” but still do not put it in state.

const total = useMemo(
  () => items.reduce((sum, i) => sum + i.price, 0),
  [items]
);

The Conditional-Render Variant

Setting state inside a conditional during render is the same bug with a disguise.

function Form({ initialValue }) {
  const [value, setValue] = useState('');

  // โŒ Still a state update during render.
  if (initialValue && !value) {
    setValue(initialValue);
  }
}

Initialise state properly instead. If the initial value is expensive to compute, pass a function so it only runs once.

const [value, setValue] = useState(initialValue ?? '');

// Lazy initialiser โ€” the function runs on the first render only.
const [rows, setRows] = useState(() => parseLargeCsv(raw));

If the value must reset when a prop changes, the idiomatic solution is a key on the component, which remounts it with fresh state.

<Form key={userId} initialValue={user.name} />

Finding the Culprit

When the cause is not obvious, log where renders originate.

function MyComponent(props) {
  console.count('MyComponent render');
  console.log('props:', props);
  // ...
}

React DevTools Profiler with “Record why each component rendered” enabled tells you which prop or state changed on each pass. For effect loops, log inside the effect and inside the setter to see the cycle directly.

useEffect(() => {
  console.log('effect ran, deps:', { userId, options });
}, [userId, options]);

Quick Reference

Symptom Likely cause Fix
Errors instantly on mount Handler called during render Wrap in an arrow function
Loops after data loads Effect missing a dependency array Add [] or the correct deps
Loops despite correct deps Object or function dependency Use primitives, useMemo, or useCallback
Extra render on every change Derived value stored in state Compute during render
Loops on a conditional setState inside render branch Initialise state or use key

Frequently Asked Questions

Q: Why does this only happen sometimes?
A: Loops that depend on data need the data to arrive first. The bug is present from the start but only triggers once a fetch resolves and sets state.

Q: Is it ever acceptable to set state during render?
A: React permits one narrow case โ€” adjusting state during render when a prop changes, updating the same component only, and guarded by a comparison. It is rarely the best answer; prefer computing during render or using key.

Q: Does this happen in class components?
A: Yes. Calling setState in render or unconditionally in componentDidUpdate produces the identical loop.

Q: My effect has an empty array but still loops. Why?
A: Then the loop is elsewhere โ€” most often a handler invoked during render, or a parent re-mounting the child because its key changes every render.

Q: Will the React Compiler fix this for me?
A: It removes the memoisation category of the problem by handling identity automatically. It does not fix calling a handler during render, which is a straightforward mistake in the JSX.

Conclusion

“Too many re-renders” always means state is being updated as part of rendering. Check the four causes in order: a handler called instead of passed in JSX, a useEffect with no dependency array, an object or function dependency that changes identity every render, and derived values stored in state instead of computed. The underlying rule is simple โ€” rendering must be a pure calculation, and every state update must originate from an event handler or an effect, never from the render itself.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work โ€” which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

โœ๏ธ Leave a Comment

Your email address will not be published. Required fields are marked *