This React error occurs when a component triggers too many consecutive state updates, usually from calling setState in useEffect without proper dependencies, or in event handlers without guards. React limits nested updates to around 50 to prevent infinite loops that would freeze the browser.
This error is React's safety mechanism to prevent infinite render loops from crashing your browser. When React detects more than approximately 50 consecutive state updates happening one after another, it throws this error and stops the update cycle. The root cause is typically a state update that triggers a re-render, which then triggers another state update, creating a cycle that never ends. This commonly happens when setState is called directly in the component body, in useEffect without proper dependencies, or in lifecycle methods like componentDidUpdate without conditional checks. React enforces this limit because without it, an infinite loop would consume all available memory and freeze the browser tab. Unlike many React errors that are warnings, this one completely halts your component from rendering because continuing would be harmful. The error message usually indicates which component is causing the loop, though in complex applications it may take some debugging to identify the exact state update creating the cycle.
Check the error stack trace in your browser console to identify which component is causing the error:
Error: Maximum update depth exceeded. This can happen when a component
repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
React limits the number of nested updates to prevent infinite loops.
at ComponentName (ComponentName.tsx:45)Use React DevTools Profiler to record the interaction that triggers the error. Look for components with many consecutive re-renders. Examine any setState calls, state updater functions from useState, or dispatch calls in the problematic component.
Never call state setters in the component render body. Move them to useEffect or event handlers:
// ❌ Bad: Calling setState during render
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // Infinite loop!
return <div>{count}</div>;
}
// ✅ Good: Use useEffect with empty dependencies
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, []); // Only runs once on mount
return <div>{count}</div>;
}
// ✅ Better: Initialize state with the value
function Counter() {
const [count] = useState(1); // Start at 1 if that's what you need
return <div>{count}</div>;
}Ensure useEffect dependencies don't cause the effect to re-run when it updates those dependencies:
// ❌ Bad: Updating state that triggers the effect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [user]); // user changes, effect runs, sets user, repeat!
return <div>{user?.name}</div>;
}
// ✅ Good: Depend only on external values
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Only re-run when userId changes
return <div>{user?.name}</div>;
}If useEffect must update a state it depends on, use the functional updater form to avoid the dependency.
Pass function references to event handlers, don't call them directly:
// ❌ Bad: Calling function immediately
function Button() {
const [count, setCount] = useState(0);
return (
<button onClick={setCount(count + 1)}>
{/* setCount is called during render! */}
Click me
</button>
);
}
// ✅ Good: Pass function reference
function Button() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Click me: {count}
</button>
);
}
// ✅ Better: Use functional update form
function Button() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Click me: {count}
</button>
);
}Objects and functions recreated on every render cause useEffect to run repeatedly:
// ❌ Bad: Object created on every render
function Component() {
const [data, setData] = useState(null);
const options = { enabled: true }; // New object each render
useEffect(() => {
fetchData(options).then(setData);
}, [options]); // options changes every render!
return <div>{data}</div>;
}
// ✅ Good: Use useMemo for objects
function Component() {
const [data, setData] = useState(null);
const options = useMemo(() => ({ enabled: true }), []);
useEffect(() => {
fetchData(options).then(setData);
}, [options]);
return <div>{data}</div>;
}
// ✅ Better: Use primitive dependencies or move object inside effect
function Component() {
const [data, setData] = useState(null);
useEffect(() => {
const options = { enabled: true };
fetchData(options).then(setData);
}, []); // No object dependencies needed
return <div>{data}</div>;
}Use useCallback for function dependencies to prevent recreation.
For class components, always check conditions before calling setState in componentDidUpdate:
// ❌ Bad: Unconditional setState
class Component extends React.Component {
componentDidUpdate(prevProps) {
this.setState({ updated: true }); // Infinite loop!
}
}
// ✅ Good: Conditional setState
class Component extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.setState({ loading: true });
this.fetchUser(this.props.userId);
}
}
}
// ✅ Better: Use getDerivedStateFromProps for state derived from props
class Component extends React.Component {
static getDerivedStateFromProps(props, state) {
if (props.userId !== state.prevUserId) {
return { prevUserId: props.userId, loading: true };
}
return null;
}
}For event handlers that trigger frequent state updates, use throttling or debouncing:
import { useState, useCallback } from 'react';
import { debounce } from 'lodash';
// ❌ Bad: Updating on every input change can trigger rapid re-renders
function SearchInput() {
const [value, setValue] = useState('');
const [results, setResults] = useState([]);
const handleChange = (e) => {
setValue(e.target.value);
fetchResults(e.target.value).then(setResults);
};
return <input onChange={handleChange} />;
}
// ✅ Good: Debounce the search
function SearchInput() {
const [value, setValue] = useState('');
const [results, setResults] = useState([]);
const debouncedSearch = useCallback(
debounce((query) => {
fetchResults(query).then(setResults);
}, 300),
[]
);
const handleChange = (e) => {
setValue(e.target.value);
debouncedSearch(e.target.value);
};
return <input value={value} onChange={handleChange} />;
}This prevents overwhelming React with too many state updates in quick succession.
React's Update Limit: React limits nested updates to approximately 50 (the exact number may vary between versions). This threshold balances catching infinite loops while allowing legitimate nested updates like cascading state changes or multiple useEffect chains.
Batching in React 18+: React 18 introduced automatic batching for all updates, including those in promises, setTimeout, and native event handlers. While this reduces re-renders, it doesn't prevent infinite loops - you still need proper dependency management and conditional guards.
Debugging Tools: Use React DevTools Profiler's "Highlight updates" feature to visually see which components are re-rendering excessively. Enable "Record why each component rendered" to see what props or state changes triggered each render, making it easier to trace the loop source.
Testing Considerations: Write unit tests that verify components don't update in loops. Use act() from react-testing-library to catch async state update issues. Monitor test execution time - tests that hang or timeout often indicate infinite update loops.
Performance Monitoring: In production, implement error boundaries with monitoring tools like Sentry to catch these errors before users report them. Track component re-render counts in performance metrics to identify potential issues before they hit the update limit. Consider using React.memo and useMemo strategically to prevent unnecessary re-renders that could contribute to hitting the limit.
Prop spreading could cause security issues
Prop spreading could cause security issues
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
React.FC expects children prop to be defined
React.FC no longer includes implicit children prop