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.
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.
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.
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
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.
Frequently Asked Questions
What causes an infinite loop in useEffect?
How do I stop an Effect from looping?
What does maximum update depth exceeded mean?
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.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.