Routing
Routing defines page boundaries, data boundaries, and loading strategy. It is a major lever for performance and maintainability.
A baseline routing checklist
This checklist is intentionally boring. If you implement it well, most routing-related UX bugs disappear.
- Loading UI per route segment (skeletons, spinners only when necessary).
- Error boundaries per route segment with actionable recovery.
- Prefetch on intent (hover/viewport) rather than eager prefetch everywhere.
- Scroll restoration or explicit behavior on navigation.
Routes are boundaries (UI, data, errors)
A good route boundary answers: what data loads here, what can stream, what errors are handled here, and what UI should stay stable across navigation.
Once you treat routing as boundaries, decisions like “where to fetch” and “where to cache” stop being personal preference and become architecture.
- Keep shared chrome (nav, layout, sidebar) outside leaf routes so it does not remount.
- Put data loading as high as needed to avoid waterfalls, but no higher.
- Define error boundaries close to the failing unit so recovery is specific.
Loading strategy
Loading UI is not just decoration—it’s how you prevent “jank” and state loss from being perceived as bugs.
- Prefer skeletons that match final layout to reduce layout shift.
- Avoid global spinners for local loading; keep the rest of the page interactive.
- Decide whether to keep previous screen during navigation (optimistic) or reset immediately (strict).
Scroll restoration (minimal pattern)
If your framework does not restore scroll automatically (or you have custom containers), you can store scroll positions by route key.
import { useEffect } from 'react';
export function useScrollRestoration(key: string) {
useEffect(() => {
const saved = sessionStorage.getItem(`scroll:${key}`);
if (saved) window.scrollTo({ top: Number(saved) });
const onScroll = () => {
sessionStorage.setItem(`scroll:${key}`, String(window.scrollY));
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, [key]);
}Avoid waterfalls
If multiple components fetch on mount, you often get waterfalls. Move fetching up to the route boundary or a shared data layer.
Common pitfalls
- State loss or flicker on navigation due to missing loading UI and error boundaries.
- Scattered fetching across components causing waterfalls and duplicate requests.
Example: stable URL state
Model view state in the URL when it should be shareable/bookmarkable.
import { useMemo } from 'react';
export function useQueryParam(searchParams: URLSearchParams, key: string) {
return useMemo(() => searchParams.get(key) ?? '', [searchParams, key]);
}Production checklist
- Every route has defined loading + error + empty states.
- Navigation preserves the right state (layout), and resets the right state (leaf).
- Scroll behavior is intentional: restored where expected, reset where expected.
- Prefetch is on intent, not everywhere; does not overload the network.
Further reading
- Guides: Data Fetching