| |

React 58 ⚛️ Client-Side Routing with React Router

⏱️ Estimated read: 17 min | |

A single-page application loads once and never reloads. The browser fetches the initial HTML and JavaScript, and from that point on, every navigation is handled by JavaScript. The URL changes, the view changes, and the browser’s back button still works, but no new document is requested from the server. React Router is the library that makes this possible. It intercepts link clicks, manipulates the browser’s history API, and renders the component that matches the current URL. The result is a navigation experience that feels instant, because the only thing that changes is the part of the page that needs to change.

Key point: Client-side routing does not eliminate the server. The first request is always a server request, and a user who pastes a deep link into a fresh browser tab will hit the server directly. The server must be configured to serve the same HTML shell for every route, or the user will see a 404. React Router handles the navigation after the initial load; the server handles the initial load itself.


Why client-side routing exists

The full-page reload problem. In a traditional multi-page application, every link click triggers a server request for a new HTML document. The browser discards the current page, fetches the new one, and re-renders everything. The user sees a white flash, the application state is lost, and the experience feels slow even on a fast connection.

The SPA promise. A single-page application loads the shell once and then swaps the content inside it. The JavaScript bundle contains the components for every view, and the router decides which one to render based on the URL. The navigation is instant because no network request is needed for the view itself.

The history API. The browser’s history.pushState method lets JavaScript change the URL without reloading the page. The popstate event fires when the user clicks the back or forward button. React Router uses both to keep the URL and the view in sync.

The deep-linking problem. A user who bookmarks /dashboard/settings and returns the next day expects to land on that page. The client-side router can render it, but only after the JavaScript loads. The server must serve the same HTML shell for that URL so the JavaScript can take over. This is why the server needs a catch-all route.

The nested layout problem. Most applications share a header, a sidebar, and a footer across many routes. The router renders the shared layout once and swaps only the content area. The Outlet component is the placeholder for the swapped content.

The data-loading problem. A route may need data before it renders. React Router’s loaders fetch the data before the route renders, which eliminates the loading spinner for the initial render and lets the router show a pending indicator during navigation.


a. Setting up React Router

React Router is installed as a package and configured at the application’s entry point. The configuration differs between the declarative API (<BrowserRouter>, <Routes>, <Route>) and the data router API (createBrowserRouter, <RouterProvider>).

The installation

npm install react-router-dom

The react-router-dom package provides the DOM-specific components: BrowserRouter, Link, NavLink, and the rest. The core react-router package provides the routing logic and is used internally.

The declarative setup

The declarative API wraps the application in a <BrowserRouter> and defines the routes with <Routes> and <Route>.

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

The <Routes> component examines its children and renders the one whose path best matches the current URL. The path="*" route is the catch-all for unmatched URLs.

The data router setup

The data router API uses createBrowserRouter to define the routes as an array and <RouterProvider> to render them.

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
  { path: '/', element: <Home /> },
  { path: '/about', element: <About /> },
  { path: '*', element: <NotFound /> },
]);

function App() {
  return <RouterProvider router={router} />;
}

The data router API supports loaders, actions, and error boundaries as part of the route configuration. The declarative API does not.

The router types

React Router provides three router components for different environments.

RouterUse Case
BrowserRouterStandard web applications with clean URLs
HashRouterStatic hosting without server configuration
MemoryRouterTesting and React Native

The BrowserRouter uses the HTML5 history API and produces clean URLs like /about. The HashRouter uses the hash fragment and produces URLs like /#/about. The hash is not sent to the server, so the server only ever sees /.

The catch-all route

A catch-all route with path="*" handles any URL that does not match a defined route. It is the standard way to implement a 404 page.

<Route path="*" element={<NotFound />} />

Without the catch-all, an unmatched URL renders nothing, and the user sees a blank page.


b. Navigation with Link and NavLink

The HTML <a> tag triggers a full page reload. React Router provides <Link> and <NavLink> as replacements that intercept the click and perform client-side navigation.

The Link component

