Welcome back, future React maestros! In our journey so far, we’ve learned how to build compelling user interfaces with components, manage state, and handle data. But what if your application grows beyond a single screen? How do you let users navigate between different “pages” or sections of your app without refreshing the entire browser? That’s where Client-Side Routing comes into play, and React Router is the undisputed champion for handling it in the React ecosystem.
In this chapter, we’re going to unlock the power of navigation within your Single-Page Applications (SPAs). You’ll learn:
- What client-side routing is and why it’s essential for modern web applications.
- How to set up and configure React Router v6 in your project.
- The core components of React Router:
BrowserRouter,Routes,Route,Link, andNavLink. - How to handle dynamic routes, allowing you to display different content based on URL parameters.
- Techniques for programmatic navigation, giving you full control over where your users go.
- How to create nested routes for complex layouts.
By the end of this chapter, you’ll be able to build multi-page user experiences that feel fast, fluid, and intuitive, just like the best web applications out there. Ready to become a navigation wizard? Let’s dive in!
Understanding Client-Side Routing
Imagine a traditional website. When you click a link, your browser makes a full request to the server, downloads a new HTML page, and then renders it. This causes a noticeable “blink” or refresh. This is called server-side routing.
Now, think about a Single-Page Application (SPA) like Gmail or Google Docs. When you click around, the content changes instantly, without a full page reload. This magic is powered by client-side routing.
What is it? Client-side routing means that your browser, not the server, takes responsibility for managing the URL and rendering the appropriate content. When the URL changes, instead of requesting a new HTML page from the server, your JavaScript application (in our case, React) intercepts the request, updates the necessary parts of the UI, and then manipulates the browser’s history to reflect the new URL.
Why does it matter for React? React is designed to build dynamic UIs. Client-side routing perfectly complements this by allowing your React components to render and re-render efficiently as the “page” changes, without losing the application’s state or forcing a full refresh. This leads to:
- Faster transitions: No full page reloads means a snappier user experience.
- Smoother user experience: The application feels more like a desktop app.
- Better performance: Only necessary data and components are loaded and updated.
React Router is the most popular library for bringing client-side routing to your React applications. It provides the tools to declare your routes, navigate between them, and access information about the current URL.
Introducing React Router v6.x
As of early 2026, React Router version 6.x is the modern, stable, and recommended way to handle routing in React. It introduced significant improvements over previous versions, making route definitions more intuitive and hook-based.
Let’s get started by setting up a fresh React project and installing React Router. We’ll use Vite for a quick and modern setup.
Step 1: Create a new React Project (if you don’t have one)
Open your terminal and run:
npm create vite@latest my-router-app -- --template react-ts
cd my-router-app
npm install
This command creates a new TypeScript React project using Vite. If you prefer JavaScript, omit --template react-ts.
Step 2: Install React Router DOM
Now, let’s add the routing library:
npm install react-router-dom@^6.x
The @^6.x ensures you get the latest stable version 6.x, which is what we’ll be focusing on.
Excellent! With React Router installed, we’re ready to explore its core components.
The Core Building Blocks of React Router
React Router provides several key components and hooks to manage your application’s navigation. Let’s look at them one by one.
BrowserRouter: The Foundation- What it is: This is the most common router for web applications. It uses the HTML5 History API (pushState, replaceState, popState) to keep your UI in sync with the URL.
- Why it’s important: You need to wrap your entire application (or at least the part that needs routing) with
BrowserRouter. It provides the routing context to all its descendants. - How it functions: It listens for URL changes and tells the rest of the React Router components what to render.
Routes: The Route Container- What it is: This component is a wrapper for all your
Routecomponents. It looks at the current URL and renders the firstRoutethat matches. - Why it’s important:
Routesis a direct replacement for theSwitchcomponent from older React Router versions. It’s smarter about matching routes, always picking the best match, even if routes are defined in a different order. - How it functions: It iterates through its
Routechildren and renders the one whosepathprop matches the current URL.
- What it is: This component is a wrapper for all your
Route: Defining a Path- What it is: This component defines a specific path in your application and tells React Router which component to render when that path is active.
- Why it’s important: This is where you declare your application’s “pages.”
- How it functions: It takes a
pathprop (the URL segment) and anelementprop (the React component to render).
Link: Navigating Without Reloads- What it is: A component that renders an
<a>tag in the DOM, but it prevents the default browser behavior of requesting a new page. - Why it’s important: This is your primary tool for navigating between different routes in your application without causing full page reloads, maintaining the SPA experience.
- How it functions: It takes a
toprop, which is the path you want to navigate to.
- What it is: A component that renders an
NavLink: Active Link Styling- What it is: A special type of
Linkthat automatically applies anactiveclass (or custom styling) to itself when itstoprop matches the current URL. - Why it’s important: Useful for navigation menus where you want to highlight the currently active page.
- How it functions: It takes a
toprop, and aclassNameprop that can be a function receiving anisActiveboolean.
- What it is: A special type of
useParams: Extracting Dynamic Data- What it is: A React Hook that allows you to access URL parameters (like an item ID in
/products/123). - Why it’s important: Essential for creating dynamic pages where content changes based on a part of the URL.
- How it functions: Returns an object where keys are the parameter names (as defined in your
Routepath) and values are the corresponding URL segments.
- What it is: A React Hook that allows you to access URL parameters (like an item ID in
useNavigate: Programmatic Navigation- What it is: A React Hook that returns a function you can use to navigate programmatically, for example, after a form submission or a button click.
- Why it’s important: When you need to trigger navigation based on an event or condition, rather than a direct link click.
- How it functions: You call the returned function with the path you want to navigate to.
Let’s see these in action!
Step-by-Step Implementation: Building a Basic Routed App
We’ll create a simple application with a Home page, an About page, and a Contact page, along with a navigation bar.
Step 1: Set up BrowserRouter in main.tsx (or index.js)
Open src/main.tsx (or src/index.js if you’re using JavaScript without TypeScript). We need to wrap our App component with BrowserRouter.
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { BrowserRouter } from 'react-router-dom'; // Import BrowserRouter
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter> {/* Wrap your App with BrowserRouter */}
<App />
</BrowserRouter>
</React.StrictMode>,
);
Explanation:
- We import
BrowserRouterfromreact-router-dom. - We wrap the entire
<App />component with<BrowserRouter>. This makes the routing context available to all components withinApp.
Step 2: Create our “Page” Components
Let’s create some simple components that will represent our different pages.
Create a new folder src/pages and add these files:
// src/pages/Home.tsx
import React from 'react';
function Home() {
return (
<div>
<h2>Welcome to the Home Page!</h2>
<p>This is the starting point of our amazing routed application.</p>
</div>
);
}
export default Home;
// src/pages/About.tsx
import React from 'react';
function About() {
return (
<div>
<h2>About Us</h2>
<p>We are learning React Router v6, and it's super cool!</p>
</div>
);
}
export default About;
// src/pages/Contact.tsx
import React from 'react';
function Contact() {
return (
<div>
<h2>Contact Us</h2>
<p>Reach out to us at learn@reactrouter.com</p>
</div>
);
}
export default Contact;
Explanation: These are just simple functional components that will be rendered when their respective routes are active.
Step 3: Define Routes and Navigation in App.tsx
Now, let’s update App.tsx to include our navigation and define the routes.
// src/App.tsx
import React from 'react';
import { Routes, Route, NavLink } from 'react-router-dom'; // Import Routes, Route, NavLink
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import './App.css'; // Assuming you have some basic styling here
function App() {
return (
<div>
<nav>
<ul>
<li>
{/* NavLink for active styling */}
<NavLink to="/" className={({ isActive }) => isActive ? 'active-link' : undefined}>
Home
</NavLink>
</li>
<li>
<NavLink to="/about" className={({ isActive }) => isActive ? 'active-link' : undefined}>
About
</NavLink>
</li>
<li>
<NavLink to="/contact" className={({ isActive }) => isActive ? 'active-link' : undefined}>
Contact
</NavLink>
</li>
</ul>
</nav>
{/* This is where our routes will be rendered */}
<div className="content">
<Routes> {/* Routes wrapper */}
<Route path="/" element={<Home />} /> {/* Home route */}
<Route path="/about" element={<About />} /> {/* About route */}
<Route path="/contact" element={<Contact />} /> {/* Contact route */}
{/* A catch-all route for unmatched paths (404) */}
<Route path="*" element={<h2>404 - Page Not Found</h2>} />
</Routes>
</div>
</div>
);
}
export default App;
Explanation:
- We import
Routes,Route, andNavLinkfromreact-router-dom. - We also import our
Home,About, andContactcomponents. - Navigation (
<nav>): We create a simple navigation bar using<ul>and<li>. NavLink: For each navigation item, we useNavLink. Thetoprop specifies the path. TheclassNameprop takes a function that receives anisActiveboolean, allowing us to conditionally apply a CSS class (active-link) when the link’s path matches the current URL.Routes: This component acts as a container for all ourRoutedefinitions.Route: EachRoutecomponent defines a mapping.pathprop: The URL path to match (e.g.,/,/about).elementprop: The React component to render when the path matches.
path="*": This is a specialRoutethat acts as a “catch-all.” It will match any path that hasn’t been matched by previousRoutecomponents. This is perfect for a 404 “Page Not Found” page. Important: TheRoutescomponent will render the first matching route, so place your*route last.
Step 4: Add some basic styling (optional but recommended)
Create or update src/App.css to add styling for our NavLink and general layout:
/* src/App.css */
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
nav ul {
list-style: none;
padding: 0;
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 2rem;
}
nav li a {
text-decoration: none;
color: #61dafb; /* React blue */
font-weight: bold;
padding: 0.5rem 1rem;
border-radius: 8px;
transition: background-color 0.3s ease;
}
nav li a:hover {
background-color: rgba(97, 218, 251, 0.1);
}
nav li a.active-link { /* The class applied by NavLink when active */
background-color: #61dafb;
color: #282c34; /* Dark text for active link */
}
.content {
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
Step 5: Run your application!
npm run dev
Open your browser to http://localhost:5173 (or whatever port Vite tells you). You should see your navigation bar and the Home page content. Click on “About” and “Contact” to see the content change instantly without a full page reload, and notice how the active link styling updates!
Dynamic Routes with useParams
Many applications need to display content that changes based on a specific identifier in the URL. Think about /products/123 or /users/john-doe. This is where dynamic routes and the useParams hook come in handy.
Step 1: Create a ProductDetail component
Let’s create a component that will display product information based on an ID.
// src/pages/ProductDetail.tsx
import React from 'react';
import { useParams } from 'react-router-dom'; // Import useParams
function ProductDetail() {
const { productId } = useParams(); // Get the productId from the URL
// In a real app, you'd fetch product data using productId
// For now, let's just display the ID
return (
<div>
<h3>Product Details</h3>
<p>You are viewing product with ID: **{productId}**</p>
<p>Imagine fetching data for product {productId} from an API here!</p>
</div>
);
}
export default ProductDetail;
Explanation:
- We import
useParamsfromreact-router-dom. - Inside our component,
useParams()is called. It returns an object where keys are the names of the dynamic segments defined in theRoutepath, and values are the actual values from the URL. - We use object destructuring (
const { productId } = useParams();) to extract theproductId.
Step 2: Add a dynamic Route in App.tsx
Now, let’s tell React Router about our new dynamic path.
// src/App.tsx (partial update)
import ProductDetail from './pages/ProductDetail'; // Import ProductDetail
function App() {
return (
<div>
<nav>
{/* ... existing NavLink for Home, About, Contact */}
<li>
{/* Example of a Link to a dynamic product, hardcoded for now */}
<NavLink to="/products/1" className={({ isActive }) => isActive ? 'active-link' : undefined}>
Product 1
</NavLink>
</li>
<li>
<NavLink to="/products/2" className={({ isActive }) => isActive ? 'active-link' : undefined}>
Product 2
</NavLink>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
{/* New dynamic route for product details */}
<Route path="/products/:productId" element={<ProductDetail />} /> {/* Notice the :productId */}
<Route path="*" element={<h2>404 - Page Not Found</h2>} />
</Routes>
</div>
</div>
);
}
export default App;
Explanation:
- We added a new
Routewithpath="/products/:productId". The colon (:) beforeproductIdsignifies thatproductIdis a dynamic segment or URL parameter. - We also added a couple of
NavLinks to demonstrate navigation to these dynamic routes.
Now, if you navigate to /products/1 or /products/awesome-widget in your browser, the ProductDetail component will render, and useParams will correctly extract “1” or “awesome-widget” as the productId. How cool is that?
Nested Routes with Outlet
Sometimes, you have layouts where certain parts of the UI remain constant while inner sections change. For example, a dashboard might have a sidebar that’s always there, but the main content area switches between “Profile” and “Settings.” React Router handles this beautifully with nested routes and the Outlet component.
Step 1: Create Dashboard, Profile, and Settings components
// src/pages/Dashboard.tsx
import React from 'react';
import { NavLink, Outlet } from 'react-router-dom'; // Import NavLink and Outlet
function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
<nav>
<ul style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
<li>
<NavLink to="profile" className={({ isActive }) => isActive ? 'active-link' : undefined}>
Profile
</NavLink>
</li>
<li>
<NavLink to="settings" className={({ isActive }) => isActive ? 'active-link' : undefined}>
Settings
</NavLink>
</li>
</ul>
</nav>
<div style={{ border: '1px dashed grey', padding: '15px', marginTop: '15px' }}>
{/* This is where nested routes will render */}
<Outlet />
</div>
</div>
);
}
export default Dashboard;
// src/pages/Profile.tsx
import React from 'react';
function Profile() {
return (
<div>
<h3>User Profile</h3>
<p>Manage your profile information here.</p>
</div>
);
}
export default Profile;
// src/pages/Settings.tsx
import React from 'react';
function Settings() {
return (
<div>
<h3>App Settings</h3>
<p>Adjust your application settings here.</p>
</div>
);
}
export default Settings;
Explanation:
Dashboard.tsx: This component acts as the parent layout. It has its own navigation (NavLinks) for its sub-sections. The crucial part is<Outlet />. This is a placeholder where React Router will render the matching child route’selement.Profile.tsxandSettings.tsx: These are the child components that will be rendered inside theDashboard’sOutlet.
Step 2: Define nested routes in App.tsx
We define the Dashboard route, and then its children routes inside it.
// src/App.tsx (partial update)
import Dashboard from './pages/Dashboard'; // Import Dashboard
import Profile from './pages/Profile'; // Import Profile
import Settings from './pages/Settings'; // Import Settings
function App() {
return (
<div>
<nav>
{/* ... existing NavLink for Home, About, Contact, Products */}
<li>
<NavLink to="/dashboard" className={({ isActive }) => isActive ? 'active-link' : undefined}>
Dashboard
</NavLink>
</li>
</ul>
</nav>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/products/:productId" element={<ProductDetail />} />
{/* Nested Route for Dashboard */}
<Route path="/dashboard" element={<Dashboard />}>
{/* Child routes for Dashboard. Notice no leading slash for relative paths */}
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
{/* Optional: Default child route for dashboard */}
<Route index element={<Profile />} /> {/* Renders Profile when at /dashboard */}
</Route>
<Route path="*" element={<h2>404 - Page Not Found</h2>} />
</Routes>
</div>
</div>
);
}
export default App;
Explanation:
- We add a new
Routefor/dashboardwithelement={<Dashboard />}. - Crucially, inside this
Route, we define otherRoutecomponents. These are the nested routes. path="profile"andpath="settings": Notice these paths do not start with a/. This makes them relative paths to their parent (/dashboard). So,/dashboard/profilewill renderProfileinsideDashboard’sOutlet.indexprop: When a childRoutehas theindexprop, it means that component will be rendered when the parent path (/dashboard) is matched exactly, without any further sub-paths. This is great for a default view.
Now, navigate to /dashboard. You’ll see the Dashboard layout with the Profile content rendered inside its Outlet. Click “Settings” to switch to the Settings view. This demonstrates a powerful way to manage complex UI layouts.
Programmatic Navigation with useNavigate
Sometimes, you need to navigate to a different route not by clicking a Link, but based on some logic – perhaps after a form submission, an API call, or a specific user action. The useNavigate hook allows you to do exactly this.
Let’s add a “Go to Home” button to our ProductDetail component.
// src/pages/ProductDetail.tsx (updated)
import React from 'react';
import { useParams, useNavigate } from 'react-router-dom'; // Import useNavigate
function ProductDetail() {
const { productId } = useParams();
const navigate = useNavigate(); // Get the navigate function
const handleGoHome = () => {
// Navigate to the home page
navigate('/');
// You can also pass a second argument for state:
// navigate('/success', { state: { message: 'Product saved!' } });
};
const handleGoBack = () => {
// Navigate back one step in the history stack
navigate(-1);
};
return (
<div>
<h3>Product Details</h3>
<p>You are viewing product with ID: **{productId}**</p>
<p>Imagine fetching data for product {productId} from an API here!</p>
<button onClick={handleGoHome} style={{ marginRight: '10px' }}>Go to Home</button>
<button onClick={handleGoBack}>Go Back</button>
</div>
);
}
export default ProductDetail;
Explanation:
- We import
useNavigatefromreact-router-dom. - We call
useNavigate()inside the component to get thenavigatefunction. handleGoHomefunction callsnavigate('/')to send the user to the root path.handleGoBackfunction callsnavigate(-1)to go back one entry in the browser’s history stack, just like hitting the browser’s back button. You can also usenavigate(1)to go forward.
Now, visit a product detail page (e.g., /products/1) and click the “Go to Home” button. You’ll be programmatically redirected to the home page!
Mini-Challenge: User Profiles
Alright, your turn! Let’s solidify your understanding of dynamic routes and programmatic navigation.
Challenge:
- Create a new page component called
UserList.tsxthat displays a simple list of users (e.g., Alice, Bob, Charlie). Each user’s name should be aLinkto their individual profile page. - Create another page component called
UserProfile.tsx. This component should:- Display the
userIdfrom the URL parameters (e.g., if the URL is/users/alice, it should show “Viewing profile for: alice”). - Include a button that, when clicked, programmatically navigates the user back to the
UserListpage.
- Display the
- Add the necessary
Routedefinitions inApp.tsxfor/users(to showUserList) and/users/:userId(to showUserProfile). - Add a
NavLinkto your main navigation inApp.tsxfor the “Users” page.
Hint:
- Remember to use
useParamsinUserProfile.tsxto get theuserId. - Use
useNavigateinUserProfile.tsxfor the “Back to Users” button. - Ensure your dynamic route
path="/users/:userId"is correctly defined.
What to Observe/Learn:
- How to combine
Linkfor list navigation anduseParamsfor detail view. - Practical application of
useNavigatefor returning to a parent list. - Reinforcement of defining dynamic routes.
Take your time, try it out, and don’t peek at solutions! If you get stuck, that’s part of the learning process.
Common Pitfalls & Troubleshooting
Even with a powerful library like React Router, it’s easy to stumble into common issues. Here are a few to watch out for:
Forgetting
BrowserRouter:- Pitfall: If your routing components (
Routes,Route,Link, etc.) aren’t wrapped within aBrowserRouter(or another router likeHashRouterfor older browsers/static sites), they won’t have the necessary context and will often throw errors like “You used aLinkoutside of aRouter.” - Troubleshooting: Always ensure your top-level
Appor the component containing yourRoutesis a child ofBrowserRouter(usually inmain.tsx/index.js).
- Pitfall: If your routing components (
Using
<a>tags instead ofLinkorNavLinkfor internal navigation:- Pitfall: While
<a>tags work, they cause a full page refresh, defeating the purpose of client-side routing and SPAs. - Troubleshooting: For all internal navigation within your React application, use
LinkorNavLinkcomponents provided byreact-router-dom. Only use<a>for external links (e.g., to Google.com).
- Pitfall: While
Incorrect
Routeorder, especially with dynamic or catch-all routes:- Pitfall:
Routesrenders the firstRoutethat matches the current URL. If you place a broad route (like/or/*) before a more specific one (like/products/:id), the broad route might match first, preventing the specific one from ever being reached. - Troubleshooting: Always place your most specific routes first, followed by more general ones. The
path="*"(404) route should always be the last one defined.
- Pitfall:
Forgetting
Outletfor nested routes:- Pitfall: When you define nested routes, the child components won’t render unless their parent component includes an
<Outlet />component. - Troubleshooting: If your child routes aren’t showing up, double-check that the parent route’s component (e.g.,
Dashboardin our example) contains<Outlet />where you want the child content to appear.
- Pitfall: When you define nested routes, the child components won’t render unless their parent component includes an
Relative vs. Absolute Paths in
Link/NavLinkandRoute:- Pitfall: Confusing
to="/path"(absolute) withto="path"(relative). For example, if you’re on/dashboardandto="settings", it will navigate to/dashboard/settings. If you useto="/settings", it will navigate to/settingsfrom the root, which might not be what you intend ifsettingsis a child ofdashboard. - Troubleshooting: For top-level navigation, use absolute paths (starting with
/). For navigation within a nested route’s context, use relative paths (no leading/) unless you explicitly want to jump out of the current nested context.
- Pitfall: Confusing
Summary
Phew! You’ve just mastered a critical piece of modern web development: client-side routing with React Router. Let’s recap what we’ve covered:
- Client-side routing allows your React app to manage URL changes and render new content without full page reloads, creating a fast and fluid user experience.
- React Router v6.x is the go-to library for implementing this in React.
- The
BrowserRoutercomponent wraps your application, providing the routing context. - The
Routescomponent acts as a container for your route definitions, rendering the first matchingRoute. Routecomponents map specific URLpaths to Reactelements (your page components).LinkandNavLinkare used for declarative navigation, withNavLinkoffering built-in active state styling.- Dynamic routes (e.g.,
/products/:productId) allow you to create flexible URLs, and theuseParamshook lets you extract these dynamic segments. - Nested routes enable complex layouts where parent components render child routes via the
Outletcomponent. - The
useNavigatehook provides programmatic control over navigation, useful for redirects after actions.
You now have the tools to build sophisticated, multi-page React applications that feel snappy and professional. This is a huge step towards building production-ready apps!
In the next chapter, we’ll shift our focus to Forms and Validation, learning how to capture user input reliably and ensure its correctness, which often goes hand-in-hand with navigation and data submission.
References
- React Router Official Documentation: https://reactrouter.com/en/main
- React.dev - Learn React: https://react.dev/learn
- MDN Web Docs - History API: https://developer.mozilla.org/en-US/docs/Web/API/History_API
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.