Client Routing Mental Model
Client-side routing updates the URL and UI without full reloads: history API, link interception, and data loading.
- react
- client-routing
Client routing (SPAs and modern framework navigations) changes the URL and renders the next screen without a full document reload. React Router, Next.js App Router client transitions, and similar tools share one mental model: URL is state, history is the timeline, components map to location.
Docs: React Router, Next.js Linking.
Core pieces
- History —
pushState/replaceState/popstate(or framework wrappers). - Location — pathname, search, hash.
- Matcher — maps location to a route element tree.
- Link — navigates via history instead of full load when possible.
- Data — loaders, RSC payloads, or client fetches on location change.
import { Link, useNavigate, useParams } from 'react-router-dom';
function UserLink({ id }) {
return <Link to={`/users/${id}`}>View</Link>;
}
function UserPage() {
const { id } = useParams();
const navigate = useNavigate();
// fetch user by id...
return <button onClick={() => navigate(-1)}>Back</button>;
}
Why full reloads hurt
They re-download documents, reset JS memory, and lose ephemeral UI state. Client routing keeps the shell alive — which is also why layout components and framework nested layouts exist.
Gotchas
| Issue | Notes |
|---|---|
| Scroll restoration | Frameworks differ; reset on pathname change intentionally |
| Focus management | Move focus to main heading after nav for a11y |
| Auth gates | Redirect loops; handle on server when possible |
| External links | Use plain <a> for other origins |
| Code splitting | Lazy route components (React.lazy) |
Next.js flavor
next/link prefetches in view by default for static routes — see prefetch behavior. Soft navigation still runs server components for the next segment as needed.
Interview out-loud
“Client routing maps URL location to a component tree via the History API, intercepting links to avoid full reloads. I treat the URL as shareable state, handle back/forward, manage focus on navigation, and load data per route with loaders or RSC.”
Related on this site
- next/link Prefetch Behavior
- File-based Routing
- Code Splitting with React.lazy
- Layouts and Nested Routes
Further reading
Edge cases worth rehearsing
Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.
Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.
When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.
Edge cases worth rehearsing
Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.
Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.
When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.
Related guides
- useCopyToClipboard HookCopy text with navigator.clipboard, fallback execCommand, transient copied state, and permissions that fail in insecure contexts.
- useDebouncedValue HookDebounce a changing value in React without firing effects on every keystroke. Cleanup, leading vs trailing, and the stale timer that updates unmounted state.
- useEventListener HookAttach a window, document, or element listener with a stable handler ref so you do not rebind on every render. Capture option and target changes.
- useFetch With AbortControllerFetch in an effect with a signal, ignore aborted errors, and prevent the slow response from winning a race after the url changes.
- useIdle HookDetect idle users: reset a timer on input events, fire idle after timeout, and decide whether mount starts the timer immediately.