Routing, Data Fetching & Async UI
Build resilient navigation and asynchronous interfaces with loading, error, cancellation, race-condition prevention, caching, optimistic updates, and URL-driven state.
Client-Side Routing
A router maps URLs to application views without requiring a full document reload for every navigation. Modern routing systems can support nested layouts, route parameters, data loading, redirects, error boundaries, and code splitting.
Routes should reflect user-facing information architecture rather than component filenames. A route such as /projects/:projectId/settings communicates a resource relationship.
Navigation should remain accessible: use real links for navigation, preserve browser history semantics, and avoid replacing links with clickable generic elements.
<nav aria-label="Primary">
<a href="/dashboard">Dashboard</a>
<a href="/projects">Projects</a>
</nav>- URLs are part of application state and information architecture.
- Use semantic links for navigation.
- Choose route boundaries around user-facing resources and flows.
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.
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]);- Model loading, success, empty, and error states explicitly.
- Abort obsolete requests when appropriate.
- Do not let stale responses overwrite newer state.
Race Conditions & Request Ordering
Suppose a user searches for rea, then quickly changes the query to react. If the first request finishes after the second, a naive implementation can display stale results.
Cancellation is one solution. Another is assigning each request an identity and accepting a response only if it still matches the latest request. Data-fetching libraries often handle these concerns for you.
The broader principle is that asynchronous work can finish in a different order from the order in which it started. UI state must respect the logical ordering expected by the user.
- Completion order is not start order.
- Cancel or ignore obsolete async work.
- Treat request identity as part of correctness, not just performance.
Optimistic UI & Rollback
Optimistic updates change the UI before the server confirms the operation. This can make interactions feel immediate, but it requires a rollback strategy.
For a simple toggle, the UI can update immediately, then revert if the server rejects the change. For complex mutations, preserve enough information to reconstruct the previous state or invalidate/refetch the affected data.
Optimistic UI is appropriate when the operation is likely to succeed and the temporary state is understandable. Do not hide important failures merely to make the UI feel fast.
- Optimistic UI needs a failure path.
- Keep enough information to rollback or invalidate stale data.
- Communicate pending or failed mutations clearly.
URL State, Search Params & Deep Linking
Filters, pagination, sort order, selected tabs, and search queries can often belong in the URL because users may bookmark, share, reload, or navigate back to them.
Treat query parameters as untrusted input. Parse and validate them rather than assuming they have the expected shape.
URL state also improves observability: support teams can reproduce a specific view from a link instead of asking users to describe hidden UI state.
const params = new URLSearchParams(window.location.search);
const page = Math.max(1, Number(params.get("page") || 1));
const query = params.get("q") ?? "";- Put shareable/navigation-relevant state in the URL.
- Validate query parameters.
- Deep links improve reproducibility and user experience.