What Is React Router and How Does Client-Side Routing Work?
Advertisement
🧩 Scenario
Architecture Walkthrough
How the History API Enables Client-Side Routing
Traditional navigation sends an HTTP request to the server and receives a new HTML document. Client-side routing intercepts that navigation before it reaches the server. The browser's History API exposes pushState and replaceState, which update the URL and add an entry to the history stack without triggering an HTTP request. React Router wraps this API: when a user clicks a <Link>, React Router calls pushState, the URL changes, and React re-renders the matching component. No HTML is fetched from the server. Subsequent navigations are pure JavaScript.
The back and forward buttons still work because they step through the history stack, triggering the popstate event, which React Router listens to and uses to re-render the correct component. From the user's perspective, navigation feels identical to a server-rendered site, but the page never reloads.
The Server Configuration Requirement
Client-side routing creates a deployment trap. The server has no knowledge of /dashboard/orders as a route because the route only exists in the JavaScript bundle. When a user navigates directly to /dashboard/orders (by typing it in, refreshing, or following a link from another site), the browser makes an HTTP request to the server for that path. If the server is not configured to return index.html for all routes, it returns a 404 because the file at /dashboard/orders does not exist on disk. The fix is a catch-all rule on the server: serve index.html for any path that does not match a static file, and let React Router handle routing in the browser.
Nested Routes and Layout Composition
React Router v6 nested routes make shared layouts straightforward. A parent route renders a layout component that includes navigation and structure. The <Outlet /> component inside the layout marks where the matched child route renders. Only the content area swaps when navigating between child routes; the layout stays mounted and avoids re-rendering.
Key Code Explained
import {
BrowserRouter,
Routes,
Route,
Link,
Outlet,
useParams,
useNavigate,
Navigate,
} from 'react-router-dom';
// Root router setup
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/products">Products</Link>
</nav>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductListPage />} />
{/* :id is a URL parameter — useParams reads it */}
<Route path="/products/:id" element={<ProductDetailPage />} />
{/* Redirect old URL to new one */}
<Route path="/shop" element={<Navigate to="/products" replace />} />
{/* 404 catch-all */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
}
// Reading URL parameters
function ProductDetailPage() {
const { id } = useParams<{ id: string }>();
const { data: product, isLoading } = useProduct(id!);
if (isLoading) return <Skeleton />;
return <h1>{product?.name}</h1>;
}
// Nested routes: shared layout with Outlet
function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/orders">Orders</Link>
<Link to="/dashboard/settings">Settings</Link>
</aside>
<main>
{/* Matched child route renders here */}
<Outlet />
</main>
</div>
);
}
function AppWithDashboard() {
return (
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
{/* index matches /dashboard exactly */}
<Route index element={<Overview />} />
<Route path="orders" element={<Orders />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
}
// Programmatic navigation
function SearchBar() {
const navigate = useNavigate();
const [query, setQuery] = useState('');
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
// Navigate to a new URL — adds to history stack
navigate(`/search?q=${encodeURIComponent(query)}`);
};
return (
<form onSubmit={handleSearch}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="submit">Search</button>
</form>
);
}
<Link to="/products"> renders an <a> tag but intercepts the click event and calls pushState instead of following the href. This is the critical difference from a plain <a href="/products">, which would cause a full page reload. Using a regular anchor tag is the most common beginner mistake.
Tradeoffs
| Router type | URL format | Server config required | SEO | Best for |
|---|---|---|---|---|
| BrowserRouter | /products/1 | Yes (catch-all for index.html) | Good | Production SPAs |
| HashRouter | /#/products/1 | No (hash never sent to server) | Poor | Static hosts without config access |
| MemoryRouter | No URL change | No | N/A | Tests; embedded apps |
What Interviewers Actually Check
- Whether you can explain pushState and why the page does not reload
- Whether you know the server configuration requirement for direct URL access
- Whether you use Link over anchor tags and can explain why
- Whether you know nested routes and Outlet for layout composition
- Whether you can distinguish index routes from path="/" and explain the difference
Follow-Up Questions
- How does React Router v6 handle scroll restoration when navigating between pages?
- How do you implement code splitting with React.lazy and Suspense in a React Router v6 route config?
- How do you pass data from one route to another without putting it in the URL? What are the tradeoffs compared to URL state?
- How does React Router determine which route wins when two routes could both match the current path?
- How do you test a component that uses useNavigate or useParams in React Testing Library?
Common Candidate Mistakes
- Using
<a href="/page">instead of<Link to="/page">— the anchor tag causes a full page reload - Not configuring the server to serve
index.htmlfor all paths — direct URL access returns a 404 in production - Calling
useParamsoruseNavigatein a component that is not inside a<BrowserRouter>— throws a runtime error - Nesting routes in the JSX without a parent layout route, then wondering why the child component renders in the wrong place
- Using
path="/"for the dashboard index route instead of theindexattribute —path="/"matches the root, not the parent route
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain how
pushStateenables navigation without a page reload? - Can you set up a basic router with
BrowserRouter,Routes,Route, andLink? - Can you create a nested layout route with
<Outlet />and a shared sidebar? - Can you read a dynamic URL segment with
useParamsand navigate programmatically withuseNavigate? - Can you explain the server config requirement and name the alternative for hosts without config access?
Summary
React Router uses the browser's History API (pushState) to update the URL and re-render matching components without an HTTP request. <Link> replaces anchor tags and intercepts clicks before they trigger navigation. Routes are matched by path specificity. Nested routes with <Outlet /> enable shared layouts where only the content area re-renders on navigation. useParams reads dynamic segments from the URL, and useNavigate handles programmatic navigation. The key production concern is server configuration: the server must return index.html for every path that does not match a static file, or direct URL access will return a 404. HashRouter avoids this requirement but at the cost of URL aesthetics and SEO.
What is the difference between BrowserRouter and HashRouter?
BrowserRouter uses the HTML5 History API and produces clean URLs like /products/1. It requires the server to return index.html for all routes. HashRouter uses the URL hash (#) so paths look like /#/products/1. The hash is never sent to the server, so no special server config is needed. HashRouter is simpler to deploy but produces uglier URLs and has SEO limitations.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement