Made by Dhawal Made by Dhawal

useEffect infinite loop? Fix “Too many re-renders” in React (The Ouroboros Render)

Kai's search page fetches forever after one visit. Chitragupta, keeper of Domlok's ledger, reads the loop: setState during render, a missing dependency array, and an object that is new on every render, and how to fix each one.

Told by Raiden, with Kai asking the questions

The Ouroboros, a jade serpent coiled in a ring and biting its own tail, beside the title "useEffect infinite loop?"

Chitragupta: I write one line in my ledger for every render, Kai. Your search page has filled forty pages since breakfast.

I only opened the page once!

If your useEffect runs in an infinite loop, or React throws Too many re-renders, something is setting state in a way that causes another render, which sets state again. There are three usual causes: setting state while rendering, an effect with no dependency array, and a dependency that is a new object or function on every render. Each has a short fix.

In the world of Domlok this is the work of the Ouroboros, a serpent that swallows its own tail. Render feeds the effect, the effect feeds state, state feeds the next render. It never stops on its own, and it often doesn’t throw an error either. It just fills your Network tab and burns your users’ batteries.

Your guide for this episode is Chitragupta, the Water guide of Domlok and keeper of every record. State, data and storage are his domain, and an infinite loop is a ledger that never stops growing, so he is the one to read it. Kai, as usual, wrote the loop: a search page that fetches forever after one visit.

How a render loop works

React renders your component, updates the DOM, then runs your effects. If an effect sets state to a new value, React renders again. Normally that second render is the end of it: the effect’s dependencies didn’t change, so React skips the effect and the loop stops after one extra line in the ledger.

The Ouroboros appears when that check never says “unchanged”. Then it is render, effect, set state, render, forever.

KaiSo which part of my code is the tail?

Cause 1: setting state while rendering

The fastest loop doesn’t even need an effect. Look at the brackets on this button:

Drawer.jsx
function Drawer() {
const [open, setOpen] = useState(false);
return <button onClick={setOpen(true)}> Open </button>;
}

onClick={setOpen(true)} calls setOpen right now, while React is rendering, and passes its return value as the handler. Setting state during render asks for another render, which sets state again. React stops it with Too many re-renders. React limits the number of renders to prevent an infinite loop. Pass a function instead:

Drawer.jsx
<button onClick={() => setOpen(true)}> Open </button>

The same error appears when the component body copies props into state unconditionally, like setCount(items.length) at the top level. You don’t need state for that: calculate it during render with const count = items.length;.

ChitraguptaIf you can calculate it from props or state, calculate it. Never copy it into state from the body.

Cause 2: an effect with no dependency array

Kai’s search page doesn’t crash. It just keeps fetching:

ScrollSearch.jsx
function ScrollSearch({ tag }) {
const [results, setResults] = useState([]);
useEffect(() => {
search(tag).then(setResults);
}); // no dependency array
return <ResultList items={results} />;
}

An effect with no array runs after every render. It fetches, calls setResults with a brand-new array from the server, and that new array causes the next render. React only skips a re-render when the new state is the same value (compared with Object.is), and a fresh array never is. Because the update happens after a network request, React doesn’t warn you. The loop simply runs as fast as your API answers.

Add the array, with the values the effect reads:

ScrollSearch.jsx
useEffect(() => {
search(tag).then(setResults);
}, [tag]); // runs on mount, then only when tag changes

Cause 3: a new object every render

KaiMy real page has an array, though. I put my filters object in it, and it still loops.

ScrollSearch.jsx
function ScrollSearch({ tag }) {
const [results, setResults] = useState([]);
const filters = { tag, sort: "new" };
useEffect(() => {
search(filters).then(setResults);
}, [filters]); // ...
}

filters is created in the component body, so every render makes a new object, even when tag and sort haven’t changed. React compares each dependency with its previous value using Object.is, and two different objects are never equal. The effect sees a “change” on every render, fetches, sets new results, renders, and makes yet another filters.

Comparison Result Why
Object.is('fire', 'fire') true strings compare by value
Object.is(3, 3) true numbers compare by value
Object.is({ tag }, { tag }) false two different objects
Object.is([], []) false two different arrays
Object.is(fn, fn2) false a new function each render

