Fix useEffect Infinite Loop: Warning Maximum Update Depth Exceeded in React

intermediateโš›๏ธ React2026-03-20| React 16.8+, any OS (Windows / macOS / Linux), Node.js 14+

Error Message

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect.
#react#useEffect#infinite-loop#dependency-array

The Error

Your browser tab slows to a crawl, then the console fills up with the same line repeating hundreds of times:

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.

React hit its internal re-render limit โ€” typically around 50 nested updates โ€” and threw the warning to stop a full browser crash. The component is stuck in an endless loop.

Why This Happens

The loop has a brutally simple structure:

  • Component renders
  • useEffect runs and calls setState
  • State change triggers a re-render
  • Re-render causes useEffect to run again
  • Back to step 2 โ€” forever

Three root causes cover 95% of cases in the wild:

  • Missing dependency array โ€” no array means the effect runs after every render, no exceptions
  • Object or array in the dependency array โ€” a new reference is created each render, so React's shallow comparison always sees a change
  • Unconditional state update inside the effect โ€” even a perfectly-formed dependency array won't save you if you always call setState

Fixes by Case

Case 1: Missing Dependency Array

No array at all is the most common beginner mistake. The effect treats every render as a trigger.

// โŒ Broken โ€” runs after every render
useEffect(() => {
  setCount(count + 1);
});

// โœ… Fixed โ€” runs only once on mount
useEffect(() => {
  setCount(1);
}, []);

That empty [] is not optional sugar โ€” it's a contract telling React "run this once and stop."

Case 2: Object or Array Dependency Recreated Each Render

JavaScript creates a brand-new object in memory every time a function runs. Two objects { page: 1 } and { page: 1 } are not equal by reference โ€” React sees them as different, so the effect re-fires.

// โŒ Broken โ€” options gets a new reference on every render
const options = { page: 1, limit: 20 };

useEffect(() => {
  fetchData(options);
  setData(result);
}, [options]); // always "changed"

Two clean solutions:

// โœ… Option A: move the object outside the component entirely
const OPTIONS = { page: 1, limit: 20 }; // created once, never changes

function MyComponent() {
  useEffect(() => {
    fetchData(OPTIONS);
  }, []);
}

// โœ… Option B: stabilize with useMemo
const options = useMemo(() => ({ page: 1, limit: 20 }), []);

useEffect(() => {
  fetchData(options);
}, [options]); // stable reference now

Case 3: Function Dependency Recreated Each Render

Functions have the same reference problem as objects. Define a function inside a component body and it's a new function on every render.

// โŒ Broken โ€” fetchUser is a new function every render
const fetchUser = () => {
  fetch('/api/user').then(r => r.json()).then(setUser);
};

useEffect(() => {
  fetchUser();
}, [fetchUser]); // always triggers

Wrap it in useCallback to get a stable reference:

// โœ… Fixed
const fetchUser = useCallback(() => {
  fetch('/api/user').then(r => r.json()).then(setUser);
}, []); // created once

useEffect(() => {
  fetchUser();
}, [fetchUser]); // now stable

Case 4: State Update Without a Guard Condition

Dependency arrays don't protect you from yourself. If you unconditionally set state that's also listed as a dependency, you've built a perfect loop.

// โŒ Broken โ€” items is a dependency AND gets set unconditionally
useEffect(() => {
  setItems([...items, newItem]); // sets items โ†’ re-render โ†’ effect runs again
}, [items]);

// โœ… Fixed โ€” use the functional updater form and depend only on newItem
useEffect(() => {
  if (newItem) {
    setItems(prev => [...prev, newItem]);
  }
}, [newItem]);

The functional form prev => [...prev, newItem] reads the current state internally โ€” no need to list items as a dependency at all.

Case 5: Data Fetching + State Update Loop

This one catches experienced developers too. Putting the state variable you're about to set into the dependency array is the trap:

// โŒ Broken โ€” user is a dependency, but setUser changes user on every fetch
useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(r => r.json())
    .then(data => {
      setUser(data);
      setLoading(false);
    });
}, [userId, user]); // user here is the problem

// โœ… Fixed โ€” fetch only when userId changes
useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(r => r.json())
    .then(data => {
      setUser(data);
      setLoading(false);
    });
}, [userId]);

Verifying the Fix

  • Open Chrome DevTools โ†’ Console tab
  • Hard-reload the page (Ctrl+Shift+R / Cmd+Shift+R)
  • Confirm the Maximum update depth exceeded warning is gone
  • Open React DevTools โ†’ Components tab โ€” the component should stop flashing with constant re-renders
  • Check the Network tab to confirm API calls are not firing in a loop (you should see one request, not 50+)

Quick Diagnostic Checklist

  • Does the useEffect have a dependency array at all?
  • Is any dependency an object or array defined inside the component body?
  • Is any dependency a function defined inside the component body?
  • Is the state variable being set also listed as a dependency?
  • Is the state update conditional or does it always fire?

The ESLint Rule That Catches This Early

eslint-plugin-react-hooks ships with Create React App and Next.js by default. If you're on a custom setup, add it manually:

npm install eslint-plugin-react-hooks --save-dev
// .eslintrc
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

The exhaustive-deps rule flags missing or incorrect dependencies at write-time, before anything reaches the browser. It's not perfect, but it catches the obvious cases โ€” including most of the patterns above.

Related Error Notes