React Hooks + Async Correctness Flashcards

detect deps/stale-closure/race/cleanup bugs fast; Output: 1 verdict sentence + 2-3 issues + fix approach. (80 cards)

1
Q

Deck 2 - React Hooks: Async Correctness (objective)

A

Goal: spot deps/stale-closure/race/cleanup issues fast.
What you practice: 1 verdict sentence + 2-3 major issues + fix approach.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

Define dependency array (deps).

A
  • Controls when an effect re-runs.
  • Missing deps => stale closure.
  • Too many deps => extra work or loops.
  • Rule: include every render-scope value you read (or stabilize intentionally).
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

Define stale closure.

A
  • A callback/effect reads values captured from an earlier render.
  • Usually from missing deps or incorrect memoization.
  • Fix: correct deps, functional updates, or a ref for latest value.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Define race condition in async effects.

A
  • Overlapping requests resolve out of order.
  • Older response can overwrite newer state.
  • Fix: AbortController or request-id guard (ignore stale results).
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

Define cleanup function (effect).

A
  • Runs on unmount and before next effect run.
  • Use it to abort fetches, clear timers, unsubscribe listeners.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

Why should useEffect callback not be async?

A
  • Effect callback must return void or cleanup, not a Promise.
  • Pattern: define an inner async function and call it.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Define setState-after-unmount.

A
  • Async work completes after unmount and calls setState.
  • Causes warnings/leaks.
  • Fix: cleanup/cancel or ignore stale results.
  • “setState-after-unmount” is when a component tries to update its state after it has already been removed from the UI (unmounted). It usually happens when an async task (fetch, timer, subscription) finishes later and calls a state setter (setState) on a component that no longer exists.
  • causes reliability bugs, memory leak patterns, and is a symptom of missing cleanup/cancellation.
  • Common Causes: fetch() resolves after navigation away. setTimeout fires after unmount. Event/subscription continues after unmount.

Example code (Bad -> fixed):

function Profile({ id }: { id: string }) {
  const [user, setUser] = React.useState<{ name: string } | null>(null);

  React.useEffect(() => {
    fetch(`/api/user?id=${encodeURIComponent(id)}`)
      .then((r) => r.json())
      .then((data) => setUser(data)); // <- can run after unmount
  }, [id]);

  return <div>{user ? user.name : "Loading..."}</div>;
}

Fixed (cancel in-flight request on unmount / id change):

