What is the role of React Router, and how does it work with dynamic routing?
React Router is the de-facto SPA routing library. It maps URLs to component trees, handles navigation without full page reloads, and provides hooks (`useNavigate`, `useParams`, `useLocation`) for programmatic access. Dynamic routing: `<Route path='/users/:id' element={<User/>} />` extracts `id` via `useParams()`. v6/7 adds nested routes, loaders (data fetching co-located with route), and form actions.
React Router is the routing layer most React SPAs use. Next.js, Remix, and TanStack Router are alternatives with different philosophies.
Basic setup (v6/v7)
import { BrowserRouter, Routes, Route } from 'react-router-dom';
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<Users />} />
<Route path="/users/:id" element={<User />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>How it works
- Listens to History API (
pushState,popstate). - On navigation, matches the URL against the route table.
- Renders the matching
<Route>'selement— no full page reload. - Updates the URL without reloading via
pushState.
Dynamic params
function User() {
const { id } = useParams<{ id: string }>();
// fetch user by id
}URL /users/42 → id === '42'.
Nested routes (the v6+ way)
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="users" element={<Users />}>
<Route path=":id" element={<User />} />
</Route>
</Route>
</Routes><Layout> renders an <Outlet/> where children appear.
Programmatic navigation
const navigate = useNavigate();
navigate('/users/42');
navigate(-1); // back
navigate('/users/42', { replace: true, state: { from: 'modal' } });Link
<Link to="/users/42">Profile</Link>
<NavLink to="/users" className={({ isActive }) => isActive ? 'active' : ''}>Users</NavLink><Link> intercepts the click, calls navigate. Right-click 'Open in new tab' still works.
Search params
const [params, setParams] = useSearchParams();
const q = params.get('q');
setParams({ q: 'react' });URL /search?q=react.
Data routers (v6.4+)
Loaders co-locate data fetching with routes.
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/users/:id',
loader: ({ params }) => fetch(`/api/user/${params.id}`).then(r => r.json()),
element: <User />,
},
]);
<RouterProvider router={router} />;function User() {
const user = useLoaderData() as User;
return <h1>{user.name}</h1>;
}Benefits: data loads in parallel with code, no useEffect, no loading flicker.
Actions (form mutations)
{
path: '/users/:id/edit',
action: async ({ request, params }) => {
const data = Object.fromEntries(await request.formData());
await updateUser(params.id, data);
return redirect(`/users/${params.id}`);
},
element: <Edit />,
}<Form> posts to the route's action. Same mental model as plain HTML forms.
Code splitting
const Users = lazy(() => import('./Users'));
<Route path="/users" element={
<Suspense fallback={<Spinner />}><Users /></Suspense>
} />Each route ships as its own chunk.
Alternatives
- Next.js App Router: file-based, RSC-first, integrated with the framework.
- TanStack Router: type-safe, search-param-first, no string paths.
- Remix: now mostly merged with React Router v7 (same data + actions API).
Senior framing
React Router is the contract between URL and UI. v6+ adds data loading and actions to that contract so routes own their data, not their components. The mental shift from 'fetch in useEffect' to 'fetch in loader' is the big win for production apps.
Follow-up questions
- •What's the advantage of loaders over useEffect for route data?
- •How does code splitting per route work with React Router?
- •When would you choose TanStack Router or Next.js over React Router?
Common mistakes
- •Using href instead of <Link> — triggers a full page reload.
- •Reading location inside an effect with bad deps — stale data.
- •Hand-rolling param parsing instead of useParams.
Performance considerations
- •Route-level code splitting cuts initial bundle. Loaders run in parallel with code download. Preload on hover (e.g. Link prefetch) hides navigation latency.
Edge cases
- •Trailing slashes can produce inconsistent matches — pick one and enforce.
- •Hash routing vs browser routing — pick based on server support.
- •Nested routes with shared layouts need <Outlet/> placement.
Real-world examples
- •Most React SPAs use React Router. Vercel's dashboard, Linear, Notion, Cal.com, Sentry — all use either React Router or Next.js routing.