This error occurs when React components are nested too deeply or contain infinite recursion, consuming all available call stack memory. It happens when components re-render infinitely, create recursive loops without base cases, or have circular dependencies.
The "Maximum call stack size exceeded" error in React means the JavaScript engine has exhausted its call stack—the memory area where function calls are tracked and executed. In React applications, this typically indicates either deeply nested components without a termination condition or a render loop where state updates trigger re-renders that trigger more state updates. React's component tree traversal combined with recursive rendering or uncontrolled state updates can quickly consume the entire stack, causing the browser or Node.js process to crash.
The most common cause is updating state directly in the render phase without conditions:
// BAD - This causes infinite loop
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // Called on every render, triggers re-render
return <div>{count}</div>;
}
// GOOD - Use useEffect with proper dependency array
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, []); // Empty dependency array - runs once
return <div>{count}</div>;
}Review your component for any state-setting calls outside of event handlers or useEffect hooks. Move them inside useEffect with appropriate dependency arrays.
useEffect can cause stack overflow if a dependency value changes as a result of the effect:
// BAD - count in dependency array, but we update count
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Updates count, effect runs again
}, [count]); // count is dependency - infinite loop
return <div>{count}</div>;
}
// GOOD - Use functional state update or proper dependency array
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(prev => prev + 1); // Update state based on previous value
}, []); // Empty array - runs once only
return <div>{count}</div>;
}
// GOOD - Separate state and effect concerns
function Component() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(d => {
setData(d);
setLoading(false);
});
}, []); // Dependencies don't include state being updated
return <div>{loading ? 'Loading...' : data}</div>;
}Check all useEffect hooks and verify that:
- Dependency array doesn't include state variables that get updated in the effect
- Or use functional state updates (setState(prev => ...))
- Or add proper guard conditions to prevent updates
Recursive component import happens due to circular dependencies:
// BadComponent.tsx
import BadComponent from './BadComponent'; // Circular import!
function BadComponent() {
return <BadComponent />; // Also renders itself
}
export default BadComponent;
// Solution 1: Separate logic and rendering
// ComponentLogic.tsx - holds the recursive logic
function TreeNode({ items }: { items: TreeItem[] }) {
return items.map(item => (
<div key={item.id}>
{item.name}
{item.children && <TreeNode items={item.children} />}
</div>
));
}
// Solution 2: Use forward references for complex recursive patterns
const LazyRecursive = lazy(() => import('./RecursiveComponent'));Check your imports and JSX. Ensure components:
- Don't import themselves
- Don't have circular dependency chains (A → B → A)
- Use proper module splitting to avoid cycles
If you intentionally use recursion for tree-like data, ensure it has a termination condition:
// BAD - No base case, infinite recursion
function Tree({ node }) {
return (
<div>
{node.name}
<Tree node={node.child} /> {/* Always recurses */}
</div>
);
}
// GOOD - Base case prevents infinite recursion
function Tree({ node }) {
// Base case - stop when node is null/undefined
if (!node) return null;
// Recursive case - only recurse if children exist
return (
<div>
{node.name}
{node.children?.map(child => (
<Tree key={child.id} node={child} />
))}
</div>
);
}
// GOOD - Limit recursion depth for safety
function Tree({ node, depth = 0, maxDepth = 20 }) {
if (!node || depth >= maxDepth) return null;
return (
<div>
{node.name}
{node.children?.map(child => (
<Tree key={child.id} node={child} depth={depth + 1} maxDepth={maxDepth} />
))}
</div>
);
}For recursive components:
- Always check if data is null/undefined before recursing
- Verify child data actually exists before passing to recursive call
- Consider adding a max depth limit as safety net
Event handlers or callbacks can trigger infinite updates:
// BAD - onClick sets state that re-renders the handler
function Component() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Click me {count} {/* onClick reference changes each render */}
</button>
);
}
// GOOD - Use useCallback to stabilize handler
function Component() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <button onClick={handleClick}>Click me {count}</button>;
}
// BAD - Passing handler that updates parent state
function Parent() {
const [data, setData] = useState([]);
return (
<Child onUpdate={() => setData([...data, 'item'])} />
{/* onUpdate prop changes each render, can cause infinite updates */}
);
}
// GOOD - Memoize the callback
function Parent() {
const [data, setData] = useState([]);
const handleUpdate = useCallback(() => {
setData(prev => [...prev, 'item']);
}, []);
return <Child onUpdate={handleUpdate} />;
}Review callback and event handler patterns:
- Use useCallback to prevent handler references from changing
- Pass functional state updates instead of current state
- Avoid creating new objects/arrays in callback dependencies
For large datasets, convert recursive rendering to iterative approaches:
// Recursive approach - can overflow with large trees
function TreeNodeRecursive({ items }) {
return items.map(item => (
<div key={item.id}>
{item.name}
{item.children && <TreeNodeRecursive items={item.children} />}
</div>
));
}
// Iterative approach using a queue - avoids stack overflow
function TreeNodeIterative({ root }) {
const queue = [root];
const elements = [];
while (queue.length > 0) {
const node = queue.shift();
if (!node) continue;
elements.push(
<div key={node.id}>
{node.name}
</div>
);
if (node.children) {
queue.push(...node.children);
}
}
return <>{elements}</>;
}
// Or use flatMap to flatten the tree first
function flattenTree(items, depth = 0) {
const MAX_DEPTH = 100;
if (depth >= MAX_DEPTH) return [];
return items.flatMap(item => [
{ ...item, depth },
...(item.children ? flattenTree(item.children, depth + 1) : [])
]);
}
function TreeNodeFlat({ items }) {
const flat = flattenTree(items);
return flat.map(item => (
<div key={item.id} style={{ marginLeft: `${item.depth * 20}px` }}>
{item.name}
</div>
));
}For large hierarchical data:
- Use a queue or stack to process items iteratively
- Flatten the tree structure before rendering
- Virtual scrolling libraries (react-window) for very large lists
React DevTools can help identify which component is causing the loop:
1. Open React DevTools (Extensions → Components tab)
2. Before the crash, switch to the Profiler tab
3. Record a profile as the error happens
4. Look for components that render repeatedly
5. Identify the component name in the call stack
Alternatively, add console.logs to track re-renders:
function ProblematicComponent() {
console.log('ProblematicComponent rendered');
// If you see this logged hundreds of times in quick succession,
// this is your culprit
return <div>Content</div>;
}
// Or use custom hook to track
function useRenderCount(componentName) {
const renders = useRef(0);
useEffect(() => {
renders.current++;
console.log(`${componentName} rendered ${renders.current} times`);
if (renders.current > 50) {
console.warn(`${componentName} rendered over 50 times - possible infinite loop`);
}
});
}
function Component() {
useRenderCount('Component');
return <div>Content</div>;
}Once you identify the component:
- Review its state management
- Check useEffect dependencies
- Look for circular component imports
Performance Considerations: Even if your component doesn't crash, deeply nested component trees can impact performance. React must traverse the entire tree during reconciliation. Consider:
- Using component composition patterns to flatten the tree
- React.memo() to prevent unnecessary re-renders in subtrees
- Splitting large trees across multiple pages or virtual scrolling
- Using Suspense boundaries to lazy-load parts of the tree
Debugging in Production: If this error appears in production but not in development, it may indicate:
- A race condition in data fetching
- Timing-dependent state updates
- Browser memory constraints on user machines
- Use Sentry or similar error tracking to capture stack traces and replay the issue
Stack Overflow in Next.js Build: When building Next.js apps, stack overflow during compilation often indicates:
- Infinite loops in getServerSideProps or getStaticProps
- Circular imports in the page tree
- Run next build --debug for more detailed error information
Prop spreading could cause security issues
Prop spreading could cause security issues
Error: error:0308010C:digital envelope routines::unsupported
Error: error:0308010C:digital envelope routines::unsupported
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.
React Hook useEffect placed inside a condition
Hook can only be called inside the body of a function component
Hook can only be called inside the body of a function component
Rollup failed to resolve import during build
How to fix "Rollup failed to resolve import" in React