Skip to the chapter
←ReactGGroundwork⚛React
React/Beginner/B9
9 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛B9 · Part 1 · Beginner

useEffect basics

For synchronising with something outside React — and most of the time you do not need it.

4 min read9 sections5 exercises
9 of 57

In this chapter

  • What an effect is for
  • The dependency array
  • Cleanup
  • Fetching, done properly
  • When exactly it runs

and 4 more

about 4 min
09On this page
  1. What an effect is for
  2. The dependency array
  3. Cleanup
  4. Fetching, done properly
  5. When exactly it runs
  6. How dependencies are compared
  7. Most effects should not exist
  8. The two loops
  9. Do not lie to the linter

What an effect is for

Rendering is pure: it computes JSX and touches nothing else. Effects are the escape hatch for the things rendering must not do — talking to a subscription, a timer, the document title, a browser API. React runs them after the DOM has been updated.

useEffect(() => {
  document.title = title;
}, [title]);

The dependency array

Written asRuns
useEffect(fn)After every render
useEffect(fn, [])Once, after the first render
useEffect(fn, [a, b])After the first render, then whenever a or b changes

React compares each dependency with Object.is against the previous render's value. That is a reference comparison, which is where most of the surprises come from.

Cleanup

Return a function and React calls it before the next run of the effect, and once more when the component unmounts. Anything you start, you clean up.

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

useEffect(() => {
  window.addEventListener("resize", onResize);
  return () => window.removeEventListener("resize", onResize);
}, []);

Without the cleanup, every remount adds another interval or another listener. They accumulate, they all keep firing, and they hold whatever they closed over in memory.

Why your effect runs twice in development

In development, Strict Mode mounts, unmounts and remounts every component once on purpose. It is not a bug and it does not happen in production — it exists to reveal effects with missing cleanup. If running twice breaks something, that thing would also break when a user navigated away and back.

Fetching, done properly

useEffect(() => {
  let cancelled = false;

  setLoading(true);
  fetch("/api/users/" + userId)
    .then((r) => r.json())
    .then((data) => {
      if (!cancelled) setUser(data);      // ignore a stale response
    })
    .catch((err) => {
      if (!cancelled) setError(err);
    })
    .finally(() => {
      if (!cancelled) setLoading(false);
    });

  return () => {
    cancelled = true;                      // this is the race fix
  };
}, [userId]);

The cancelled flag is the whole point. Change userId from 1 to 2 quickly and both requests are in flight; if request 1 is slower, it resolves last and overwrites user 2 with user 1. The screen shows the wrong person and nothing looks broken. Every fetching effect without that flag has this bug, and it only shows up on slow connections.

AbortController does the same job and also cancels the request itself — worth using once you are comfortable. In real projects, a data library handles all of this; data fetching patterns covers when to stop hand-rolling it.

When exactly it runs

  1. You set state, React re-renders the component.
  2. React applies the changes to the DOM.
  3. The browser paints.
  4. Then effects run — children before parents.

Because it runs after paint, an effect that measures the DOM and then changes layout produces a visible flicker: the user sees the first version. That is what useLayoutEffect is for — identical API, but it runs before paint, so the browser never shows the intermediate state. It blocks painting, so use it only for measuring and adjusting, never for fetching.

How dependencies are compared

useEffect(fn, [user.id]);        // a string — compares by value ✓
useEffect(fn, [user]);           // an object — compares by reference
useEffect(fn, [{ id }]);         // ✗ new object every render → runs every render
useEffect(fn, [items.length]);   // ✓ a number

React uses Object.is on each entry. Primitives compare by value, everything else by identity. If a dependency is an object or a function created during render, it is new every time — which is the single most common cause of an effect that will not stop running.

The array length must also be constant between renders. React compares position by position, so a conditionally built dependency array is the same class of bug as a conditional hook.

Most effects should not exist

This is the part that separates people who use the hook from people who understand it. Before writing one, check whether it belongs to one of these categories — none of which needs an effect.

Deriving a value

// ✗ an extra render, and it can go stale
const [full, setFull] = useState("");
useEffect(() => { setFull(first + " " + last); }, [first, last]);

// ✓ just compute it
const full = first + " " + last;

Responding to a user action

// ✗ now every path that changes items sends analytics
useEffect(() => { if (items.length) track("added"); }, [items]);

// ✓ it happened because of a click, so put it in the click
function handleAdd(item) {
  setItems([...items, item]);
  track("added");
}

The test is simple: did this happen because the user did something, or because the component appeared on screen? The first belongs in a handler. Only the second is an effect.

Resetting state when a prop changes

// ✗ renders once with the old value first
useEffect(() => { setDraft(""); }, [userId]);

// ✓ let React throw the component away
<Editor key={userId} userId={userId} />

The two loops

useEffect(() => { setCount(count + 1); });          // ✗ no array — infinite

useEffect(() => {
  setUser({ ...user, seen: true });                 // ✗ new object each time
}, [user]);

The second is the one that catches people out. Objects, arrays and functions created during render are new references every render, so an effect that depends on one runs every render — and if it sets state, forever. Depend on a primitive instead ([user.id]), or create the value outside the component.

Do not lie to the linter

The exhaustive-deps rule tells you which values your effect reads. Removing one to stop the effect re-running does not fix the loop; it freezes the effect on an old value and turns an obvious bug into a subtle one. If satisfying the linter breaks the effect, the effect is modelling the wrong thing.

Interview answer, one sentence

"An effect synchronises the component with something outside React, and it runs after paint with a cleanup that runs before the next one. The interesting part is what is not an effect — derived values, anything caused by a user action, and resetting state on a prop change, which is a key."

05Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsDid the dependency array change?beginner
  • 9 testsWhich dependency changed?beginner
  • 7 testsSubscribe to the window, and unsubscribeintermediate
  • 8 testsKeep the tab title in syncintermediate
  • 5 testsA side effect that cleans up after itselfbeginner
All 46 for this level →
B9

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. B8Previous · B8Controlled forms
  2. B9you are here
  3. Next · B10Lifting state up & basic stylingWhen two components need the same value, it belongs to the one above both of them.5 min read

01On this page

9 sections · 0%

  1. 01What an effect is for
  2. 02The dependency array
  3. 03Cleanup
  4. 04Fetching, done properly
  5. 05When exactly it runs
  6. 06How dependencies are compared
  7. 07Most effects should not exist
  8. 08The two loops
  9. 09Do not lie to the linter

[] chapters · t top