Fetching Data & Async State
A robust data-fetching flow distinguishes at least loading, success, empty, and error states. Production UIs also need retry behavior, cancellation, stale-data handling, and feedback during background refresh.
Avoid setting loading = false blindly in multiple unrelated branches. Model the lifecycle explicitly so a stale request cannot overwrite a newer result.
For complex applications, server-state libraries often provide caching and request coordination. For simple pages, a small custom hook may be sufficient.
Basic fetch with cancellation
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(error => {
if (error.name !== "AbortError") setError(error);
});
return () => controller.abort();
}, [id]);💡 Aborting the previous request prevents a component from retaining unnecessary work when its input changes.
Key Takeaways
- Model loading, success, empty, and error states explicitly.
- Abort obsolete requests when appropriate.
- Do not let stale responses overwrite newer state.