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.