# Fix: Maximum update depth exceeded

A component sets state after every commit, so each update causes another one. React stops after 50 nested updates and throws. The usual cause is a `setState` in `componentDidUpdate` or `useLayoutEffect` with no condition, or an effect whose dependency is a new object on every render. The fix is to update only when a value really changed, or to stop storing a value you can compute.

| Fact | Detail |
|---|---|
| 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.` |
| In production builds | `Minified React error #185` |
| Development warning | `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.` |
| Where you see it | Browser console, the Next.js error overlay |
| Usual cause | State set in `componentDidUpdate`, `useLayoutEffect` or through a store selector on every update |
| Usual fix | Compare before setting state, keep dependencies stable, or compute the value during render |
| BugTape catches it | Yes, automatically, for the thrown error (#185) when no error boundary of yours catches it. The `useEffect` warning only exists in development |

## What the error means

React counts synchronous updates that are scheduled while it commits the previous one. In the React source the limit is 50 (`NESTED_UPDATE_LIMIT`). When a component keeps scheduling an update from `componentDidUpdate`, `useLayoutEffect` or a store subscription, the count passes the limit and React throws error #185.

There are two versions of the message. The thrown error names `componentWillUpdate` and `componentDidUpdate`, but it also covers `useLayoutEffect` and other synchronous updates. The `useEffect` version is a separate check that React runs only in development and prints with `console.error`. React does not throw in that case. A production build keeps re-rendering, which shows up as a slow tab, a hot CPU or the same request sent again and again.

This is a different error from `Too many re-renders` (#301), which means state was set during render itself.

## Common causes

1. **An effect dependency that changes on every render.** An object, array or function created in the component body is listed in the dependency array, and the effect sets state.
2. **An effect or layout effect with no dependency array that sets state.** It runs after every render, so every render sets state again.
3. **`setState` in `componentDidUpdate` without a comparison.** Class components update, then update again.
4. **A store selector that returns a new object.** Zustand v5 and other stores built on `useSyncExternalStore` compare results with `Object.is`. A selector such as `(s) => ({ a: s.a, b: s.b })` returns a new object each time. React prints `The result of getSnapshot should be cached to avoid an infinite loop` in development before the error.

## How to fix it

### Make effect dependencies stable

Move constant objects out of the component, or depend on the primitive values inside them.

```tsx
// Before: options is a new object each render, so the effect runs each render
function Results({ query }: { query: string }) {
  const [rows, setRows] = useState<Row[]>([]);
  const options = { query, limit: 20 };
  useEffect(() => {
    search(options).then(setRows);
  }, [options]);
  // ...
}

// After: depend on the value that changes
function Results({ query }: { query: string }) {
  const [rows, setRows] = useState<Row[]>([]);
  useEffect(() => {
    search({ query, limit: 20 }).then(setRows);
  }, [query]);
  // ...
}
```

If the dependency must be an object or function, create it with `useMemo` or `useCallback` so it keeps the same identity between renders. The `react-hooks/exhaustive-deps` lint rule points out dependencies that change on every render.

### Compute the value instead of syncing it with an effect

When an effect only copies props or state into other state, remove both the effect and the extra state.

```tsx
// Before
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${first} ${last}`);
});

// After
const fullName = `${first} ${last}`;
```

### Compare before setting state in componentDidUpdate

```tsx
componentDidUpdate(prevProps: Props) {
  // Before: this.setState({ value: this.props.value }) on every update
  if (prevProps.value !== this.props.value) {
    this.setState({ value: this.props.value });
  }
}
```

The same rule applies to `useLayoutEffect`: give it a dependency array, and set state only when the measured value differs from the current one.

```tsx
useLayoutEffect(() => {
  const next = ref.current?.offsetHeight ?? 0;
  setHeight((prev) => (prev === next ? prev : next));
}, [items]);
```

Returning the previous value lets React skip the update.

### Return stable values from store selectors

In Zustand v5, wrap a selector that builds an object with `useShallow`, or select each value on its own.

```tsx
import { useShallow } from 'zustand/react/shallow';

const { searchValue, setSearchValue } = useStore(
  useShallow((state) => ({
    searchValue: state.searchValue,
    setSearchValue: state.setSearchValue,
  })),
);

// Or
const searchValue = useStore((state) => state.searchValue);
```

For your own `useSyncExternalStore` call, `getSnapshot` must return the same value until the store changes.

## How to catch it in production with BugTape

In production React throws `Minified React error #185` with a link to the full text. Where the error goes depends on your error boundaries.

