Contents
15 Chapter

Routing overview (React Router)

The concept of SPA routing, the basics of React Router v7, and a comparison with Part 4's Next.js App Router — all in one chapter.

Chapter 14 covered the performance-optimization tools. This is the final chapter of Part 2. So far we have covered what happens inside a single screen, but real apps usually have multiple screens. Clicking a menu changes the screen and the URL, and the back button must work. The tool that handles these screen transitions is routing.

The React Router model we cover in this chapter is comparable to Part 4’s App Router (Modern Next.js). At the end of this chapter, we put a decision table for the two models so you have a feel for “which one fits which case”.

Traditional web vs SPA #

A traditional web page asked the server for a new page every time the user clicked a link, received freshly built HTML, and repainted the entire screen. That is the model with the white flash on every transition.

A SPA (Single Page Application) receives HTML once, and after that the client redraws the screen with JavaScript for every transition. Without asking the server for new HTML, the client swaps the screen on its own, so transitions are fast and smooth.

The basic React app generated by Vite is an SPA. Since everything happens inside the HTML received once, we have to decide which screen to show for which URL ourselves. This is called client-side routing, and the de-facto standard library in the React ecosystem is React Router.

Note
Meta-frameworks like Next.js provide their own filesystem-based routing and do not use React Router. Part 4 covers that. This chapter focuses on handling screen transitions in pure React (a Vite-built app).

Installing React Router #

Add React Router to the Vite project we have been using.

install React Router
pnpm add react-router

This book uses React Router v7 as its baseline. From v7, everything is unified into a single react-router package; react-router-dom, which you often see in older material, is no longer a separate package. When migrating older code, change only the imports from react-router-dom to react-router — the API works almost the same way.

The simplest example #

Let’s see the basic structure of routing first.

src/App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router';

function Home() {
  return <h1>Home page</h1>;
}

function About() {
  return <h1>About page</h1>;
}

function App() {
  return (
    <BrowserRouter>
      <nav style={{ padding: '8px', borderBottom: '1px solid #ccc' }}>
        <Link to="/">Home</Link>
        {' | '}
        <Link to="/about">About</Link>
      </nav>

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

export default App;

The core elements:

  • <BrowserRouter> — the top-level wrapper that enables routing. It wraps the whole app
  • <Routes> — the container that picks the one <Route> matching the current URL among several and renders it
  • <Route path="..." element={<...>} /> — defines which component to show for which path
  • <Link to="..."> — a link that switches routes without a screen flash

Using a plain anchor tag like <a href="/about"> makes the browser reload the page and lose the SPA benefit. You must use <Link> for client-side transitions.

URL parameters — dynamic paths #

For paths where part of the URL is dynamic, like product detail pages or user profiles, prefix the dynamic part with a colon (:).

src/App.jsx
<Route path="/users/:userId" element={<UserProfile />} />

URLs like /users/123 or /users/cheolsu all match this route. Inside the component, pull the dynamic part out with the useParams hook.

src/UserProfile.jsx
import { useParams } from 'react-router';

function UserProfile() {
  const { userId } = useParams();

  return <h1>User ID: {userId}</h1>;
}

export default UserProfile;

useParams returns every parameter named in path as an object. With path="/users/:userId/posts/:postId", you can pull out { userId, postId }.

Programmatic navigation — useNavigate #

Beyond links, there are times when you have to navigate from code. Going to a result page after submitting a form, or sending the user home from a logout button. Use the useNavigate hook.

src/LoginForm.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router';

function LoginForm() {
  const [email, setEmail] = useState('');
  const navigate = useNavigate();

  function handleSubmit(e) {
    e.preventDefault();
    // ... handle login ...
    navigate('/dashboard');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Sign in</button>
    </form>
  );
}

navigate('/path') to go to a path, navigate(-1) to go back, navigate(1) to go forward.

Query parameters — useSearchParams #

For the ?key=value&key2=value2 part of a URL (the query string), use useSearchParams.

src/SearchPage.jsx
import { useSearchParams } from 'react-router';

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get('q') ?? '';

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setSearchParams({ q: e.target.value })}
        placeholder="Search"
      />
      <p>Current query: {query}</p>
    </div>
  );
}

useSearchParams works with an interface similar to useState. As you type, the URL itself updates to something like /search?q=react, and refreshing or sharing the URL restores the same state. Useful for cases where the state should be reflected in the URL, like search-result pages.

Nested routes and Outlet #

When several pages share the same layout (header, sidebar, etc.), nested routes are clean. A parent route paints the common layout, and a child route fills the content area inside it.

src/App.jsx
<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="about" element={<About />} />
    <Route path="users/:userId" element={<UserProfile />} />
  </Route>
</Routes>
src/Layout.jsx
import { Link, Outlet } from 'react-router';

function Layout() {
  return (
    <div>
      <header style={{ padding: '8px', background: '#f4f4f4' }}>
        <Link to="/">Home</Link>
        {' | '}
        <Link to="/about">About</Link>
      </header>
      <main style={{ padding: '16px' }}>
        <Outlet />
      </main>
    </div>
  );
}

The child route’s component renders at the position of <Outlet />. <Route index> means the child to show when the URL exactly matches the parent path (/).

Thanks to this pattern, header / footer code lives in one place while the middle content changes by URL. Part 4’s Next.js App Router automates the same operating principle (layout + child page) with the filesystem.

404 page #

To show a page when no route matches, put a wildcard route with path="*" at the end.

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

Matching is attempted top-down, and * catches anything that does not match.

Active link styling — NavLink #

When you want to highlight the link for the current page in a navigation bar, use <NavLink> instead of <Link>.

src/Nav.jsx
import { NavLink } from 'react-router';

function Nav() {
  return (
    <nav>
      <NavLink
        to="/"
        end
        style={({ isActive }) => ({
          fontWeight: isActive ? 'bold' : 'normal',
          color: isActive ? 'tomato' : 'inherit',
        })}
      >
        Home
      </NavLink>
      {' | '}
      <NavLink
        to="/about"
        style={({ isActive }) => ({
          fontWeight: isActive ? 'bold' : 'normal',
          color: isActive ? 'tomato' : 'inherit',
        })}
      >
        About
      </NavLink>
    </nav>
  );
}

If you pass a function to style (or className), it receives an isActive flag that you can branch on. The end prop means “active only on this exact path” and is commonly used for the root path /. Without it, every nested path also triggers active.

React Router vs Next.js App Router — making a decision #

Part 4 of this book covers the Next.js App Router. It tackles the same problem as React Router from this chapter (changing screens based on URL) in a different way. A short comparison of the two operating models follows.

ItemReact Router (v7)Next.js App Router
Defining a route<Route path> component treeFilesystem (app/users/[userId]/page.tsx)
Dynamic parameter:userId + useParams[userId] folder + params prop
Layout<Outlet /> + nested routesapp/layout.tsx (auto-nested)
Data fetchinguseEffect inside the component (or v7’s loader API)Direct fetch in the body of a Server Component function
SSROptional (Framework Mode requires extra setup)Default
Build outputClient SPARSC + client component mix
Learning curveRelatively simpleNeed to learn App Router + the RSC model
Suited forSPAs you can ship fast, client-only tools / dashboardsServices where SEO matters, fullstack apps, server-first models

The React Router in this chapter fits client-side SPAs: low SEO demand, simple server data fetching, and the desire to ship one SPA fast. The Next.js App Router fits fullstack apps and SEO-critical services.

Part 6 of this book (the fullstack Todo capstone) is built with Next.js. The React Router in this chapter is the step that builds the foundation for “how to handle SPA routing in React alone”.

Try it yourself #

Let’s tie what we have learned together into a small mini-site. It has four pages: Home, About, User List, User Detail.

src/Layout.jsx:

src/Layout.jsx
import { NavLink, Outlet } from 'react-router';

function Layout() {
  const linkStyle = ({ isActive }) => ({
    fontWeight: isActive ? 'bold' : 'normal',
    color: isActive ? 'tomato' : 'inherit',
    marginRight: '12px',
  });

  return (
    <div>
      <header style={{ padding: '12px', background: '#f4f4f4', borderBottom: '1px solid #ccc' }}>
        <NavLink to="/" end style={linkStyle}>Home</NavLink>
        <NavLink to="/about" style={linkStyle}>About</NavLink>
        <NavLink to="/users" style={linkStyle}>Users</NavLink>
      </header>
      <main style={{ padding: '16px' }}>
        <Outlet />
      </main>
    </div>
  );
}

export default Layout;

src/pages/Home.jsx:

src/pages/Home.jsx
function Home() {
  return (
    <div>
      <h1>Home</h1>
      <p>A React Router mini-site.</p>
    </div>
  );
}

export default Home;

src/pages/UserList.jsx:

src/pages/UserList.jsx
import { Link } from 'react-router';

const USERS = [
  { id: 1, name: 'Cheolsu' },
  { id: 2, name: 'Younghee' },
  { id: 3, name: 'Minsu' },
];

function UserList() {
  return (
    <div>
      <h1>User list</h1>
      <ul>
        {USERS.map(user => (
          <li key={user.id}>
            <Link to={`/users/${user.id}`}>{user.name}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

src/pages/UserDetail.jsx:

src/pages/UserDetail.jsx
import { useParams, useNavigate } from 'react-router';

const USERS = {
  1: { name: 'Cheolsu', email: 'cheolsu@example.com' },
  2: { name: 'Younghee', email: 'younghee@example.com' },
  3: { name: 'Minsu', email: 'minsu@example.com' },
};

function UserDetail() {
  const { userId } = useParams();
  const navigate = useNavigate();
  const user = USERS[userId];

  if (!user) {
    return (
      <div>
        <h1>User not found.</h1>
        <button onClick={() => navigate('/users')}>Back to list</button>
      </div>
    );
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <button onClick={() => navigate(-1)}>Back</button>
    </div>
  );
}

export default UserDetail;

src/pages/NotFound.jsx:

src/pages/NotFound.jsx
import { Link } from 'react-router';

function NotFound() {
  return (
    <div>
      <h1>404  page not found</h1>
      <Link to="/">Back to home</Link>
    </div>
  );
}

export default NotFound;

src/App.jsx:

src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router';
import Layout from './Layout';
import Home from './pages/Home';
import UserList from './pages/UserList';
import UserDetail from './pages/UserDetail';
import NotFound from './pages/NotFound';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="users" element={<UserList />} />
          <Route path="users/:userId" element={<UserDetail />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Save and check it in the browser.

  • Clicking the header menu changes the screen without a refresh
  • The link for the current page is bold and tomato-colored
  • Clicking a name on the user list takes you to a dynamic URL (/users/1) → the detail page
  • Clicking “Back” triggers the browser’s back navigation
  • Typing /nonexistentpath directly into the URL bar shows the 404 page

Almost everything from Parts 1 ~ 2 (component decomposition, props, useState, event handling, conditional / list rendering) is in this one site.

Exercises #

  1. Add a search feature to the mini-site above. Put a search box on the /users page and reflect the input as ?q=... in the URL using useSearchParams. Confirm that refreshing or sharing the URL restores the search state.
  2. Build a protected route. Build a simple auth flow that holds isLoggedIn with useState, and when accessing the /admin route while isLoggedIn === false, automatically redirect to /login (<Navigate to="/login" />). This is the foundation for Chapter 32 (auth and sessions).
  3. Compare React Router and the Next.js App Router. Imagine moving the mini-site above to the Next.js App Router in your head and write down briefly what the app/ directory structure would look like. e.g., app/layout.tsx, app/page.tsx, app/users/page.tsx, app/users/[userId]/page.tsx. Sketching this once before Chapters 22 ~ 23 of Part 4 helps.

In one line: an SPA handles screen transitions on the client. React Router’s core is BrowserRouter, Routes, Route, Link / NavLink. Dynamic paths use :param + useParams, programmatic navigation uses useNavigate, query parameters use useSearchParams, shared layout uses nested routes + <Outlet />, and 404 uses path="*". If SEO matters and you are building fullstack, Part 4’s Next.js App Router fits better.

Next chapter #

This chapter wraps up Part 2: Effects, State, Routing. On top of Part 1’s components / props / state / events / forms, Part 2 added useEffect / lifting state up / Context / custom hooks / performance / routing. You now have the tools to build a small SPA end to end without any library.

Part 3 begins with Chapter 16 TypeScript + React setup. We rebuild all the code so far on top of TypeScript. Typing props · hooks · events · forms · Context · API responses is covered in turn across six chapters.

X