<Link> renders an <a> tag in the DOM but prevents the browser’s default navigation. It uses the to prop instead of href.

import { Link } from 'react-router-dom';

<Link to="/about">About</Link>
<Link to={`/users/${userId}`}>Profile</Link>

The rendered HTML is <a href="/about">About</a>. The href is present for accessibility and for users who open the link in a new tab, but the click is intercepted for the normal case.

The NavLink component

<NavLink> is a <Link> that knows whether its route is active. It applies the active class or calls a style function with isActive: true when the current URL matches.

import { NavLink } from 'react-router-dom';

<NavLink
  to="/about"
  className={({ isActive }) => isActive ? 'active' : ''}
>
  About
</NavLink>

The isActive flag is true when the location matches the link’s to value. The default matching is a prefix match, so /about/team marks the /about link as active.

The navigate function

For programmatic navigation, the useNavigate hook returns a function that changes the route.

import { useNavigate } from 'react-router-dom';

function LoginButton() {
  const navigate = useNavigate();
  return (
    <button onClick={() => navigate('/dashboard')}>
      Go to dashboard
    </button>
  );
}

The navigate function accepts a path and an options object. The replace: true option replaces the current history entry instead of adding a new one. The state option passes data to the next route.

The Navigate component

The <Navigate> component performs a redirect during render. It is used for route guards.

import { Navigate } from 'react-router-dom';

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" replace />;
  return children;
}

The replace prop replaces the current history entry, so the back button does not return to the protected route.


c. Nested routes and dynamic segments

Nested routes let a parent route render a layout and a child route render the content inside it. Dynamic segments capture values from the URL.

The nested route configuration

The route tree mirrors the URL hierarchy. A parent route defines the layout, and child routes define the content.

<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="about" element={<About />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
    </Route>
  </Route>
</Routes>

The Layout component renders a shared header and an <Outlet>. The child routes render into the outlet. The URL /teams/firebirds renders Layout > Teams > Team.

The Outlet component

<Outlet> is the placeholder for the child route’s element.

import { Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <Header />
      <main>
        <Outlet />
      </main>
      <Footer />
    </div>
  );
}

The header and footer remain mounted across navigations. Only the outlet’s content changes.

The index route

An index route renders at the parent’s URL. It is the default child when no other child matches.

<Route path="/" element={<Layout />}>
  <Route index element={<Home />} />
  <Route path="about" element={<About />} />
</Route>

The URL / renders Layout > Home. The URL /about renders Layout > About.

The dynamic segment

A path segment that starts with : is a dynamic segment. The value is captured and made available through the useParams hook.

<Route path="teams/:teamId" element={<Team />} />
import { useParams } from 'react-router-dom';

function Team() {
  const { teamId } = useParams();
  return <h1>Team: {teamId}</h1>;
}

The URL /teams/firebirds renders Team with teamId equal to "firebirds".

The splat route

A path ending in * is a splat route. It matches any remaining segments and captures them as the splat param.

<Route path="files/*" element={<FileViewer />} />
function FileViewer() {
  const { '*': splat } = useParams();
  return <p>File: {splat}</p>;
}

The URL /files/docs/report.pdf renders FileViewer with splat equal to "docs/report.pdf".


Complete Example Session

// ============================================
// PART 1: THE ROUTER SETUP
// ============================================

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Layout from './Layout';
import Home from './pages/Home';
import About from './pages/About';
import Teams from './pages/Teams';
import Team from './pages/Team';
import NotFound from './pages/NotFound';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'about', element: <About /> },
      { path: 'teams', element: <Teams /> },
      { path: 'teams/:teamId', element: <Team /> },
      { path: '*', element: <NotFound /> },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

// ============================================
// PART 2: THE LAYOUT WITH OUTLET
// ============================================

import { Outlet, NavLink } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <header>
        <nav>
          <NavLink to="/">Home</NavLink>
          <NavLink to="/about">About</NavLink>
          <NavLink to="/teams">Teams</NavLink>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
      <footer>© 2026</footer>
    </div>
  );
}

