# How to fix maximum update depth exceeded error in React

# What the error means

This error occurs when React detects too many state updates happening one after another. React has a limit (around 50) of how many render updates can happen in a row. When this limit is exceeded, React stops with this error to prevent your browser from freezing.

# Common causes

## 1\. Setting state in component body

```typescript
function Counter() {
  const [count, setCount] = useState(0);

  // This runs on every render, creating an infinite loop
  setCount(count + 1);

  return <div>{count}</div>;
}
```

## 2\. Missing dependency array in useEffect

```typescript
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then((data) => setUser(data));
  }); // No dependency array = runs after every render
}
```

## 3\. Circular state dependencies (most common)

```typescript
function Dashboard() {
  const [filters, setFilters] = useState({});
  const [results, setResults] = useState([]);

  // Update results when filters change
  useEffect(() => {
    fetchResults(filters).then((data) => setResults(data));
  }, [filters]);

  // But also update filters when results change
  useEffect(() => {
    if (results.length === 0) {
      setFilters({ ...filters, showEmpty: true });
    }
  }, [results]); // Creates a cycle: filters → results → filters
}
```

# How to debug this error

## Step 1: Add render counting

```typescript
function ProblemComponent() {
  // Add this to suspect components
  const renderCount = useRef(0);
  renderCount.current++;
  console.log(`Component rendered ${renderCount.current} times`);

  // Rest of component...
}
```

## Step 2: Track state changes with named console logs

```typescript
useEffect(() => {
  console.log("Effect running with filters:", filters);
  fetchResults(filters).then((data) => {
    console.log("Setting results from fetch");
    setResults(data);
  });
}, [filters]);
```

## Step 3: Use the stack trace to find the source

```typescript
// Understand what led to this setState
// Helpful if you don't know what's causing this setter to be called
console.log(new Error().stack);
setResults(data);
```

## Step 4: Comment out effects one by one

Temporarily disable suspected useEffect hooks to isolate which one is causing the problem (binary elimination).

# Solutions for different scenarios

## For simple state updates in component body

Move the state update to an event handler or useEffect:

```typescript
function Counter() {
  const [count, setCount] = useState(0);

  // Only run once after initial render
  useEffect(() => {
    setCount(count + 1);
  }, []);

  return <div>{count}</div>;
}
```

## For missing dependency arrays

Add the appropriate dependencies:

```typescript
useEffect(() => {
  fetchUser(userId).then((data) => setUser(data));
}, [userId]); // Only runs when userId changes
```

## For circular dependencies

### 1\. Combine related state

```typescript
const [state, setState] = useState({
  filters: {},
  results: [],
});

// Update atomically
const updateFilters = (newFilters) => {
  setState((prev) => ({
    ...prev,
    filters: newFilters,
  }));
};
```

### 2\. Use useReducer for complex state

```typescript
const initialState = { filters: {}, results: [] };

function reducer(state, action) {
  switch (action.type) {
    case "SET_FILTERS":
      return { ...state, filters: action.payload };
    case "SET_RESULTS":
      return { ...state, results: action.payload };
    case "HANDLE_EMPTY_RESULTS":
      // Logic for handling empty results without circular updates
      return state.results.length === 0
        ? { ...state, filters: { ...state.filters, showEmpty: true } }
        : state;
    default:
      return state;
  }
}

function Dashboard() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    fetchResults(state.filters).then((data) => {
      dispatch({ type: "SET_RESULTS", payload: data });
      // Let the reducer handle the empty check
      dispatch({ type: "HANDLE_EMPTY_RESULTS" });
    });
  }, [state.filters]);
}
```

### 3\. Use refs to break the cycle

```typescript
function Dashboard() {
  const [filters, setFilters] = useState({});
  const [results, setResults] = useState([]);
  const prevResultsRef = useRef([]);

  useEffect(() => {
    fetchResults(filters).then((data) => setResults(data));
  }, [filters]);

  useEffect(() => {
    // Only update filters if this is the first time we've had empty results
    if (results.length === 0 && prevResultsRef.current.length > 0) {
      setFilters({ ...filters, showEmpty: true });
    }
    prevResultsRef.current = results;
  }, [results]);
}
```

# Prevention best practices

1. Never update state directly in component body
    
2. Always include dependency arrays in useEffect
    
3. Be careful with multiple useEffect hooks that update different state
    
4. Use functional updates when new state depends on old state:
    
    ```typescript
    // Instead of:
    setCount(count + 1);
    
    // Use:
    setCount((prevCount) => prevCount + 1);
    ```
    
5. For complex state interactions, consider using a state machine pattern or libraries like XState
    
6. Add comments on useEffect hooks explaining their purpose and expected behavior
    

The key to fixing this error is understanding the flow of state updates in your app and breaking any circular dependencies.