- **No error boundary of yours catches it.** React 19 passes it to `reportError()`, which fires a window `error` event, and React 18 rethrows it as an uncaught error. The BugTape browser SDK reports it automatically with the page URL, the stack and the steps the user took before it. In the Next.js App Router, an error that only the built-in boundary catches also goes to `reportError()`.
- **Your error boundary catches it**, including a Next.js `error.tsx`. React and Next.js then only call `console.error`. BugTape records console output into a report but does not start a report from it. Add one line to the boundary:

```tsx
'use client';
import { useEffect } from 'react';

export default function Error({ error, retry }: { error: unknown; retry: () => void }) {
  useEffect(() => {
    reportError(error); // fires the window error event BugTape listens for
  }, [error]);
  return <button onClick={() => retry()}>Try again</button>;
}
```

The `useEffect` version of the message is a development-only console warning, so there is nothing for BugTape to catch in production. If you suspect that loop in production, look for a page where the same request repeats. BugTape reports a failed request only when it follows a click within 8 seconds, so a background loop of successful requests does not start a report. Fix it in development, where React prints the warning and the component stack.

The SDK records nothing before `init()` runs. Add it once (see the [quickstart](/docs/quickstart/)):

```ts
import { init } from '@bugtape/sdk';

init({ apiKey: 'YOUR_PROJECT_KEY', release: 'YOUR_GIT_SHA' });
```

Repeats of #185 from the same place group into one issue with a count. Set `release` so that when you mark the issue fixed, a report from a newer release reopens it as a regression. Your coding agent can read the issue over MCP (`npx -y bugtape connect claude`, or `cursor` or `codex`) and propose the change. You review the pull request.

## FAQ

### What is React error 185?

It is the production code for `Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.` The production message links to `react.dev/errors/185`. Run the app in development to see the full message and which component is updating.

### Why do I get this error with useEffect if React says it only warns?

The thrown error covers synchronous updates. A `useEffect` loop alone gives the development warning. When the loop also involves a synchronous update, such as a `useLayoutEffect` or a store built on `useSyncExternalStore`, React throws #185. Look at every effect and subscription in the component stack the development message shows.

### How do I find which component causes it?

Run the app in development. The console error includes the component stack, and React DevTools can highlight components as they render. Temporarily logging each dependency of the suspect effect shows which one changes on every render.

### Is this the same as "Too many re-renders"?

No. "Too many re-renders" (#301) means a state setter ran in the component body during render. "Maximum update depth exceeded" means updates kept coming after each commit. The first is usually an `onClick={handler()}` mistake. The second is usually an effect, lifecycle method or subscription.

## Sources

- [React error codes](https://github.com/facebook/react/blob/main/scripts/error-codes/codes.json) (185), checked 2 Nov 2026.
- [React source, ReactFiberWorkLoop.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberWorkLoop.js) (`NESTED_UPDATE_LIMIT = 50`, the development-only `useEffect` warning), checked 2 Nov 2026.
- [React source, ReactFiberHooks.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js) (`getSnapshot` warning), checked 2 Nov 2026.
- [React docs: useEffect, "My Effect keeps re-running in an infinite cycle"](https://react.dev/reference/react/useEffect#my-effect-keeps-re-running-in-an-infinite-cycle), checked 2 Nov 2026.
- [Zustand docs: useShallow, "Maximum update depth exceeded"](https://github.com/pmndrs/zustand/blob/main/docs/reference/hooks/use-shallow.md), checked 2 Nov 2026.

## Related errors

- [React: Too many re-renders](/guides/react-too-many-re-renders/): `Too many re-renders. React limits the number of renders to prevent an infinite loop.`
- [React: Invalid hook call](/guides/react-invalid-hook-call/): `Invalid hook call. Hooks can only be called inside of the body of a function component.`
- [Next.js hydration mismatch](/guides/nextjs-hydration-mismatch/): `Hydration failed because the server rendered HTML didn't match the client`
- [Next.js Server Components render error](/guides/nextjs-server-components-render-error/): `An error occurred in the Server Components render. The specific message is omitted in production builds`
- [React: Objects are not valid as a React child](/guides/react-objects-not-valid-as-child/): `Objects are not valid as a React child`

All guides: [Fix an error](/guides/).

[Start free](/console/register?utm_source=bugtape_seo&utm_campaign=error-guides). Free plan · No credit card · Set up in minutes. Or [try the live demo](/demo/?from=landing&start=1#capture) first.
