Fix: Too many re-renders. React limits the number of renders to prevent an infinite loop.
The Error
You render a React component and your app crashes with this error:
Too many re-renders. React limits the number of renders to prevent an infinite loop.Sometimes you also see this variant in the stack trace:
Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
at renderWithHooks (react-dom.development.js)This means your component is triggering a state update on every render, which causes another render, which triggers another state update — an infinite loop. React detects this after a threshold (usually around 50 consecutive renders) and kills the loop by throwing this error.
Why This Happens
React re-renders a component whenever its state or props change. If something inside the render body calls setState unconditionally, the cycle never stops:
- Component renders.
- During render,
setStateis called. - State changes, so React schedules another render.
- Go to step 1.
The same loop happens when a useEffect updates state in a way that keeps triggering itself, or when you accidentally call a function in JSX instead of passing a reference to it.
Fix
1. Calling a Function in JSX Instead of Passing a Reference
This is the single most common cause. You write onClick={handleClick()} with parentheses, which calls the function during render instead of passing it as a callback.
Broken code:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={setCount(count + 1)}>
Count: {count}
</button>
);
}setCount(count + 1) executes immediately during render. It updates state, which triggers a re-render, which calls setCount again.
Fix — wrap it in an arrow function or pass a reference:
// Option A: arrow function
<button onClick={() => setCount(count + 1)}>
// Option B: function reference (no arguments)
<button onClick={handleClick}>
// Option C: updater function via arrow
<button onClick={() => setCount(prev => prev + 1)}>The arrow function creates a callback that only runs when the button is clicked, not on every render.
This same mistake shows up with other handlers too:
// Wrong — calls immediately
<input onChange={handleChange(e)} />
// Right — passes a reference
<input onChange={handleChange} />
// Right — wraps in arrow function (if you need to pass extra args)
<input onChange={(e) => handleChange(e, someId)} />2. Calling setState Directly in the Render Body
Any state update in the component body (outside of an event handler, useEffect, or callback) runs on every render and triggers an infinite loop.
Broken code:
function UserGreeting({ name }) {
const [greeting, setGreeting] = useState('');
// This runs on every render — infinite loop
setGreeting(`Hello, ${name}!`);
return <h1>{greeting}</h1>;
}Fix — move it into a useEffect or derive the value directly:
// Option A: useEffect (when you actually need state)
function UserGreeting({ name }) {
const [greeting, setGreeting] = useState('');
useEffect(() => {
setGreeting(`Hello, ${name}!`);
}, [name]);
return <h1>{greeting}</h1>;
}
// Option B: derive the value (no state needed at all)
function UserGreeting({ name }) {
const greeting = `Hello, ${name}!`;
return <h1>{greeting}</h1>;
}Option B is almost always better. If a value can be computed from props or existing state, don’t put it in state. Derived values don’t need useState or useEffect. This eliminates the re-render loop entirely and simplifies your component. If you’re accessing properties on a potentially undefined value in your derived state, also watch out for Object is possibly undefined errors in TypeScript.
3. useEffect with Missing or Wrong Dependency Array
A useEffect without a dependency array runs after every render. If it calls setState, each state update triggers a new render, which triggers the effect again.
Broken code:
function DataLoader() {
const [data, setData] = useState(null);
// No dependency array — runs after every render
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData); // setState triggers re-render, which runs this effect again
});
return <pre>{JSON.stringify(data)}</pre>;
}Fix — add a dependency array:
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []); // Empty array = runs once on mountIf you need the effect to re-run when specific values change, list those values:
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setData);
}, [userId]); // Re-runs only when userId changes4. useEffect That Updates Its Own Dependency
This creates a subtler infinite loop. The effect runs, updates state, and that state is in the dependency array, so the effect runs again.
Broken code:
function ItemList({ items }) {
const [sortedItems, setSortedItems] = useState([]);
useEffect(() => {
setSortedItems([...items].sort((a, b) => a.name.localeCompare(b.name)));
}, [items, sortedItems]); // sortedItems is updated by this effect — infinite loop
}Every time setSortedItems runs, sortedItems changes, which triggers the effect again.
Fix — remove the state you’re setting from the dependency array:
useEffect(() => {
setSortedItems([...items].sort((a, b) => a.name.localeCompare(b.name)));
}, [items]); // Only re-run when items changesBetter yet, derive it with useMemo:
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);No state, no effect, no loop.
5. Object or Array as useEffect Dependency (Reference Equality)
JavaScript compares objects and arrays by reference, not by content. If you create a new object or array on every render, useEffect sees a “new” dependency each time and runs again.
Broken code:
function UserProfile({ userId }) {
const [profile, setProfile] = useState(null);
// New object created every render — different reference each time
const options = { includeAvatar: true, format: 'full' };
useEffect(() => {
fetch(`/api/users/${userId}`, { body: JSON.stringify(options) })
.then(res => res.json())
.then(setProfile);
}, [userId, options]); // options is a new object every render — infinite loop
}options is re-created on every render. Even though its content is identical, { includeAvatar: true } !== { includeAvatar: true } in JavaScript. The effect sees a new reference, runs again, calls setProfile, which triggers a re-render, which creates a new options object.
Fix — stabilize the reference with useMemo:
const options = useMemo(
() => ({ includeAvatar: true, format: 'full' }),
[] // Stable reference — only created once
);
useEffect(() => {
fetch(`/api/users/${userId}`, { body: JSON.stringify(options) })
.then(res => res.json())
.then(setProfile);
}, [userId, options]); // options reference is stable nowOr move the object inside the effect:
useEffect(() => {
const options = { includeAvatar: true, format: 'full' };
fetch(`/api/users/${userId}`, { body: JSON.stringify(options) })
.then(res => res.json())
.then(setProfile);
}, [userId]); // No need to depend on optionsMoving the object inside the effect is the cleanest solution when nothing outside the effect needs it.
The same issue applies to arrays, functions, and any non-primitive value:
// These all create new references on every render:
const tags = ['react', 'javascript']; // new array each render
const config = { theme: 'dark' }; // new object each render
const format = (val) => val.toFixed(2); // new function each renderUse useMemo for objects/arrays and useCallback for functions to keep references stable.
6. Callback Function as useEffect Dependency
Functions defined inside a component are re-created on every render. If you use one as a useEffect dependency, the effect runs every time.
Broken code:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
// New function reference every render
const fetchResults = (q) => {
fetch(`/api/search?q=${q}`)
.then(res => res.json())
.then(setResults);
};
useEffect(() => {
fetchResults(query);
}, [query, fetchResults]); // fetchResults changes every render
}Fix — wrap the function with useCallback:
const fetchResults = useCallback((q) => {
fetch(`/api/search?q=${q}`)
.then(res => res.json())
.then(setResults);
}, []); // setResults is stable, so no dependencies needed
useEffect(() => {
fetchResults(query);
}, [query, fetchResults]); // fetchResults reference is now stableOr define the function inside the effect:
useEffect(() => {
const fetchResults = (q) => {
fetch(`/api/search?q=${q}`)
.then(res => res.json())
.then(setResults);
};
fetchResults(query);
}, [query]);7. Conditional State Update That Always Triggers
You add a condition around a setState call, but the condition is always true — so the “guard” doesn’t actually prevent the loop.
Broken code:
function Formatter({ text }) {
const [formatted, setFormatted] = useState('');
useEffect(() => {
const result = text.trim().toLowerCase();
// This condition is always true on the first pass, and the new value
// is always "different" because formatted starts as ''
if (result !== formatted) {
setFormatted(result);
}
}, [text, formatted]); // formatted is a dependency AND gets set — loop
return <p>{formatted}</p>;
}On first render, formatted is '' and result is the processed text. They differ, so setFormatted runs. Now formatted changes, the effect fires again, but this time they match — so it stops. This might work, but adding formatted to the dependency array is unnecessary and risky. If the logic is more complex, it can loop forever.
Fix — remove the output state from dependencies:
useEffect(() => {
setFormatted(text.trim().toLowerCase());
}, [text]); // Only depends on the inputOr derive it directly:
const formatted = text.trim().toLowerCase();No state, no effect, no risk of looping.
8. setState in a Component That Renders a Parent
If a child component updates state that’s lifted to a parent, and the parent re-renders the child as a result, you can get a loop.
Broken code:
function Parent() {
const [value, setValue] = useState('');
return <Child onChange={setValue} />;
}
function Child({ onChange }) {
// Calls onChange (which is setState) during render — infinite loop
onChange('default');
return <div>Child</div>;
}Fix — move the call into useEffect:
function Child({ onChange }) {
useEffect(() => {
onChange('default');
}, [onChange]);
return <div>Child</div>;
}Still Not Working?
Use React DevTools Profiler to Find the Loop
If you can’t identify which state update is causing the loop, the React DevTools Profiler can show you.
- Install React Developer Tools browser extension.
- Open DevTools and go to the Profiler tab.
- Click Record, then trigger the error.
- Look at which components re-render repeatedly and what caused each render (state update, props change, or parent re-render).
You can also add a console.log at the top of your component to see how many times it renders:
function MyComponent() {
console.log('MyComponent rendered');
// ...
}If you see that message flooding the console, the loop is in that component. If the fetch call inside your effect is failing, make sure your localhost server is actually running.
Check for setState in Third-Party Library Callbacks
Some libraries call your callbacks synchronously during render. If your callback calls setState, you get a loop. Check if a third-party component is triggering your state update during its own render cycle.
// Some form libraries call validation functions during render
const validate = (values) => {
// If this calls setState, you'll loop
setErrors(checkErrors(values)); // Move this to an effect instead
return checkErrors(values);
};Verify You’re Not Passing a New Inline Object to a Memoized Component
React.memo prevents re-renders when props don’t change. But if you pass a new object literal as a prop, the reference changes every render and React.memo can’t help:
// This defeats React.memo — style is a new object every render
<MemoizedChild style={{ color: 'red' }} />
// Fix — stable reference
const style = useMemo(() => ({ color: 'red' }), []);
<MemoizedChild style={style} />Check for State Updates in useLayoutEffect
useLayoutEffect runs synchronously after DOM mutations but before the browser paints. A setState inside useLayoutEffect triggers an immediate synchronous re-render. If that re-render triggers useLayoutEffect again, you get a loop that’s harder to debug because it happens before the browser even paints — your screen may freeze without any visible output.
Apply the same dependency array rules as useEffect. Don’t update state that’s also in the dependency array.
Check for Infinite Loops in Custom Hooks
If you’re using a custom hook that internally manages state and effects, the loop might be inside the hook. Trace into the hook’s source code and check if any of its useEffect calls update state that’s also in the dependency array.
// A buggy custom hook
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
// If this creates a new object every time and something upstream
// depends on the size reference, it can cascade into a loop
setSize({ width: window.innerWidth, height: window.innerHeight });
}); // Missing dependency array — runs every render
return size;
}Add the missing [] dependency array, or use an event listener pattern:
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // Runs once, updates on resize events only
return size;
}Related: Fix: React Hook is called conditionally | Fix: Hydration failed because the initial UI does not match what was rendered on the server | Fix: TypeError: Cannot read properties of undefined
Related Articles
Fix: React Hook "useXxx" is called conditionally. React Hooks must be called in the exact same order in every component render.
How to fix 'React Hook is called conditionally', 'Rendered more hooks than during the previous render', 'Invalid hook call', and other React Hooks order errors. Covers conditional hooks, hooks in loops, hooks after early returns, duplicate React versions, and ESLint setup.
Fix: Next.js Image Optimization Errors – Invalid src, Missing Loader, or Unoptimized
How to fix Next.js Image component errors including 'Invalid src prop', 'hostname not configured', missing loader, and optimization failures in production.
Fix: React Can't Perform a State Update on an Unmounted Component
How to fix the React warning 'Can't perform a React state update on an unmounted component' caused by async operations, subscriptions, or timers.
Fix: React useEffect runs infinitely (infinite loop / maximum update depth exceeded)
How to fix useEffect infinite loops in React — covers missing dependency arrays, referential equality, useCallback, unconditional setState, data fetching cleanup, event listeners, useRef, previous value comparison, and the exhaustive-deps lint rule.