Home

Fix this error · React and Next.js

Fix: Maximum update depth exceeded

Maximum update depth exceeded (#185) comes from setState loops in effects, componentDidUpdate or store selectors. Causes, fixes and production capture.

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.

FactDetail
ErrorMaximum 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 buildsMinified React error #185
Development warningMaximum 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 itBrowser console, the Next.js error overlay
Usual causeState set in componentDidUpdate, useLayoutEffect or through a store selector on every update
Usual fixCompare before setting state, keep dependencies stable, or compute the value during render
BugTape catches itYes, 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.

// 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.

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

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

Compare before setting state in componentDidUpdate

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.

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.

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:
'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):

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

All guides: Fix an error.

Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.