Fix React Maximum Update Depth Exceeded: Root Causes and Minimal Checks

Topic: react-maximum-update-depth-exceededUpdated 7/18/2026

Quick Answer

  • The error occurs when React detects a render loop — typically when setState is called inside useEffect without proper dependency management, or when state updates are triggered directly during rendering.
  • First check: Look for setState calls inside useEffect that depend on the same state they update, or setState calls directly in the component body (not inside an event handler or effect).
  • Minimal fix: Ensure useEffect dependency arrays are correct, use functional updates (setCount(prev => prev + 1)), and avoid creating new object/function references on every render without memoization.
  • Applies to: React 16.8+ (hooks), both function and class components. The error is most common in development mode but can crash production apps.

What Problem It Solves

The "Maximum update depth exceeded" error is React's safety mechanism to prevent infinite render loops. Without this limit, a buggy component would freeze the browser tab by triggering endless re-renders. Understanding and fixing this error helps you:

  • Write stable React components that don't crash
  • Optimize rendering performance by eliminating unnecessary re-renders
  • Master React's state update lifecycle and dependency management
  • Debug complex component interactions efficiently

Root Cause Analysis

The error occurs when React detects that a component has re-rendered more than 50 times (the default limit) in a single update cycle. This typically happens through one of these patterns:

Pattern 1: useEffect updating its own dependency

JSX
// ❌ BAD: Infinite loop
useEffect(() => {
  setCount(count + 1);  // Updates count
}, [count]);  // count is a dependency → triggers re-run → infinite loop

Pattern 2: setState during render

JSX
// ❌ BAD: setState called directly in render
function MyComponent() {
  const [count, setCount] = useState(0);
  setCount(count + 1);  // Triggers re-render, which calls setCount again
  return <div>{count}</div>;
}

Pattern 3: Unstable object/function references in dependencies

JSX
// ❌ BAD: New object created every render
useEffect(() => {
  fetchData(filters);
}, [filters]);  // filters is recreated every render → infinite loop

Pattern 4: Class component lifecycle issues

JSX
// ❌ BAD: componentDidUpdate without condition
componentDidUpdate() {
  this.setState({ count: this.state.count + 1 });  // No condition check
}

Common Errors and Fixes

Error MessageRoot CauseFix
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.useEffect updates state that's in its dependency arrayUse functional update setCount(prev => prev + 1) with empty deps, or restructure logic
Too many re-renders. React limits the number of renders to prevent an infinite loop.setState called directly in render function bodyMove setState into event handlers, useEffect, or useCallback
Cannot update a component (Parent) while rendering a different component (Child).Child component updates parent state during renderMove state update to useEffect in child, or restructure component hierarchy
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.Class component lifecycle method updates state unconditionallyAdd condition checks in componentDidUpdate, or use shouldComponentUpdate

Minimal Working Configuration

Fix for useEffect infinite loop

JSX
// ✅ GOOD: Functional update with empty dependency array
useEffect(() => {
  setCount(prev => prev + 1);
}, []);  // Empty array = run once

// ✅ GOOD: Conditional update with proper dependencies
useEffect(() => {
  if (shouldUpdate) {
    setCount(count + 1);
  }
}, [shouldUpdate, count]);  // Only re-run when these change

Fix for render-time setState

JSX
// ✅ GOOD: setState in event handler
function MyComponent() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(prev => prev + 1);
  };
  
  return <button onClick={handleClick}>{count}</button>;
}

Fix for unstable references

JSX
// ✅ GOOD: Memoize objects and functions
const filters = useMemo(() => ({ status: 'active', page: 1 }), []);
const handleSubmit = useCallback((data) => {
  // handle submission
}, [dependencies]);

useEffect(() => {
  fetchData(filters);
}, [filters]);

Debugging Tools and Techniques

Custom useRenderCount Hook

JSX
import { useRef, useEffect } from 'react';

function useRenderCount(componentName = 'Component') {
  const count = useRef(0);
  
  useEffect(() => {
    count.current += 1;
    console.log(`${componentName} rendered ${count.current} times`);
  });
  
  return count.current;
}

// Usage
function MyComponent() {
  useRenderCount('MyComponent');
  return <div>Hello</div>;
}

Using why-did-you-render

Install the library for development only:

BASH
npm install @welldone-software/why-did-you-render

Configure it in your app entry point:

JSX
import React from 'react';

if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}

Then mark components for tracking:

JSX
const MyComponent = React.memo(function MyComponent(props) {
  return <div>{props.name}</div>;
});

MyComponent.whyDidYouRender = true;

React DevTools Profiler

  1. Open React DevTools in browser
  2. Go to the Profiler tab
  3. Click the record button (circle icon)
  4. Interact with your app
  5. Stop recording to see flamegraph of renders
  6. Look for components that render excessively without prop/state changes

Production Notes and Security Checks

Critical: Remove Debug Code in Production

JSX
// ✅ GOOD: Conditional debug code
if (process.env.NODE_ENV === 'development') {
  // Debug hooks, why-did-you-render, console.logs
}

// ❌ BAD: Debug code in production
useEffect(() => {
  console.log('State changed:', state);  // Logs in production
}, [state]);

Performance Impact

Debug ToolDevelopment ImpactProduction Risk
why-did-you-renderHigh (slows renders)Must be removed
Custom render count hooksMediumLogs in console
React DevTools ProfilerMediumNot active without DevTools
console.log/console.traceLowPollutes console, exposes internals

Security Considerations

  • Debug logs may expose component state, API responses, or business logic
  • why-did-you-render can reveal prop structures and data flow
  • Always use environment variables (process.env.NODE_ENV) to gate debug code
  • Consider using build-time flags (e.g., Webpack DefinePlugin) to strip debug code entirely

React 18 Concurrent Mode Caveats

  • Some debugging techniques may produce false positives in concurrent mode
  • useLayoutEffect behavior differs from useEffect in concurrent rendering
  • Test thoroughly if using React 18's createRoot with concurrent features

FAQ

Q: Why does updating state inside useEffect cause an infinite loop?

A: When useEffect's dependency array includes the state variable being updated, each state change triggers the effect to re-run, which updates state again, creating a cycle. For example: useEffect(() => { setCount(count + 1); }, [count]); — every count update triggers the effect, which increments count again. Fix by using functional updates (setCount(prev => prev + 1)) with an empty dependency array, or move the update logic to an event handler.

Q: How do useCallback and useMemo help prevent infinite render loops?

A: useCallback and useMemo stabilize function and object references through memoization. Without them, every render creates new function/object instances, which can trigger useEffect re-runs (if used as dependencies) or cause React.memo-wrapped children to re-render. Use useCallback(fn, deps) for functions and useMemo(() => obj, deps) for objects to ensure stable references across renders.

Q: Can this error occur in class components?

A: Yes. In class components, the error typically happens when setState is called inside componentDidUpdate without a condition check, or inside componentWillUpdate. Always wrap state updates in conditional checks: componentDidUpdate(prevProps, prevState) { if (this.state.count !== prevState.count) { /* update logic */ } }. Consider migrating to function components with hooks for simpler dependency management.

Related Guides