Functions behave the same way. A const load = () => search(tag) written in the body is a new function on every render, so listing load as a dependency loops just like an object.

ChitraguptaYour array promised the effect something new on every render, Kai. Give it values that change only when they mean to.

The fix: steady dependencies

First choice: build it inside the effect

ScrollSearch.jsx
useEffect(() => {
const filters = { tag, sort: "new" };
search(filters).then(setResults);
}, [tag]);

Now the effect depends on tag, a string, and a string is always equal to itself. One request per tag.

If it never changes: move it outside the component

ScrollSearch.jsx
const SORT = { sort: "new" }; // created once, not per render
function ScrollSearch({ tag }) {
const [results, setResults] = useState([]);
useEffect(() => {
search({ ...SORT, tag }).then(setResults);
}, [tag]);
}

Last resort: useMemo and useCallback

Sometimes the object or function has to come from render, for example when you pass it to a child that uses it in its own effect. Then keep its identity stable: useMemo returns the same object and useCallback the same function until their own dependencies change.

ScrollSearch.jsx
const filters = useMemo(() => ({ tag, sort: "new" }), [tag]);
const load = useCallback(() => search({ tag }), [tag]);

React’s docs treat useMemo as a performance hint, so moving the value into the effect is simpler and safer when you can. In a real app, also ignore stale responses in the effect’s cleanup (see Episode 16), or use a data library that caches by query.

KaiOne request. The Network tab is quiet!

Catch the serpent in DevTools

  1. Network tab. Open DevTools → Network, filter by Fetch/XHR, and load the page. A request that keeps repeating while nobody clicks anything is the first sign.
  2. React DevTools. In the Components tab settings, turn on Highlight updates when components render. A component that flashes nonstop is looping. The Profiler shows commit after commit with nothing clicked.
  3. The console. When an effect sets state synchronously in a loop, React warns: 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. The message names both causes.
  4. The linter. Keep react-hooks/exhaustive-deps on. It warns when an object or function declared in the body makes an effect’s dependencies change on every render, and suggests moving it inside the effect or wrapping it in useMemo.

Before you merge an effect

  • Is every setState inside an event handler or an effect, never in the component body?
  • Does every effect have a dependency array?
  • Is every dependency a primitive, or an object or function that stays the same between renders?
  • After loading the page and waiting, does the Network tab show one request, not a stream?
Acharya's Rule #22

Never set state while rendering. Give every effect dependencies that change only when they should.

Next episode: The Nameless Field. Vishwa builds form fields that work: <label for>, the right input type and inputmode, autocomplete tokens and native validation messages.

Think you can uncoil the serpent yourself? Play Break the Ouroboros on domlok.in and earn seal 22.

Watch the episode on YouTube at @dom-lok and save the carousel on Instagram at @mbd.domlok.

Further reading

Frequently asked questions

Why is my useEffect running in an infinite loop?+
The effect sets state, the new state causes a render, and the effect runs again after that render. This happens when the effect has no dependency array, or when one of its dependencies is a new object, array or function on every render.
How do I fix "Too many re-renders" in React?+
Find the state update that runs while the component is rendering. The most common cause is writing onClick={setOpen(true)}, which calls the setter immediately; pass a function instead, onClick={() => setOpen(true)}. If you copy props into state in the component body, calculate the value during render instead.
Why does an object in the useEffect dependency array cause an infinite loop?+
React compares each dependency with its previous value using Object.is, and an object created during render is a different object every time. The effect sees a change on every render. Build the object inside the effect and depend on its primitive values, such as a string or a number.
Should I use useMemo or useCallback to stop an infinite loop?+
They keep the same object or function between renders until their own dependencies change, so they can stop the loop. React recommends first moving the object or function inside the effect, or outside the component if it uses no props or state. Use useMemo and useCallback when the value has to come from render.
What does "Maximum update depth exceeded" mean in React?+
React stopped a component that kept updating state in a loop. The warning says it usually happens when a component calls setState inside useEffect and the effect either has no dependency array or has a dependency that changes on every render.