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
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Batching State UpdatesHow React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.
- Children Prop PatternsUsing children and slot props for flexible APIs: wrappers, compound components, and when to prefer explicit props.
- Client Component BoundariesWhere to put use client: push interactivity to leaves, serializable props, and children as server slots.