Infinite Loops in useEffect: Causes and Fixes

An infinite loop in useEffect happens when an Effect updates state that is also a dependency. Learn the causes and how to break the cycle.

6 min read

Infinite loops in useEffect happen when an Effect updates state that is also listed as a dependency. The update triggers a render, the render changes the dependency, and React runs the Effect again. Breaking the cycle means changing either the update or the dependency.

Why the loop starts

An Effect that reads a reactive value must list it as a dependency. When the same Effect also updates that value, every run produces a new value. That new value schedules another render, which schedules another run.

The dependency array is what closes the circle, because it names the exact value the Effect keeps changing.

The loop is a logical circle: render, run the Effect, set state, render again, and repeat. React keeps going until the browser freezes or the page throws a maximum update depth error.

A loop that appears only after a state update is the tell. The console message and the frozen tab are symptoms, but the dependency list is where the cause lives.

A self-sustaining update

This counter reads its own state inside the Effect and updates it on every run.

App.jsxApp.jsx
import { useEffect, useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(count + 1);
  }, [count]);
  return <p>{count}</p>;
}

The component never settles. Each render reads count, the Effect adds one, and the new count forces the next render. The console eventually reports that the maximum update depth was exceeded.

The count climbs without a click, a timer, or any user action. That self-driving behavior is the signature of a loop, not of a feature.

Remove the state you do not need

Many loops come from storing a value that can be computed instead. If the number is derived from other state, drop the state and the Effect together. The fastest question to ask is whether the value needs to exist as state at all.

App.jsxApp.jsx
import { useState } from "react";
 
function NameForm() {
  const [first, setFirst] = useState("");
  const [last, setLast] = useState("");
  const fullName = `${first} ${last}`;
  return <p>{fullName}</p>;
}

fullName is calculated during render, so it always matches first and last. There is no second render and no loop, because nothing updates state after the first render.

Whenever two pieces of state depend on each other, the fix is usually to compute one from the other during render. The component gets shorter and the render count drops to one.

Use an updater when the state is real

Sometimes the state is genuine, such as a ticking timer. The loop appears because the Effect reads the same value it updates. Pass an updater function instead, and the value no longer needs to be a dependency.

The updater receives the latest value from React, so the Effect itself never reads count.

App.jsxApp.jsx
import { useEffect, useState } from "react";
 
function Timer() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setCount((c) => c + 1), 1000);
    return () => clearInterval(id);
  }, []);
  return <p>{count}</p>;
}

The Effect no longer reads count, so the dependency array is empty and the interval is created once. React hands the updater the latest value on each tick, and the empty array means React never re-runs this Effect after mount. The interval runs once and cleans itself up on unmount.

Updating state based on the previous state explains that pattern in depth.

Check the dependency array

If the state update is correct but the loop continues, the culprit is usually a dependency that changes on every render. Objects and functions created in the component body are new values every time. How the dependency array works shows how React compares those values with Object.is.

When an Effect only adjusts state based on other state, you might not need an Effect at all. Treat the dependency lint warning as a signal, not an inconvenience; it usually points at the value that keeps changing. The fastest fix is usually the smallest one: compute during render first, pass an updater next, and inspect the dependency array last.

What to learn next

Async work follows the same cleanup shape, and the next article in this section covers it in detail.

Rune AI

Rune AI

Key Insights

  • A loop starts when an Effect updates state that is also a dependency.
  • Compute derived values during render instead of syncing them with an Effect.
  • Pass an updater function so the state no longer needs to be a dependency.
  • Check for object or function dependencies that change on every render.
RunePowered by Rune AI

Frequently Asked Questions

What causes an infinite loop in useEffect?

The Effect updates a state value that is also one of its dependencies. The update triggers a render, which changes the dependency, which runs the Effect again.

How do I stop an Effect from looping?

Compute derived values during render, pass an updater function so the state is no longer a dependency, or move the update into an event handler.

What does maximum update depth exceeded mean?

It is the error React throws when a component keeps scheduling renders in a cycle. It usually points at a state update that runs during render or in an unguarded Effect.

Conclusion

An infinite Effect loop is always a dependency problem in disguise. Compute during render when you can, pass an updater when state is real, and check the dependency array last.