Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. Too many re-renders in React: the one real cause
Tools

Too many re-renders in React: the one real cause

Too many re-renders always means a state update running during render. Here is how to spot which of the four shapes you have written.

By Max Arthur
Co-Founder & Content Marketer·October 15, 2026·9 Min read
White text reading 'Too many re-renders in React' on a dark purple dotted background
Too many re-renders. React limits the number of renders to prevent an infinite loop.

There is exactly one cause. A state update is running during render, so React re-renders, which runs the update again, which re-renders. React counts the passes, notices they are not settling, and stops rather than freezing the tab.

function Boom() {
  const [n, setN] = useState(0);
  setN(n + 1);           // runs on every render
  return <div>{n}</div>;
}

Your job is to find where that update is running. It takes one of four shapes. All output here came from React 19.2.4.

Shape 1: setState in the component body

The example above, and the easiest to spot once you know to look. The component body is the render, so any unconditional state update sitting there loops.

The fix depends on why the update is there:

  • Reacting to a user action? Move it into an event handler.
  • Reacting to a prop or a fetch? Move it into useEffect.
  • Deriving a value from other state? Do not store it in state at all, just calculate it during render.

That last one is the most commonly missed. If a value can be computed from other values, it is not state:

// not state
const fullName = `${first} ${last}`;

Storing derived values in state creates exactly the loop this error catches, and the calculation is cheaper than the extra render anyway.

Shape 2: calling the handler instead of passing it

The version that catches everyone once:

<button onClick={setN(n + 1)}>click</button>

That does not pass a function to onClick. It calls setN(n + 1) immediately, during render, and passes the return value. Same loop, and the same error, confirmed in the same run as shape 1.

The difference is a pair of parentheses:

<button onClick={() => setN(n + 1)}>click</button>

onClick={handleClick} passes the function. onClick={handleClick()} calls it now. Whenever you need to pass arguments, wrap it in an arrow function so the call happens on click rather than on render.

Shape 3: an effect with no dependency array

useEffect(() => {
  setCount(count + 1);
});          // no second argument

An effect with no dependency array runs after every render. Setting state in it causes a render, which runs the effect, which sets state. React's limit catches this too, and it is worth knowing that the three forms of the second argument mean three different things:

Written as Runs
useEffect(fn) After every render
useEffect(fn, []) Once, after the first render
useEffect(fn, [a, b]) When a or b changes

Omitting the array is rarely what anyone means.

Shape 4: a dependency that is new every render

The subtlest, and the one that survives code review:

const options = { pageSize: 20 };

useEffect(() => {
  load(options);
}, [options]);          // new object every render

options is created fresh on every render. React compares dependencies with Object.is, which compares objects by reference, not by contents. Two objects with identical contents are different values, so the dependency always looks changed, so the effect always runs.

The same applies to arrays and to functions defined in the component body.

Three fixes, in order of preference:

Move it inside the effect, if nothing else uses it:

useEffect(() => {
  load({ pageSize: 20 });
}, []);

Depend on the primitive fields rather than the object:

useEffect(() => {
  load({ pageSize });
}, [pageSize]);

Numbers and strings compare by value, so this behaves the way you expected the object to.

Memoise it when the object genuinely has to be shared:

const options = useMemo(() => ({ pageSize }), [pageSize]);

Reach for useMemo last. The first two fixes remove the problem; this one manages it.

Updating state during render is not always wrong

Worth knowing, because "never set state during render" is the usual advice and it is slightly too strong. A guarded update runs once and settles:

function Ok() {
  const [n, setN] = useState(0);
  if (n === 0) setN(1);
  return <div>{n}</div>;
}

That renders 1 with no error. React re-runs the component immediately, the condition is now false, the update does not repeat, and the loop terminates.

React supports this deliberately, for adjusting state when a prop changes. The rule is not "never update during render", it is "an update during render must be conditional on something that stops being true". The looping versions above all fail that test: none of them has a condition, so none of them ever settles.

Finding yours quickly