function Profile({ id }: { id: string }) {
  const [user, setUser] = React.useState<{ name: string } | null>(null);

  React.useEffect(() => {
    const controller = new AbortController();

    (async () => {
      const res = await fetch(`/api/user?id=${encodeURIComponent(id)}`, {
        signal: controller.signal,
      });
      if (!res.ok) return;
      const data = await res.json();
      setUser(data);
    })().catch((e) => {
      if (e?.name === "AbortError") return; // ignore cancellation
    });

    return () => controller.abort(); // <- prevents setState-after-unmount
  }, [id]);

  return <div>{user ? user.name : "Loading..."}</div>;
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

Define functional update.

A
  • Use setX(prev => next) when next depends on prev.
  • Prevents stale updates after awaits / batching.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

StrictMode note (dev).

A
  • In dev StrictMode, effects may run more than once.
  • Code must be idempotent with correct cleanup and no duplicated subscriptions.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

When should you reset state on input change?

A
  • When old data would mislead while new request is pending.
  • Typical: setData(null) and show Loading.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

Minimum safe fetch handling.

A
  • Encode params.
  • Check res.ok.
  • Parse JSON.
  • Validate shape.
  • Set data or error state.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

Treat res.json() as what type by default?

A
  • unknown.
  • Validate required fields before use to prevent runtime crashes.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

Define “Dependency Loop” in React Effects:

  • What is the technical cause, the immediate symptom, and the architectural fix?
A
  • Cause: useEffect updates a value that is also in its dependency array.
  • Symptom: Infinite re-render loop (App crash/freeze).
  • The Fixes:
    • Functional Updates: setCount(c => c + 1) (removes dependency).
    • Primitive Deps: Depend on id, not the object.
  • De-sync: Move logic to an Event Handler (e.g., onClick).

THE LONG (FULL) ANSWER

  • The Technical Cause: A useEffect performs an action that updates a dependency listed in its own dependency array.
    • Example: useEffect(() => { setCount(count + 1) }, [count])
  • The Symptom: An Infinite Re-render Cycle. The effect triggers a state update → React re-renders the component → The dependency changed → The effect runs again.
  • The “Evaluator” Insight (The Smells):
    • Direct Loops: Setting state that is directly in the array.
    • Indirect Loops: Effect updates A -> B is a memoized value based on A -> Effect depends on B.
    • Object/Function Identity: Effect depends on an object/function defined in the component body that isn’t memoized. Every render creates a “new” version, re-triggering the effect.
  • The Fixes:
    • Functional Updates: Use setCount(c => c + 1) and remove count from the dependency array.
    • Move Logic: Move the logic into an event handler (e.g., onClick) instead of a “syncing” effect.
    • Primitive Dependencies: Depend on id (string) instead of user (object).
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

Define referential instability.

A
  • Objects/functions created in render have new identity each render.
  • If used in deps, effect re-runs.
  • Fix: depend on primitives or memoize with correct deps.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

Define AbortController usage (review-level).

A
  • Create controller inside effect (per run).
  • Pass signal to fetch.
  • Abort in cleanup.
  • Ignore AbortError.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

Define request-id guard (review-level).

A
  • Increment token per request.
  • Only commit result if token matches latest.
  • Works for any async (not just fetch).
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
17
Q

Define debounce + cleanup.

A
  • Delay work to reduce request spam.
  • Must clearTimeout in cleanup.
  • Still needs cancellation/race protection for the request.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
18
Q

Define TOCTOU bug (async UI).

A
  • You validate inputs, start async work, then apply result later.
  • Inputs may have changed.
  • Fix: cancel/ignore stale work.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
19
Q

Evaluator phrasing pattern (1 sentence).

A

Name issue -> impact -> fix pattern.
Example: “FAIL - Missing cancellation allows out-of-order fetches to overwrite newer state (race condition); add AbortController or request-id guard.”

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
20
Q

Top 3 async-effect risks to look for first.

A

1) deps correctness
2) cancellation / ignore-stale
3) error handling (res.ok + catch)

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
21
Q

Gotcha: missing deps causes stale id.

useEffect(() => {
  fetch(`/api/users/${id}`).then(r => r.json()).then(setUser);
}, []);
A

Verdict: FAIL
- Issue: effect reads id but deps are [].
- Impact: stale user when id changes.
- Fix: include [id] and add race protection.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
22
Q

Gotcha: race condition on id changes.

useEffect(() => {
  fetch(`/api/users/${id}`).then(r => r.json()).then(setUser);
}, [id]);
A

Verdict: FAIL
- Issue: no cancellation/ignore-stale.
- Impact: older response can overwrite newer.
- Fix: AbortController or request-id guard.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
23
Q

Gotcha: no res.ok handling.

const res = await fetch(url);
const data = await res.json();
setData(data);
A

Verdict: FAIL
- Issue: missing res.ok check.
- Impact: 404/500 treated as success.
- Fix: if (!res.ok) throw/map to error state.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
24
Q

Gotcha: async effect callback.

useEffect(async () => {
  const res = await fetch(url);
  setData(await res.json());
}, [url]);
A

