4. Routing (React Router)
React has no built-in router. React Router is the standard choice for client-side routing.
These notes use React Router v6/v7 syntax. If you learned v5 (Switch, useHistory, component={}), the differences are listed at the end — interviewers still ask about them.
1. What Is Client-Side Routing?
In a traditional site, every link triggers a full page request to the server.
In a SPA, the router:
- Intercepts the click
- Updates the URL with the History API (
pushState) - Swaps the component — no page reload, no white flash
Interview Point
The URL changes but the browser never makes a document request. That is why a refresh on /about returns 404 unless the server is configured to serve index.html for all paths — a very common deployment interview question.
2. Basic Setup
import { BrowserRouter, Routes, Route } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}Router Types
| Router | Uses | When |
|---|---|---|
BrowserRouter | History API — clean URLs | Default; needs server rewrite config |
HashRouter | #/about | Static hosts with no rewrite control |
MemoryRouter | In-memory, no URL | Tests, React Native |
3. Navigation
Link and NavLink
import { Link, NavLink } from "react-router-dom";
<Link to="/about">About</Link>
<NavLink
to="/about"
className={({ isActive }) => (isActive ? "active" : "")}
>
About
</NavLink>NavLink knows whether it is the current route. Link does not.
Never Use an Anchor Tag
<a href="/about">About</a> // full page reload, loses all state
<Link to="/about">About</Link> // correctProgrammatic Navigation
import { useNavigate } from "react-router-dom";
function Login() {
const navigate = useNavigate();
const handleLogin = async () => {
await api.login();
navigate("/dashboard", { replace: true });
};
}replace: true removes the login page from history, so the back button doesn't return the user to it after logging in. This is the standard answer to "how do you redirect after login?".
Navigate Component
if (!user) return <Navigate to="/login" replace />;Use this for a redirect during render; use useNavigate inside an event handler or effect.
4. Route Params
<Route path="/users/:id" element={<UserDetail />} />import { useParams } from "react-router-dom";
function UserDetail() {
const { id } = useParams(); // always a string
const { data } = useQuery({ queryKey: ["user", id] });
}Optional and Splat Params
<Route path="/files/*" element={<FileBrowser />} /> // matches /files/a/b/c
<Route path="/users/:id?" element={<Users />} /> // id optional5. Query Strings
import { useSearchParams } from "react-router-dom";
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const page = Number(searchParams.get("page") ?? 1);
const sort = searchParams.get("sort") ?? "name";
const nextPage = () => setSearchParams({ page: page + 1, sort });
}Interview Point
Filters, sorting and pagination belong in the URL, not in useState. It makes the view shareable, bookmarkable, and survivable across a refresh — and the back button works for free.
6. Nested Routes and Outlet
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} />
<Route path="settings" element={<Settings />} />
<Route path="billing" element={<Billing />} />
</Route>
</Routes>function DashboardLayout() {
return (
<div className="layout">
<Sidebar />
<main>
<Outlet /> {/* child route renders here */}
</main>
</div>
);
}index is the default child, rendered at /dashboard exactly.
The sidebar stays mounted while children swap — no re-render, no flicker.
7. Protected Routes
function ProtectedRoute({ children }) {
const { user, isLoading } = useAuth();
const location = useLocation();
if (isLoading) return <Spinner />;
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>Then after login, send them where they were going:
const from = location.state?.from?.pathname || "/dashboard";
navigate(from, { replace: true });Layout Route Version
Cleaner when many routes need the same guard:
<Route element={<ProtectedLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>Security Note
Route guards are UX only. Anyone can open DevTools and flip the client-side flag. Every protected resource must be authorised on the server as well.
8. Lazy Loading Routes
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./pages/Dashboard"));
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>Route-level code splitting is the highest-value performance win in most React apps — the initial bundle only contains the landing page.
9. Data Router (v6.4+)
The newer API moves data fetching into the route definition, removing the render-then-fetch waterfall.
const router = createBrowserRouter([
{
path: "/users/:id",
element: <UserDetail />,
loader: async ({ params }) => {
const res = await fetch(`/api/users/${params.id}`);
if (!res.ok) throw new Response("Not Found", { status: 404 });
return res.json();
},
action: async ({ request }) => {
const formData = await request.formData();
return updateUser(formData);
},
errorElement: <ErrorPage />,
},
]);
<RouterProvider router={router} />function UserDetail() {
const user = useLoaderData();
return <h1>{user.name}</h1>;
}Interview Point
Loaders fetch in parallel with the code chunk, before the component renders. The classic useEffect fetch can only start after the component has mounted — a render → fetch → render waterfall.
10. Useful Hooks
| Hook | Returns |
|---|---|
useNavigate | Function to navigate programmatically |
useParams | Route params object |
useSearchParams | [params, setParams] for the query string |
useLocation | { pathname, search, hash, state } |
useMatch | Whether the current URL matches a pattern |
useLoaderData | Data from the route loader |
useNavigation | Global navigation state (idle / loading / submitting) |
11. v5 vs v6 Differences
Still asked, because plenty of production code is on v5.
| v5 | v6 / v7 |
|---|---|
<Switch> | <Routes> |
component={Home} or render={} | element={<Home />} |
exact prop needed | Matching is exact by default |
useHistory() | useNavigate() |
history.push("/x") | navigate("/x") |
history.replace("/x") | navigate("/x", { replace: true }) |
| Nested routes defined in the child | Nested <Route> + <Outlet> |
withRouter HOC | Hooks only |
| Best-match by declaration order | Best-match by specificity (ranked) |
12. Re-render on Browser Resize
A frequent "experienced" question that has nothing to do with routing but gets grouped with it.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return width;
}Follow-up They Want
Resize fires dozens of times per second. Throttle or debounce it:
useEffect(() => {
let frame;
const onResize = () => {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => setWidth(window.innerWidth));
};
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
cancelAnimationFrame(frame);
};
}, []);Better still: use a CSS media query if you only need it for styling. JavaScript is the wrong tool for a purely visual breakpoint.