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.
Installing React Router #
Add React Router to the Vite project we have been using.
pnpm add react-routerThis 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.
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 (:).
<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.
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.
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.
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.
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users/:userId" element={<UserProfile />} />
</Route>
</Routes>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>.
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.
| Item | React Router (v7) | Next.js App Router |
|---|---|---|
| Defining a route | <Route path> component tree | Filesystem (app/users/[userId]/page.tsx) |
| Dynamic parameter | :userId + useParams | [userId] folder + params prop |
| Layout | <Outlet /> + nested routes | app/layout.tsx (auto-nested) |
| Data fetching | useEffect inside the component (or v7’s loader API) | Direct fetch in the body of a Server Component function |
| SSR | Optional (Framework Mode requires extra setup) | Default |
| Build output | Client SPA | RSC + client component mix |
| Learning curve | Relatively simple | Need to learn App Router + the RSC model |
| Suited for | SPAs you can ship fast, client-only tools / dashboards | Services 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:
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:
function Home() {
return (
<div>
<h1>Home</h1>
<p>A React Router mini-site.</p>
</div>
);
}
export default Home;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:
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:
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:
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
/nonexistentpathdirectly 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 #
- Add a search feature to the mini-site above. Put a search box on the
/userspage and reflect the input as?q=...in the URL usinguseSearchParams. Confirm that refreshing or sharing the URL restores the search state. - Build a protected route. Build a simple auth flow that holds
isLoggedInwithuseState, and when accessing the/adminroute whileisLoggedIn === false, automatically redirect to/login(<Navigate to="/login" />). This is the foundation for Chapter 32 (auth and sessions). - 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 usesuseNavigate, query parameters useuseSearchParams, shared layout uses nested routes +<Outlet />, and 404 usespath="*". 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.