// ============================================
// PART 3: THE HOME PAGE
// ============================================

function Home() {
  return (
    <div>
      <h1>Home</h1>
      <p>Welcome to the application.</p>
    </div>
  );
}

// ============================================
// PART 4: THE ABOUT PAGE
// ============================================

function About() {
  return (
    <div>
      <h1>About</h1>
      <p>This is the about page.</p>
    </div>
  );
}

// ============================================
// PART 5: THE TEAMS LIST
// ============================================

import { Link } from 'react-router-dom';

const teams = [
  { id: 'firebirds', name: 'Firebirds' },
  { id: 'dolphins', name: 'Dolphins' },
];

function Teams() {
  return (
    <div>
      <h1>Teams</h1>
      <ul>
        {teams.map((team) => (
          <li key={team.id}>
            <Link to={`/teams/${team.id}`}>{team.name}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

// ============================================
// PART 6: THE DYNAMIC TEAM PAGE
// ============================================

import { useParams } from 'react-router-dom';

function Team() {
  const { teamId } = useParams();
  return (
    <div>
      <h1>Team: {teamId}</h1>
      <p>Details for team {teamId}.</p>
    </div>
  );
}

// ============================================
// PART 7: THE 404 PAGE
// ============================================

function NotFound() {
  return (
    <div>
      <h1>404</h1>
      <p>Page not found.</p>
      <Link to="/">Go home</Link>
    </div>
  );
}

// ============================================
// PART 8: PROGRAMMATIC NAVIGATION
// ============================================

import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();

  async function handleSubmit(e) {
    e.preventDefault();
    await login();
    navigate('/dashboard', { replace: true });
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

// ============================================
// PART 9: THE PROTECTED ROUTE
// ============================================

import { Navigate, useLocation } from 'react-router-dom';

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

// ============================================
// PART 10: THE SERVER CONFIGURATION
// ============================================

// The server must serve index.html for all routes.
// Express example:
//
// app.get('*', (req, res) => {
//   res.sendFile(path.join(__dirname, 'build', 'index.html'));
// });
//
// Without this, a direct request to /about returns 404
// because the server has no file at that path.

The ten parts showed the router setup, the layout with outlet, the home and about pages, the teams list, the dynamic team page, the 404 page, programmatic navigation, the protected route, and the server configuration.


Quick Reference

Core Components

ComponentPurpose
BrowserRouterStandard web router
HashRouterStatic hosting router
MemoryRouterTesting and native
RoutesRoute matching container
RouteSingle route definition
LinkClient-side navigation link
NavLinkLink with active state
OutletChild route placeholder
NavigateDeclarative redirect

Core Hooks

HookPurpose
useNavigateProgrammatic navigation
useParamsRead dynamic segments
useLocationCurrent location object
useSearchParamsRead and write query string
useRouteErrorAccess route errors

Route Patterns

PatternMeaning
/aboutStatic segment
:teamIdDynamic segment
*Splat (catch-all)
indexIndex route
/Root layout route

Router Types

RouterURL FormatServer Config
BrowserRouter/aboutRequired
HashRouter/#/aboutNot required
MemoryRouterIn memoryNot applicable

Navigation Options

OptionEffect
replace: trueReplace history entry
state: {...}Pass state to next route
navigate(-1)Go back

Best Practices

✅ Do This:

// Use Link instead of <a>.
<Link to="/about">About</Link>  // ✅

// Use NavLink for navigation menus.
<NavLink to="/about" className={({ isActive }) => ...}>  // ✅

// Use Outlet for nested layouts.
<Outlet />  // ✅

// Use the catch-all route for 404s.
<Route path="*" element={<NotFound />} />  // ✅

// Configure the server to serve index.html for all routes.
app.get('*', (req, res) => res.sendFile('index.html'));  // ✅

// Use useNavigate for programmatic navigation.
const navigate = useNavigate();  // ✅

// Use useParams for dynamic segments.
const { teamId } = useParams();  // ✅

❌ Don’t Do This:

// Don't use <a href> for internal links.
<a href="/about">About</a>  // ❌ Causes full page reload

// Don't forget the catch-all route.
// Unmatched URLs render nothing.  // ❌

// Don't forget the server configuration.
// Deep links return 404 on refresh.  // ❌

// Don't nest routes without an Outlet.
// Child routes render nowhere.  // ❌

// Don't access params in the parent layout.
// Params belong to the matched route.  // ❌

// Don't use useNavigate in the module scope.
// It must be called inside a component.  // ❌

Common Pitfalls

PitfallWhy It HappensFix
404 on refreshServer not configured for SPAAdd catch-all server route
Link reloads pageUsing <a> instead of <Link>Use <Link>
Child route not renderingMissing <Outlet>Add <Outlet> to the layout
Params are undefinedAccessing in wrong componentAccess in the matched route
NavLink always activeWrong end propAdd end for exact matching
Redirect loopProtected route redirects to itselfCheck the auth state correctly

Real-World Examples

1. Basic Router

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
  </Routes>
</BrowserRouter>

2. Nested Routes

<Route path="/" element={<Layout />}>
  <Route index element={<Home />} />
  <Route path="about" element={<About />} />
</Route>

3. Dynamic Segment

<Route path="teams/:teamId" element={<Team />} />

4. Navigation Link

<Link to="/about">About</Link>

5. Active Link

<NavLink to="/about" className={({ isActive }) => isActive ? 'active' : ''}>About</NavLink>

6. Programmatic Navigation

const navigate = useNavigate();
navigate('/dashboard');

7. Redirect

<Navigate to="/login" replace />

8. Query Parameters

const [searchParams, setSearchParams] = useSearchParams();
const page = searchParams.get('page');

9. Splat Route

<Route path="files/*" element={<FileViewer />} />

10. Server Configuration

app.get('*', (req, res) => res.sendFile('index.html'));

Visual

Client-Side Routing Flow

┌────────────────────────────────────────────────────────────┐
│  CLIENT-SIDE ROUTING                                       │
│                                                            │
│  USER CLICKS <Link to="/about">                            │
│         │                                                  │
│         v                                                  │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. React Router intercepts the click.            │     │
│  │  2. history.pushState("/about") changes the URL.  │     │
│  │  3. No server request is made.                    │     │
│  │  4. Router matches "/about" to the About route.   │     │
│  │  5. About component renders into the Outlet.      │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The header and footer remain mounted.                     │
│  Only the content area changes.                            │
└────────────────────────────────────────────────────────────┘

The Server vs Client Boundary

┌────────────────────────────────────────────────────────────┐
│  THE TWO PHASES OF SPA ROUTING                             │
│                                                            │
│  PHASE 1: INITIAL REQUEST                                  │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Browser ──▶ Server                                │     │
│  │  GET /about                                       │     │
│  │                                                   │     │
│  │  Server ──▶ Browser                                │     │
│  │  index.html + JavaScript bundle                   │     │
│  │  (The server must serve this for every route.)    │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        │  JavaScript loads                │
│                        v                                   │
│  PHASE 2: CLIENT-SIDE NAVIGATION                           │
│  ┌──────────────────────────────────────────────────┐     │
│  │  User clicks <Link to="/dashboard">                │     │
│  │  React Router changes the URL locally.            │     │
│  │  No server request.                                │     │
│  │  Dashboard component renders.                     │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The server is needed for the first load and for           │
│  deep links. The client handles navigation after that.     │
└────────────────────────────────────────────────────────────┘

Nested Routes

┌────────────────────────────────────────────────────────────┐
│  NESTED ROUTES AND OUTLET                                  │
│                                                            │
│  URL: /teams/firebirds                                     │
│                                                            │
│  <Routes>                                                  │
│    <Route path="/" element={<Layout />}>                  │
│      <Route path="teams" element={<Teams />}>             │
│        <Route path=":teamId" element={<Team />} />        │
│      </Route>                                             │
│    </Route>                                               │
│  </Routes>                                                │
│                                                            │
│  RENDERED TREE:                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <Layout>                                         │     │
│  │    <header>...</header>                           │     │
│  │    <main>                                         │     │
│  │      <Outlet>                                     │     │
│  │        └── <Teams>                               │     │
│  │              <Outlet>                            │     │
│  │                └── <Team teamId="firebirds" />   │     │
│  │      </Outlet>                                    │     │
│  │    </main>                                        │     │
│  │    <footer>...</footer>                           │     │
│  │  </Layout>                                        │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  Each route renders into its parent's Outlet.              │
└────────────────────────────────────────────────────────────┘

Router Types

┌────────────────────────────────────────────────────────────┐
│  THREE ROUTER TYPES                                        │
│                                                            │
│  BrowserRouter                                             │
│  ┌──────────────────────────────────────────────────┐     │
│  │  URL: https://example.com/about                   │     │
│  │  Uses the HTML5 history API.                      │     │
│  │  Requires server configuration.                   │     │
│  │  Clean URLs. Best for production.                 │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  HashRouter                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  URL: https://example.com/#/about                 │     │
│  │  Uses the hash fragment.                          │     │
│  │  No server configuration needed.                  │     │
│  │  Ugly URLs. Good for static hosting.              │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  MemoryRouter                                              │
│  ┌──────────────────────────────────────────────────┐     │
│  │  URL: (in memory, not visible)                    │     │
│  │  Used in tests and React Native.                  │     │
│  │  No browser history.                              │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
React RouterClient-side routing library for React
BrowserRouterStandard web router using history API
HashRouterRouter using hash fragment
LinkClient-side navigation link
NavLinkLink with active state
OutletChild route placeholder
useNavigateProgrammatic navigation hook
useParamsDynamic segment hook
useSearchParamsQuery string hook
Nested routesRoutes rendered inside parent routes
Dynamic segments:param syntax for URL values
Catch-all routepath="*" for 404 pages

Key takeaways:

  • Client-side routing intercepts link clicks and changes the URL without a server request. React Router uses the browser’s history API to keep the URL and the view in sync. The navigation is instant because no new document is fetched.
  • The server is still needed for the initial load and deep links. A user who pastes a URL into a fresh browser hits the server directly. The server must serve the same HTML shell for every route, or the user sees a 404. This is the catch-all server configuration.
  • <Link> and <NavLink> replace <a> for internal navigation. <a> triggers a full page reload, which defeats the purpose of a SPA. <Link> renders an <a> for accessibility but intercepts the click.
  • Nested routes mirror the URL hierarchy. A parent route renders the shared layout and an <Outlet>. Child routes render into the outlet. The URL /teams/firebirds renders Layout > Teams > Team.
  • Dynamic segments capture URL values. A path segment starting with : is dynamic, and its value is available through useParams. The URL /teams/firebirds gives teamId equal to "firebirds".
  • The catch-all route handles unmatched URLs. A route with path="*" renders the 404 page. Without it, an unmatched URL renders a blank page.
  • useNavigate provides programmatic navigation. It returns a function that changes the route. The replace option replaces the history entry, and the state option passes data to the next route.
  • The HashRouter avoids server configuration. It uses the hash fragment, which the server never sees. The trade-off is uglier URLs and worse SEO.
  • useSearchParams reads and writes the query string. It returns a URLSearchParams object and a setter. The setter updates the URL and triggers a re-render.

Remember: React Router is the library that turns a React application into a single-page application. It intercepts navigation, changes the URL without reloading, and renders the component that matches the route. The BrowserRouter uses the history API for clean URLs, the HashRouter uses the hash fragment for static hosting, and the MemoryRouter is for tests and native. The Link and NavLink components replace <a> for internal navigation, and the Outlet component renders nested child routes. Dynamic segments capture URL values, and the catch-all route handles 404s. The server must be configured to serve the HTML shell for every route, because the first request is always a server request. The result is a navigation experience that feels instant, because the only thing that changes is the part of the page that needs to change.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!