The error message does not name a line, which is why it feels harder than it is. This order finds it fast:

  1. Read the component in the stack trace and look for a setX( call that is not inside a function.
  2. Scan the JSX for ={handler()}. Any handler prop with parentheses is calling on render.
  3. Check every useEffect for its second argument. A missing array is shape 3.
  4. Look at what is in the dependency arrays. Objects, arrays and inline functions are shape 4.

Add a console.log at the top of the component if none of that finds it. A component printing hundreds of lines confirms the loop and shows which values are changing between passes.

The state that should not have been state

Shapes 1 and 4 share a root cause worth naming on its own, because fixing it removes whole categories of this error rather than one instance.

A large share of loops come from storing something in state that could have been calculated. The pattern looks responsible and is the problem:

const [items, setItems] = useState([]);
const [filtered, setFiltered] = useState([]);
const [count, setCount] = useState(0);

useEffect(() => {
  setFiltered(items.filter((i) => i.active));
}, [items]);

useEffect(() => {
  setCount(filtered.length);
}, [filtered]);

Three pieces of state, two effects, and a chain where each update triggers the next. Add one dependency mistake anywhere in it and you have this error. Even working correctly, it renders three times to settle on a value that was knowable in one.

filtered and count are not state. They are facts about items:

const [items, setItems] = useState([]);
const filtered = items.filter((i) => i.active);
const count = filtered.length;

No effects, no synchronisation, no loop possible. There is only one source of truth, so nothing can disagree with anything else.

The test is a question you can ask about any useState in your code: can I work this value out from the others? If yes, it should not be state. The recalculation runs on every render and is almost always cheaper than the extra render the effect would have caused, and if profiling ever proves otherwise, useMemo is available for that specific case.

The related habit is to be suspicious of any effect whose only job is to call a setter. That is usually a derived value wearing a costume, and it is the shape that turns into this error the moment a dependency is an object.

How many renders you actually get

The limit is a fixed count rather than a timer. Counting the passes with a component that updates state unconditionally, the component function ran 26 times before React threw: one initial render plus twenty-five re-renders.

That number is worth knowing for two reasons.

It explains why the error is instant rather than gradual. There is no slow degradation to notice, no rising memory, no chance to catch it in a profiler. The component runs twenty-six times in a few milliseconds and then throws.

It also explains a debugging trap. A console.log in a looping component prints twenty-six lines, not thousands, so the console does not look catastrophic. It looks like a component that rendered more than you expected, which is easy to dismiss. If you see a component log in the twenties and then an error, that is this bug, and the count is the confirmation rather than a coincidence.

A component that renders "a lot" is a different problem

Worth separating, because the two get conflated. This error is about a loop that never settles. A component that renders three or four times more than you expected is not looping, and it will never produce this message.

That second thing usually comes from state living higher in the tree than it needs to, and the fix is to move state down rather than to add memoisation. React re-rendering a subtree is cheap; the cost only matters when the render itself is expensive.

So if you arrived here because a component renders more often than you like but nothing throws, this is not your error. Nothing is broken, and reaching for useMemo and React.memo to fix it usually adds complexity without changing anything a user would notice.

A worked example

Here is a component with the error, of the kind that actually gets written:

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

  const options = { query, limit: 20 };

  useEffect(() => {
    search(options).then(setResults);
  }, [options]);

  setIsEmpty(results.length === 0);

  return <List items={results} />;
}

Two separate faults, and it is worth naming both rather than fixing the first one and moving on.

setIsEmpty(...) sits in the component body with no condition, which is shape 1. And options is rebuilt every render, so the effect's dependency always looks new, which is shape 4. Either alone produces the error.

The fix removes rather than adds:

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

  useEffect(() => {
    search({ query, limit: 20 }).then(setResults);
  }, [query]);

  const isEmpty = results.length === 0;

  return <List items={results} />;
}

isEmpty is derived, so it stops being state. options moves inside the effect, so the dependency is query, a string, which compares by value. One piece of state, one effect, no loop available.

Notice that the corrected version is shorter. That is the usual outcome, and it is a decent signal that you found the real fix rather than suppressing the symptom: solutions that add a useMemo, a ref, or an extra condition to hold the loop back are usually working around a value that should not have been state.

Why React stops instead of hanging

The limit is a safety net, not a performance ceiling. Without it, a render loop would run until the tab became unresponsive and the browser offered to kill the page, with no indication of which component was at fault.

Stopping at a fixed count converts that into an error with a stack trace, on the render pass that caused it. So the error is React doing you a favour, and it always means the same thing: something in that component updates state every time it renders, with nothing to stop it.

Want to write hooks somewhere the loop is caught in a second rather than a frozen tab? Start with the React track, or read objects are not valid as a React child for the other error this code shape tends to produce.

Keep reading

More from the blog

White text reading 'Why React lists need unique keys' on a dark purple dotted background
October 16, 2026·7 min readTools

Each child in a list should have a unique key prop

The unique key prop warning in React still renders your list. Here is the bug it predicts, shown with real output from two runs.

Read more
White text reading 'NoneType is not subscriptable in Python' on a dark purple dotted background
October 11, 2026·7 min readTools

'NoneType' object is not subscriptable: find the None

NoneType object is not subscriptable means something returned None and you indexed it. The usual culprit is a method that sorts in place.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons