React Data Fetching Patterns
Use a server-state library by default; otherwise keep fetches close to the feature, cancel stale requests with AbortController, debounce user-driven queries, and know your Promise combinators.
🌱 A rough idea, planted recently. Expect rough edges — it will grow.
Use a server-state library (TanStack Query, formerly React Query) by default. Without one, keep fetch close to the
feature that uses it, cancel stale requests with an AbortController, and debounce anything the user types.
Where to fetch
- TanStack Query handles caching, de-duplication, retries and cancellation. It’s my default.
- Without it, fetching inside the feature component (or a feature hook) is fine. Keep the data close to what uses it, with an abort in the effect cleanup.
Cancelling with AbortController
Cancelling stale requests prevents race conditions, where an older, slower response overwrites a newer one. It also saves you from manual “ignore this” flags.
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users?q=${q}`, { signal: ctrl.signal })
.then((r) => r.json())
.then(setUsers)
.catch((e) => { if (e.name !== "AbortError") setError(e); });
return () => ctrl.abort(); // a new q, or unmounting, cancels the old request
}, [q]);
// A timeout on anything that accepts a signal:
fetch(url, { signal: AbortSignal.timeout(3000) });
// Both at once:
AbortSignal.any([ctrl.signal, AbortSignal.timeout(3000)]);
TanStack Query passes a signal into your queryFn (({ signal }) => fetch(url, { signal })) and aborts it when the
query goes stale or unmounts.
A debounce hook
Around 300 ms is a sensible delay for search-as-you-type.
function useDebounce<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id); // each keystroke restarts the timer
}, [value, delay]);
return debounced;
}
Debounce waits for a pause (search). Throttle caps the rate (scroll, streaming updates); see Finding React Re-render and Memory Leaks.
Promise combinators
| Combinator | Resolves when | Rejects when |
|---|---|---|
Promise.all | all fulfil (results in order) | any rejects (fail fast) |
Promise.allSettled | all settle; you get {status, value/reason} for each | never |
Promise.any | the first fulfils | all reject (AggregateError) |
Promise.race | the first settles, success or failure | the first to settle rejects |
Racing against a timeout promise used to be the way to time out a request. AbortSignal.timeout is better because it
actually cancels the request.
Related
What to do when a request fails is in React Error Handling; testing fetch code without mocking fetch itself is in
Stop Mocking Fetch; the n+1 request problem is in Browser Fundamentals for Front-end Engineers.
- #react
- #data-fetching
- #react-query
- #abortcontroller
- #promises
↩ Linked from (2)
…e users distinct states: offline, 4xx (fix your input), 5xx (try again) and timeout (AbortSignal.timeout; see React Data Fetching Patterns). Race conditions between responses are a data-fetching bug, not an error. Cancel…
…back at how my thinking on this has changed: State Management in React, Revisited. Where data fetching fits: React Data Fetching Patterns.
↪ Links to (4)
Related by topic
- Seedcase study
Building and Levelling Up a UI Team
Career - Seed
Contract-Driven Development
Platform engineering - Growing
Front-end Performance Checklist
Frontend craft - Growing
Front-end Rendering Strategies and Architecture
Frontend craft