Verdict: FAIL
- Issue: effect callback returns Promise.
- Fix: inner async function; keep cleanup available.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
25
Gotcha: loading set before guard. ``` useEffect(() => { setLoading(true); if (!query) return; fetch(`/api?q=${query}`); }, [query]); ```
Verdict: FAIL - Issue: loading can get stuck when query is falsy. - Fix: guard first, then setLoading and fetch.
26
Gotcha: loading never cleared. ``` useEffect(() => { setLoading(true); fetch(url).then(r => r.json()).then(setData); }, [url]); ```
Verdict: FAIL - Issue: no catch/finally. - Impact: loading may stay true. - Fix: catch/finally (or try/catch/finally) to clear loading.
27
Gotcha: debounce missing cleanup. ``` useEffect(() => { const t = setTimeout(() => fetch(url), 300); }, [url]); ```
Verdict: FAIL - Issue: clearTimeout missing. - Fix: return () => clearTimeout(t).
28
Gotcha: timer missing cleanup. ``` useEffect(() => { setTimeout(() => setReady(true), 500); }, []); ```
Verdict: FAIL - Issue: timeout not cleared. - Impact: setState after unmount. - Fix: clearTimeout in cleanup.
29
Gotcha: listener missing cleanup. ``` useEffect(() => { window.addEventListener('resize', onResize); }, []); ```
Verdict: FAIL - Issue: removeEventListener missing. - Impact: leak/duplicate handlers. - Fix: return cleanup to remove listener.
30
Gotcha: stale state after await. ``` const onClick = async () => { await api.save(); setCount(count + 1); }; ```
Verdict: FAIL - Issue: count may be stale after await. - Fix: setCount(prev => prev + 1).
31
Gotcha: dependency loop includes state set by effect. ``` useEffect(() => { fetch(url).then(r => r.json()).then(setData); }, [url, data]); ```
Verdict: FAIL - Issue: data in deps triggers repeated fetch. - Fix: depend on url only.
32
Gotcha: object dep identity changes. ``` const params = { q: query }; useEffect(() => { fetch(`/api/search?q=${params.q}`); }, [params]); ```
Verdict: FAIL - Issue: params is new each render. - Fix: depend on query (or memoize params).
33
Gotcha: missing encodeURIComponent. ``` fetch(`/api/search?q=${query}`); ```
Verdict: FAIL - Issue: query not encoded. - Fix: encodeURIComponent(query).
34
Gotcha: unknown response shape used directly. ``` const data = await res.json(); setName(data.name.toUpperCase()); ```
Verdict: FAIL - Issue: assumes data.name is string. - Fix: validate shape before using.
35
Gotcha: AbortError shown to user. ``` catch (e) { setError(String(e)); } ```
Verdict: FAIL - Issue: AbortError is expected. - Fix: ignore AbortError; surface real failures only.
36
Gotcha: stale finally clobbers loading. ``` useEffect(() => { setLoading(true); fetch(url) .then(r => r.json()) .then(setData) .finally(() => setLoading(false)); }, [url]); ```
Verdict: FAIL - Issue: older request can setLoading(false) during newer. - Fix: cancel/ignore stale (AbortController or request-id).
37
Gotcha: returning null hides loading/empty/error. ``` if (!data) return null; ```
Verdict: FAIL - Issue: ambiguous UI state. - Fix: explicit idle/loading/error/empty/success states.
38
Gotcha: helper closes over changing input. ``` async function load() { const r = await fetch(`/api?q=${query}`); return r.json(); } useEffect(() => { load().then(setData); }, [query]); ```
Verdict: FAIL - Issue: helper reads query from closure. - Fix: pass query as arg or define helper inside effect.
39
Gotcha: subscription missing cleanup. ``` useEffect(() => { api.subscribe(cb); }, []); ```
Verdict: FAIL - Issue: unsubscribe missing. - Fix: return cleanup to unsubscribe.
40
Gotcha: suppressing exhaustive-deps. ``` useEffect(() => { doThing(value); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); ```
Verdict: FAIL - Issue: hides stale-closure bugs. - Fix: refactor so deps are correct; avoid suppression.
41
Gotcha: interval stale closure. ``` const [n, setN] = useState(0); useEffect(() => { const t = setInterval(() => setN(n + 1), 1000); return () => clearInterval(t); }, []); ```
Verdict: FAIL - Issue: interval captures stale n. - Fix: setN(prev => prev + 1).
42
Gotcha: unstable function dep. ``` function Parent({ id }) { const load = () => fetch(`/api/${id}`); useEffect(() => { load(); }, [load]); } ```
Verdict: FAIL - Issue: load changes each render. - Fix: inline in effect or useCallback([id]).
43
Gotcha: open missing from deps. ``` useEffect(() => { if (!open) return; fetch(url).then(r => r.json()).then(setData); }, [url]); ```
Verdict: FAIL - Issue: open missing from deps. - Fix: include open or restructure.
44
Gotcha: controller created outside effect. ``` const controller = new AbortController(); useEffect(() => { fetch(url, { signal: controller.signal }); return () => controller.abort(); }, [url]); ```
Verdict: FAIL - Issue: controller reused across runs. - Fix: create controller inside effect per run.
45
Gotcha: setData receives Response. ``` fetch(url).then(setData); ```
Verdict: FAIL - Issue: setData gets Response, not parsed JSON. - Fix: parse json and validate.
46
Gotcha: unhandled rejection. ``` fetch(url) .then(r => { if (!r.ok) throw new Error('bad'); return r.json(); }) .then(setData); ```
Verdict: FAIL - Issue: thrown error not caught. - Fix: add catch + error state.
47
Gotcha: error not reset on retry. ``` useEffect(() => { fetch(url).catch(() => setError('failed')); }, [url]); ```
Verdict: FAIL - Issue: error never cleared at request start. - Fix: setError(null) before starting request.
48
Gotcha: JSON.stringify in deps. ``` useEffect(() => { fetch('/api', { body: JSON.stringify(filters) }); }, [JSON.stringify(filters)]); ```
Verdict: FAIL - Issue: fragile deps + noisy reruns. - Fix: memoize filters or depend on stable primitives.
49
Gotcha: derived state drift (missing items dep). ``` useEffect(() => { setFiltered(items.filter(i => i.includes(query))); }, [query]); ```
Verdict: FAIL - Issue: items missing from deps. - Fix: include items or derive in render.
50
Gotcha: appends results across queries unintentionally. ``` useEffect(() => { fetch(url).then(r => r.json()).then(d => setItems(prev => [...prev, ...d.items])); }, [query]); ```
Verdict: FAIL - Issue: mixes results. - Fix: reset items when query changes (unless intentional).
51
Gotcha: key driver dep missing (url built from query). ``` const url = `/api?q=${encodeURIComponent(query)}`; useEffect(() => { fetch(url); }, []); ```
Verdict: FAIL - Issue: url/query missing from deps. - Fix: include [url] (or [query]).
52
Gotcha: returning early without clearing loading. ``` useEffect(() => { setLoading(true); if (disabled) return; fetch(url).then(...).finally(() => setLoading(false)); }, [url, disabled]); ```
Verdict: FAIL - Issue: early return can leave loading stuck. - Fix: guard first or clear loading before return.
53
Gotcha: predicate missing from deps. ``` useEffect(() => { setFiltered(items.filter(predicate)); }, [items]); ```
Verdict: FAIL - Issue: predicate missing from deps. - Fix: include predicate or derive in render.
54
Gotcha: cleanup removes wrong handler if handler changes. ``` useEffect(() => { window.addEventListener('scroll', handler); return () => window.removeEventListener('scroll', handler); }, []); ```
Verdict: FAIL - Issue: if handler changes, cleanup is wrong. - Fix: stabilize handler or include it in deps.
55
Gotcha: optimistic update without rollback. ``` setItems(prev => [...prev, item]); await api.create(item); ```
Verdict: PARTIAL FAIL - Issue: no rollback/error handling. - Fix: rollback on failure or refetch.
56
Gotcha: conflates states. ``` if (!data) return
; ```
Verdict: FAIL - Issue: no distinct loading/empty/error. - Fix: explicit states.
57
Gotcha: effect depends on props object. ``` useEffect(() => { doThing(props.value); }, [props]); ```
Verdict: FAIL - Issue: over-broad deps causes reruns. - Fix: depend on props.value.
58
Gotcha: missing clearInterval. ``` useEffect(() => { const t = setInterval(tick, 1000); }, []); ```
Verdict: FAIL - Issue: no clearInterval. - Fix: return () => clearInterval(t).
59
Gotcha: stale closure in subscription handler. ``` const onMessage = (m) => setMsgs([...msgs, m]); useEffect(() => { sub(onMessage); return () => unsub(onMessage); }, [onMessage]); ```
Verdict: FAIL - Issue: stale msgs + unstable handler. - Fix: setMsgs(prev => [...prev, m]) + stabilize handler.
60
Gotcha: missing validation for array. ``` const data = await res.json(); setResults(data.items.map(String)); ```
Verdict: FAIL - Issue: assumes items is array. - Fix: Array.isArray(data.items) check + error state.
61
Scenario: verdict + top 3 issues. ``` function Profile({ id }) { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${id}`).then(r => r.json()).then(setUser); }, [id]); return
{user.name}
; } ```
Verdict: FAIL 1) Race protection missing (older fetch can overwrite newer) 2) Null safety missing (user can be null; user.name can throw) 3) No res.ok/error state
62
Scenario: one-sentence verdict (deps).
FAIL - The effect reads a render-scope value but omits it from deps, causing stale behavior when it changes.
63
Scenario: one-sentence verdict (race).
FAIL - The effect does not cancel or ignore in-flight requests, so older results can overwrite newer state (race condition).
64
Scenario: primary category for missing cleanup.
Primary: Reliability Reason: missing cleanup can trigger updates after unmount or duplicated subscriptions.
65
Scenario: fix approach (no code). ``` useEffect(() => { setLoading(true); fetch(url).then(r => r.json()).then(setData); }, [url]); ```
- Add res.ok + catch/finally to clear loading - Add cancellation/request-id to prevent stale overwrite - Reset state at request start if stale UI is misleading
66
Scenario: user types fast into search (what breaks first?).
- Overlapping requests create race condition - Without debounce/cancel, older results can overwrite - Fix: debounce + AbortController/request-id + error handling
67
Scenario: effect uses object dep (why is it wrong?).
Because object identity changes each render, effect re-runs unnecessarily; depend on primitives or memoize object.
68
Scenario: when is AbortError ignored?
When request is intentionally cancelled on cleanup (unmount/input change). Do not show AbortError to user.
69
Scenario: what is the best fix for stale state after await?
Use functional update: setX(prev => prev + 1) (or compute from prev).
70
Scenario: how do you justify avoiding eslint-disable exhaustive-deps?
Usually you should not. Refactor so deps are correct. Suppress only with strong, documented justification.
71
Scenario: out-of-order requests + loading state (risk).
Old request's finally can setLoading(false) during a newer request; fix with cancel/ignore-stale or request-id.
72
Scenario: why validate JSON as unknown?
Because runtime data can be malformed; validation prevents crashes and makes failures explicit via error state.
73
Scenario: when can you derive data in render instead of state + effect?
When it's purely derived from props/state and doesn't require side effects; avoids deps bugs entirely.
74
Scenario: what does "driver input" mean for deps?
The minimal input(s) that determine the effect's purpose (e.g., id/query/url); depend on drivers, not state set by effect.
75
Scenario: should you reset error state on retry?
Yes. Clear error at request start so new attempt is not blocked by stale error UI.
76
Checklist: top 5 async-effect review points.
1) deps correctness 2) cancellation/ignore-stale 3) res.ok + error handling 4) runtime validation 5) explicit UI states
77
Checklist: dependency reasoning steps.
- Identify driver inputs - List values read from render scope - Include in deps or stabilize - Check for loops - Check for staleness
78
Checklist: minimum fetch hardening.
- encode params - res.ok - parse JSON - validate shape - error state
79
Checklist: cleanup triggers to look for.
- Abort fetch - clearTimeout/clearInterval - removeEventListener - unsubscribe external sources - ignore stale async results
80
Checklist: evaluator wording pattern.
Name issue (stale closure/race/missing cleanup) -> impact (stale UI/wrong data/leak) -> fix (deps + abort/request-id + explicit states).