Deck 2 - React Hooks: Async Correctness (objective)
Goal: spot deps/stale-closure/race/cleanup issues fast.
What you practice: 1 verdict sentence + 2-3 major issues + fix approach.
Define dependency array (deps).
Define stale closure.
Define race condition in async effects.
Define cleanup function (effect).
Why should useEffect callback not be async?
Define setState-after-unmount.
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>;
}Define functional update.
StrictMode note (dev).
When should you reset state on input change?
Minimum safe fetch handling.
Treat res.json() as what type by default?
Define “Dependency Loop” in React Effects:
useEffect updates a value that is also in its dependency array.setCount(c => c + 1) (removes dependency).id, not the object.onClick).THE LONG (FULL) ANSWER
useEffect(() => { setCount(count + 1) }, [count])setCount(c => c + 1) and remove count from the dependency array.onClick) instead of a “syncing” effect.id (string) instead of user (object).Define referential instability.
Define AbortController usage (review-level).
Define request-id guard (review-level).
Define debounce + cleanup.
Define TOCTOU bug (async UI).
Evaluator phrasing pattern (1 sentence).
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.”
Top 3 async-effect risks to look for first.
1) deps correctness
2) cancellation / ignore-stale
3) error handling (res.ok + catch)
Gotcha: missing deps causes stale id.
useEffect(() => {
fetch(`/api/users/${id}`).then(r => r.json()).then(setUser);
}, []);Verdict: FAIL
- Issue: effect reads id but deps are [].
- Impact: stale user when id changes.
- Fix: include [id] and add race protection.
Gotcha: race condition on id changes.
useEffect(() => {
fetch(`/api/users/${id}`).then(r => r.json()).then(setUser);
}, [id]);Verdict: FAIL
- Issue: no cancellation/ignore-stale.
- Impact: older response can overwrite newer.
- Fix: AbortController or request-id guard.
Gotcha: no res.ok handling.
const res = await fetch(url); const data = await res.json(); setData(data);
Verdict: FAIL
- Issue: missing res.ok check.
- Impact: 404/500 treated as success.
- Fix: if (!res.ok) throw/map to error state.
Gotcha: async effect callback.
useEffect(async () => {
const res = await fetch(url);
setData(await res.json());
}, [url]);Verdict: FAIL
- Issue: effect callback returns Promise.
- Fix: inner async function; keep cleanup available.