Back to React
React
medium
mid

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.

7 min read·~5 min to think through

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)

tsx
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>'s element — no full page reload.
  • Updates the URL without reloading via pushState.

Dynamic params

tsx
function User() {
  const { id } = useParams<{ id: string }>();
  // fetch user by id
}

URL /users/42id === '42'.

Nested routes (the v6+ way)

tsx
<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

tsx
const navigate = useNavigate();
navigate('/users/42');
navigate(-1); // back
navigate('/users/42', { replace: true, state: { from: 'modal' } });

Link

tsx
<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

tsx
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.

tsx
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} />;
tsx
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)

tsx
{
  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

tsx
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.

Senior engineer discussion

Senior framing: routing is the architecture of the app, not just URL parsing. v6+ data routers reframe routes as the unit of data ownership — closer to Remix and Next.js. That's the direction the ecosystem is going.