Too Many Re-renders: Finding the Infinite Loop in React
React caught the loop and stopped it. Here are the five ways to create one and how the Profiler tells you which you have.
The short answer
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Something sets state during render, or an effect sets state that causes the effect to run again.
The five causes:
- Calling a function instead of passing it:
onClick={handleClick()} setStatedirectly in the component body- An effect with a missing or unstable dependency
- A new object or array in a dependency array
- A parent re-rendering that recreates a prop the child depends on
Find it fast by opening React DevTools Profiler, enabling "Record why each component rendered", and looking at the render reason.
Tested on React 19.1.
Cause 1: calling instead of passing
// wrong: calls handleClick during render, every render
<button onClick={handleClick()}>Save</button>
// right
<button onClick={handleClick}>Save</button>
If handleClick calls setState, you have an infinite loop immediately. This is the most common cause and the easiest to miss when you need to pass an argument:
// wrong
<button onClick={deleteItem(id)}>Delete</button>
// right
<button onClick={() => deleteItem(id)}>Delete</button>
Cause 2: setState in the component body
function Profile({ user }) {
const [name, setName] = useState("");
setName(user.name); // runs on every render, triggers a render
return <h1>{name}</h1>;
}
Render, set state, re-render, set state, forever.
This is almost always an attempt to derive state from props, and the fix is to not store it as state at all:
function Profile({ user }) {
const name = user.name; // just use it
return <h1>{name}</h1>;
}
If it needs a transformation, compute it during render:
const displayName = user.name.trim() || "Anonymous";
There is a legitimate pattern for adjusting state when a prop changes, and it requires a guard:
const [prevUserId, setPrevUserId] = useState(user.id);
if (user.id !== prevUserId) {
setPrevUserId(user.id);
setSelection(null); // reset when the user changes
}
The condition is what stops the loop. React explicitly supports setting state during render when it is guarded like this, and it is better than an effect because it avoids a wasted render pass.
Cause 3: the effect that triggers itself
useEffect(() => {
setCount(count + 1);
});
No dependency array means the effect runs after every render, and it sets state, which causes a render.
useEffect(() => {
fetchData().then(setData);
}, [data]); // depends on what it sets
Fetch, set data, data changed, effect runs, fetch again. This one loops more slowly and can look like a performance problem rather than an error, and it will hammer your API.
The fix is usually a correct dependency array, and often the realisation that the effect should not depend on its own output.
Cause 4: a new reference every render
The subtle one, and the one that survives review.
function Search({ filters }) {
const options = { limit: 10, ...filters }; // new object every render
useEffect(() => {
fetchResults(options).then(setResults);
}, [options]); // never equal to last time
}
React compares dependencies with Object.is. A newly created object is never equal to the previous one, even with identical contents. So the effect runs every render, sets state, renders again.
Same trap with arrays, and with functions:
const handleChange = () => { ... }; // new function every render
useEffect(() => { subscribe(handleChange); }, [handleChange]);
Three fixes, in order of preference.
Depend on primitives:
useEffect(() => {
fetchResults({ limit: 10, ...filters }).then(setResults);
}, [filters.category, filters.minPrice]);
Move it outside the component if it does not depend on props or state:
const DEFAULT_OPTIONS = { limit: 10 };
Memoise as a last resort:
const options = useMemo(() => ({ limit: 10, ...filters }), [filters.category]);
useMemo is the answer people reach for first and it should be last, because it adds a dependency array of its own that can have the same problem one level up.
If you are on React 19 with the compiler enabled, a lot of this memoisation is handled automatically. It does not fix a genuinely incorrect dependency, only unnecessary re-creation.
Cause 5: a parent recreating props
function Parent() {
return <Child config={{ theme: "dark" }} onSave={() => save()} />;
}
Both props are new references on every parent render. If Child has an effect depending on either, it runs every time the parent renders, regardless of React.memo.
React.memo does a shallow comparison, and a new object fails it. So the memo does nothing and people conclude memo is broken.
Same fixes: hoist constants out, useCallback for handlers that are dependencies, or restructure so the child does not need the object.
Finding it with the Profiler
Open React DevTools, Profiler tab, gear icon, enable "Record why each component rendered".
Record a few seconds of the loop. Each render shows a reason:
- Props changed: config tells you exactly which prop, and if the value looks identical, it is a reference problem
- Hook 3 changed points at a specific hook by index, counted in declaration order
- The parent rendered means the child is fine and you should look up the tree
That last one matters. Chasing a re-render in a child when the parent is the cause wastes a lot of time, and the Profiler tells you in one click.
For a loop that crashes before you can profile, comment out effects one at a time. Crude and it converges in a few reloads.
A useful debugging hook
function useWhyDidYouUpdate(name, props) {
const prev = useRef();
useEffect(() => {
if (prev.current) {
const changed = {};
for (const key of Object.keys({ ...prev.current, ...props })) {
if (prev.current[key] !== props[key]) {
changed[key] = { from: prev.current[key], to: props[key] };
}
}
if (Object.keys(changed).length) console.log("[why-update]", name, changed);
}
prev.current = props;
});
}
Call it at the top of a suspect component with its props. It logs exactly which prop changed identity, and seeing from: {limit: 10}, to: {limit: 10} with different references makes the reference problem obvious in a way that reading code does not.
Prevention
react-hooks/exhaustive-deps. The lint rule. Turn it on and do not suppress it casually. Most infinite loops from effects are dependency arrays that were wrong in a way the rule catches.
When the rule complains and adding the dependency creates a loop, that is a signal the effect is structured wrong, not that the rule is wrong. It is telling you the effect depends on something it changes.
Ask whether you need the effect at all. A large share of these are effects that should not exist: deriving state from props, transforming data for rendering, or responding to a user event. The React documentation section on this is genuinely worth reading and it eliminates the problem rather than fixing it.
Prefer computing during render to storing derived values in state. Fewer renders, no synchronisation bugs, less code.
Use a data library for fetching. TanStack Query or SWR handle caching, deduplication, and cancellation. Manual fetching in useEffect is a category of code that is easy to get subtly wrong, and the same reasoning applies as with effects running twice under Strict Mode.