Handbooks / React / Chapter 4

Effects and Data Fetching

7 pages · ~5 min✓ Reviewed

Builds on Hooks. Next up: Rendering and Performance.

ReactPage 1

What useEffect Is and the Mental Model

1. What it is and when it runs

  • useEffect(setup, deps?) synchronizes a component with an external system
  • External = network, DOM API, timer, subscription
  • Not a lifecycle hook
  1. Renderpure calc
  2. CommitDOM updated
  3. Paintbrowser shows
  4. Effectsetup runs
useEffectuseLayoutEffect
Runsafter paintbefore paint
Blocks paintnoyes
Use forfetch, subscribemeasure layout

2. Mental model

  • Think synchronize, not 'on mount'
  • Each render has its own effect closure
  • It sees that render's props and state
Caused byPut it in
Component being displayed (fetch on show, subscribe, listener)Effect
User action (click, submit)Event handler
Remember

Shown on screen => effect. User did something => handler.

3. Interview Point

  • Why after paint? So effects never block rendering
  • Layout vs effect? useLayoutEffect runs pre-paint, useEffect after
  • componentDidMount? No: it re-syncs whenever deps change
ReactPage 2

Dependency Arrays

1. Three Forms

DepsRuns
noneafter every render
[]once after mount
[a, b]mount, then when a or b changes
  • Each dep compared with Object.is
  • List every reactive value the effect reads: props, state, derived values
  • Never silence react-hooks/exhaustive-deps

2. Unstable Deps

  • Objects, arrays, functions made in the body are new every render
  • As deps they re-fire the effect each time
  • Fix: move inside the effect, outside the component, or memoize

3. Drop a Dep

  • Functional updater c => c + 1 removes state from deps
  • Must not re-run? Read the latest value through a ref
javascript
useEffect(() => {
  const id = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(id);
}, []);

4. Interview Point

  • Omit a dep? Stale closure: the effect keeps an old value
  • Object in deps loops? New identity each render, so effect sets state and repeats
  • How compared? Object.is on each item
ReactPage 3

Cleanup Functions and Strict Mode

1. Cleanup Basics

  • Return a function from the effect to undo its setup
  • removeEventListener, clearInterval, unsubscribe, abort() a request
javascript
useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
}, []);

setup starts the timer, cleanup stops it

2. Run Order

  1. Deps change
  2. Old cleanupold values
  3. New effectnew values
  • Cleanup also runs on unmount

3. Strict Mode (dev only)

  • Dev runs setup, cleanup, setup to expose missing cleanup
  • Result differs from one run (two timers, two connections)? Cleanup is missing or wrong
  • Fix the cleanup, not the double run
ReactPage 4

Fetching Data: Race Conditions, Loading and Error States

1. The Race

  • Fast id change: older, slower response lands last
  • It overwrites the newer result

2. Two Fixes

  • Ignore flag: cleanup sets ignore = true, .then skips setData
  • AbortController: signal to fetch, abort() in cleanup
javascript
useEffect(() => {
  const c = new AbortController(); setLoading(true); setError(null);
  fetch(url, { signal: c.signal })
    .then(r => { if (!r.ok) throw new Error(r.status); return r.json(); })
    .then(setData)
    .catch(e => { if (e.name !== 'AbortError') setError(e); })
    .finally(() => { if (!c.signal.aborted) setLoading(false); });
  return () => c.abort(); }, [url]);

3. Request State

  • Start: loading true, error null
  • fetch resolves on 404/500: check res.ok
  • finally: skip setLoading(false) if aborted

4. Interview Point

  • Stale overwrite? Ignore flag or abort in cleanup
  • fetch rejects on 500? No, only on network failure
  • Why TanStack Query or SWR? Caching, dedupe, races handled
ReactPage 5

Avoiding Unnecessary Effects and Common Mistakes

1. Skip the Effect

SituationInstead of an effect
Derived dataCompute in render; useMemo if expensive
Click, submit, POSTDo it in the event handler
Reset state on prop changeGive the component a key
  • Effect + setState for derived data costs an extra render
jsx
const full = first + ' ' + last;
<Profile key={userId} id={userId} />

new key = fresh state

2. Common Mistakes

  • useEffect(async () => ...) returns a Promise, not a cleanup
  • Missing cleanup: listeners, timers, requests leak
  • Effect sets state it also depends on: infinite loop
jsx
useEffect(() => {
  async function load() { /* await ... */ }
  load();
}, []);

inner async function, then call it

3. Interview Point

  • Three cases with no effect? Derived data, user actions, prop-driven resets
  • Why not async? It returns a Promise; React expects a cleanup function
  • Reset on prop change? Give the component a key
ReactPage 6

useEffect Cheat Sheet

1. Deps and cleanup

DepsRuns
noneevery render
[]mount only
[x]when x changes
  • List every reactive value; trust the lint rule
  • Cleanup runs before the next effect and on unmount
  • Strict Mode dev double-run: result differs from one run = bad cleanup

2. Fetch recipe

javascript
useEffect(() => { const c = new AbortController();
setLoading(true); setError(null);
fetch(url, { signal: c.signal })
  .then(r => r.ok ? r.json() : Promise.reject(r.status))
  .then(setData, e => e.name !== 'AbortError' && setError(e))
  .finally(() => { if (!c.signal.aborted) setLoading(false); });
return () => c.abort();
}, [url]);
  • Abort in cleanup (or an ignore flag)
  • Check res.ok: fetch won't reject on 404 or 500
  • Loading false in finally unless aborted
  • State: loading, error, data

3. Skip the effect

NeedUse
Derived valuecompute in render
User actionevent handler
Reset on propkey
  • Safe fetch on prop change? Deps [id], abort in cleanup, guard finally.
  • Wrong tool when? Derived values, user actions, prop resets.
ReactPage 7

Check yourself

In development an effect logs connected twice in a row, with no disconnected between them. What is wrong?
  • The cleanup is missing or does not disconnect: the second connection leaks.
  • Strict Mode runs setup, cleanup, setup, so a correct effect logs connected, disconnected, connected.
  • Fix the cleanup: return () => conn.disconnect().
useEffect(() => { fetch(url + id).then(r => r.json()).then(setData) }, [id]). After id changes 1 to 2 quickly, the page shows user 1. Why?
  • The slower response for 1 arrived last and overwrote the result for 2.
  • Fix: pass an AbortController signal and abort in cleanup, or use an ignore flag.
Why does useEffect(async () => { ... }, []) break cleanup?
  • An async function returns a Promise, which React cannot use as a cleanup function.
  • Define an inner async function inside the effect and call it.