ToolPopToolPop
React · Lesson 4 of 8

useEffect, what runs when, and the infinite-loop bug everyone hits

12 min readUpdated 24 Jun 2026

useEffect is React's "do something after rendering" escape hatch. Talk to the network, set a timer, sync with a non-React library. Anything that touches the world outside your component.

It is also where 80% of React bugs live. Let us defuse it.

Diagram
rendering diagram...
useEffect timing: the dep array decides when fn runs

When effects run

The signature is useEffect(fn, deps). React calls fn after the component renders to the DOM, based on deps.

  • No deps array: useEffect(fn). Runs after every render. Almost always wrong.
  • Empty deps array: useEffect(fn, []). Runs once after the first render. Good for one-time setup.
  • Filled deps array: useEffect(fn, [userId]). Runs after the first render, and again whenever userId changes.
jsx
useEffect(() => {
  document.title = `Cart (${count})`;
}, [count]);

Cleanup, the part everyone forgets

Return a function from your effect. React calls it before the next effect run, and when the component unmounts.

jsx
useEffect(() => {
  const id = setInterval(() => tick(), 1000);
  return () => clearInterval(id);
}, []);

Timers, subscriptions, event listeners, websockets; if you set it up, tear it down. The first Swiggy-style live-order page I built leaked three intervals per second. The browser tab ate 1.2 GB before I figured it out.

The infinite-loop classic

jsx
const [data, setData] = useState(null);
useEffect(() => {
  fetch('/api/restaurants')
    .then(r => r.json())
    .then(setData);
}); // no deps, runs every render, setData re-renders, runs again

Add [] and it runs once. Add [userId] and it refetches when the user changes. The dep array is not optional decoration; it is the contract.

If your linter (react-hooks/exhaustive-deps) yells about a missing dependency, listen. It is almost never wrong.

When NOT to use useEffect

This is the lesson nobody tells you in 2026.

  • Derivable from props or state? Just calculate it inline. No effect needed.
  • Reacting to a user event? Put the logic in the event handler, not in an effect.
  • Transforming data for render? Do it during render.
jsx
// Wrong
useEffect(() => {
  setFullName(first + ' ' + last);
}, [first, last]);
 
// Right
const fullName = first + ' ' + last;

Modern React thinking: useEffect is for syncing with external systems. Network, browser APIs, third-party widgets. Not for "running code when something changes". The new docs literally have a page titled "You Might Not Need an Effect", and it is correct.

Quick checklist

  • One concern per effect. If you are subscribing AND fetching AND setting a title, that is three effects.
  • Always return cleanup for anything subscription-like.
  • In dev, effects run twice on mount (Strict Mode). That is on purpose, to surface missing cleanup. Do not "fix" it by removing Strict Mode.

Next lesson: lists, keys, and forms; the three things React makes weirdly fiddly.

Free tools you can use while you learn

Common questions

Q.When does useEffect run?
A.After every render where its dependencies changed. An empty dependency array means once after the first render. No dependency array means after every render (almost always a bug).
Q.How do I avoid an infinite loop in useEffect?
A.Make sure setState calls inside useEffect change state that is NOT in the dependency array. The classic trap is setting state on every render with state itself listed as a dependency. Read the deps carefully.
Chai0/1 done

Watching quietly. Tap me if you want a tip.

React Playground
preview

Try this (0 of 1 done)

  1. 1

    Add a useEffect with empty deps that logs "mounted" once.

    show answer
    function App() {
      React.useEffect(() => { console.log('mounted'); }, []);
      return <p>see console</p>;
    }