Design a client-side router for a single-page app. Apply RADIO; cover the History API, lazy routes, and scroll.
History API usage, path matching, code-splitting routes, scroll restoration, and deep-link handling.
Requirements: client-side navigation with no full reloads, deep-linkable URLs, back/forward support, lazy-loaded routes, scroll restoration, and per-route data loading. Architecture: a Router listens to history changes and matches the current path against a route table to pick a component; Links call history.pushState instead of navigating; a popstate listener re-renders on back/forward. Data model: the route table [{ path, component, loader }] plus current location; params parsed from the path. Interface: <Link to>, useParams(), useNavigate(). Optimizations: use the History API (pushState/replaceState + popstate) — intercept Link clicks, preventDefault, push the URL, swap the view; CODE-SPLIT routes with dynamic import() so each route's bundle loads on demand (a big first-load win); prefetch a route's chunk on link hover/focus; restore scroll to top on push but preserve position on back (save scrollY per history entry); handle a not-found route and nested/relative routes. Path matching: convert patterns like /users/:id into a regex capturing params, matching most-specific first. Why History API over hash routing: real paths (/users/1) are clean, server-renderable, and SEO-friendly — hash routing (#/users/1) is a fallback for static hosts that can't rewrite. Critically, the server must rewrite all unknown paths to index.html so a deep link or refresh doesn't 404. Accessibility: move focus to the new view's heading on navigation and announce route changes so screen-reader users aren't stranded after a silent DOM swap.
Any SPA that needs deep links and back/forward without reloads; reasoning about history, lazy routes, and the index.html rewrite.
function Router({ routes }) {
const [path, setPath] = React.useState(location.pathname);
React.useEffect(() => {
const onPop = () => setPath(location.pathname);
window.addEventListener('popstate', onPop); // back/forward
return () => window.removeEventListener('popstate', onPop);
}, []);
const match = routes.find((r) => matchPath(r.path, path));
const Comp = match ? match.component : NotFound;
return <Comp params={match ? match.params : {}} />;
}
function Link({ to, children }) {
return (
<a href={to} onClick={(e) => {
e.preventDefault();
history.pushState({}, '', to); // no reload
dispatchEvent(new PopStateEvent('popstate')); // trigger re-render
}}>{children}</a>
);
}
// Lazy route: { path: '/settings', component: React.lazy(() => import('./Settings')) }