Routing with React Router
Give your app real pages and URLs, like /books/2 and /add, without full page reloads.
So far your Reading List is one screen. Real apps have several: a list, a detail page for each book, a form to add one. Users expect each of those to have its own URL, so they can bookmark it, share it, refresh it, and use the browser's back button.
React itself has no opinion about URLs. A router library fills that gap. It watches the URL and decides which components to show. The most widely used one is React Router, which is what you will use today. This course uses React Router version 7. You install it with npm install react-router@7 and import everything from 'react-router' (older tutorials import from react-router-dom, which version 7 no longer needs).
Your first routes
Here is the smallest useful example. Click the links and watch the content change.
Four pieces work together:
- A router component wraps the app and keeps track of the current location.
<Routes>looks at the current location and renders the first<Route>whosepathmatches.<Route path="/about" element={<About />} />says "at/about, showAbout".<Link to="/about">renders a normal<a>tag, but when clicked it updates the URL and re-renders without asking the server for a new page.
That last point is what client-side routing means. The browser loads your app once. After that, moving between "pages" is only React swapping components, which is fast and keeps all your state alive.
In your real app you use <BrowserRouter>, which reads and writes the browser's address bar. The sandboxes on this page use <MemoryRouter>, which keeps the location in memory instead, because a sandbox preview has no address bar of its own. Everything else is identical. When you add routing to your own project at the end of today, you will use BrowserRouter.
Layouts with Outlet and NavLink
Most pages share a header and navigation. Instead of repeating them, you write a layout route: a parent Route whose element renders the shared parts plus an <Outlet />. The outlet is the slot where the matching child route appears.
New things in this example:
<Route element={<Layout />}>has nopath. It only wraps its children in the layout.<Route index ...>is the index route: what shows in the outlet when the URL matches the parent exactly, here/.- Child paths like
"add"are relative to the parent, so they match/add. path="*"matches anything that nothing else matched, which is how you show a "not found" page.NavLinkis aLinkthat knows whether it is active. It adds the classactiveautomatically, which the CSS styles. Theendprop on the/link means "only active on exactly/", otherwise it would match every URL.
URL parameters with useParams
A detail page needs to know which book to show. You put the id in the URL, and declare that part of the path as a dynamic segment with a colon: books/:id. Now /books/1, /books/2 and /books/abc all match, and the component reads the actual value with useParams().
Two details matter here:
- URL parameters are always strings. That is one reason the books use string ids like
'2'. - TypeScript types
idasstring | undefined, because the router cannot prove at compile time that the parameter exists. Comparing withb.id === idhandles that safely: ifidisundefined, nothing matches, and you show the "not found" message.
Navigating from code with useNavigate
Links cover clicks. Sometimes you need to change the page after something happens, such as sending the user back to the list once they submit a form. The useNavigate hook returns a navigate function for that.
Notice that titles lives in App, above the router's routes, so it survives moving between pages. In the checkpoint you will do the same with BooksProvider: wrap the routes in it, and every page can reach the books through context.
Deep diveDeclarative mode and data mode
What you used today is React Router's declarative mode: you describe routes as JSX inside <BrowserRouter>. React Router also has a data mode, where you create the router as an object and get extra features such as loader functions that fetch data before a page renders:
import { createBrowserRouter, RouterProvider } from 'react-router';
const router = createBrowserRouter([
{
path: '/',
Component: Layout,
children: [
{ index: true, Component: BookListPage },
{ path: 'books/:id', Component: BookDetailPage },
{ path: 'add', Component: AddBookPage },
],
},
]);
createRoot(document.getElementById('root')!).render(<RouterProvider router={router} />);There is also a full framework mode with file-based routes and server rendering. Declarative mode is the easiest place to start, and moving to data mode later mostly means moving your route list into createBrowserRouter. On Day 7 you will meet Next.js, which takes the framework approach.
A friendly 404 with a button
The app below has a list page and a detail page, and a link to a page that does not exist. Add a catch-all route that renders a NotFound page. It should say the page was not found and have a button (not a link) labeled "Go to my books" that uses useNavigate to go to /.
Show solution
import { MemoryRouter, Routes, Route, Link, useNavigate } from 'react-router';
function BookListPage() {
return (
<>
<h2>My books</h2>
<Link to="/stats">See reading stats</Link>
</>
);
}
function NotFound() {
const navigate = useNavigate();
return (
<>
<h2>Page not found</h2>
<p>There is nothing at this address.</p>
<button onClick={() => navigate('/')}>Go to my books</button>
</>
);
}
export default function App() {
return (
<MemoryRouter>
<Routes>
<Route path="/" element={<BookListPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</MemoryRouter>
);
}path="*" matches any location that no other route claimed. useNavigate only works inside a router, which is fine here because NotFound is rendered by <Routes> inside <MemoryRouter>. In a real app you could also write navigate(-1) to go back one step in history.
One of your routes has the path books/:id and renders BookDetailPage. The user visits /books/42. What does useParams() return inside BookDetailPage?
Why should you use Link instead of a plain a tag for pages inside your app?
Day 5 progress