Effects and Data Fetching
7 pages · ~5 min✓ Reviewed
Builds on Hooks. Next up: Rendering and Performance.
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
- Renderpure calc
- CommitDOM updated
- Paintbrowser shows
- Effectsetup runs
| useEffect | useLayoutEffect | |
|---|---|---|
| Runs | after paint | before paint |
| Blocks paint | no | yes |
| Use for | fetch, subscribe | measure 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 by | Put 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?
useLayoutEffectruns pre-paint,useEffectafter - componentDidMount? No: it re-syncs whenever deps change
Dependency Arrays
1. Three Forms
| Deps | Runs |
|---|---|
| none | after 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 + 1removes 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.ison each item
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
- Deps change
- Old cleanupold values
- 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
Fetching Data: Race Conditions, Loading and Error States
1. The Race
- Fast
idchange: older, slower response lands last - It overwrites the newer result
2. Two Fixes
- Ignore flag: cleanup sets
ignore = true,.thenskipssetData - AbortController:
signalto 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:
loadingtrue,errornull - fetch resolves on 404/500: check
res.ok finally: skipsetLoading(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
Avoiding Unnecessary Effects and Common Mistakes
1. Skip the Effect
| Situation | Instead of an effect |
|---|---|
| Derived data | Compute in render; useMemo if expensive |
| Click, submit, POST | Do it in the event handler |
| Reset state on prop change | Give the component a key |
- Effect +
setStatefor 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
useEffect Cheat Sheet
1. Deps and cleanup
| Deps | Runs |
|---|---|
| none | every 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
ignoreflag) - Check res.ok: fetch won't reject on 404 or 500
- Loading false in
finallyunless aborted - State: loading, error, data
3. Skip the effect
| Need | Use |
|---|---|
| Derived value | compute in render |
| User action | event handler |
| Reset on prop | key |
- Safe fetch on prop change? Deps
[id], abort in cleanup, guardfinally. - Wrong tool when? Derived values, user actions, prop resets.
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
ignoreflag.
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.