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, and NavLink.
  • 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.

  1. 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.
  2. Routes: The Route Container

    • What it is: This component is a wrapper for all your Route components. It looks at the current URL and renders the first Route that matches.
    • Why it’s important: Routes is a direct replacement for the Switch component 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 Route children and renders the one whose path prop matches the current URL.
  3. 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 path prop (the URL segment) and an element prop (the React component to render).
  4. 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 to prop, which is the path you want to navigate to.
  5. NavLink: Active Link Styling

    • What it is: A special type of Link that automatically applies an active class (or custom styling) to itself when its to prop 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 to prop, and a className prop that can be a function receiving an isActive boolean.
  6. 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 Route path) and values are the corresponding URL segments.
  7. 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 BrowserRouter from react-router-dom.
  • We wrap the entire <App /> component with <BrowserRouter>. This makes the routing context available to all components within App.

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, and NavLink from react-router-dom.
  • We also import our Home, About, and Contact components.
  • Navigation (<nav>): We create a simple navigation bar using <ul> and <li>.
  • NavLink: For each navigation item, we use NavLink. The to prop specifies the path. The className prop takes a function that receives an isActive boolean, 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 our Route definitions.
  • Route: Each Route component defines a mapping.
    • path prop: The URL path to match (e.g., /, /about).
    • element prop: The React component to render when the path matches.
  • path="*": This is a special Route that acts as a “catch-all.” It will match any path that hasn’t been matched by previous Route components. This is perfect for a 404 “Page Not Found” page. Important: The Routes component 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 useParams from react-router-dom.
  • Inside our component, useParams() is called. It returns an object where keys are the names of the dynamic segments defined in the Route path, and values are the actual values from the URL.
  • We use object destructuring (const { productId } = useParams();) to extract the productId.

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 Route with path="/products/:productId". The colon (:) before productId signifies that productId is 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’s element.
  • Profile.tsx and Settings.tsx: These are the child components that will be rendered inside the Dashboard’s Outlet.

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 Route for /dashboard with element={<Dashboard />}.
  • Crucially, inside this Route, we define other Route components. These are the nested routes.
  • path="profile" and path="settings": Notice these paths do not start with a /. This makes them relative paths to their parent (/dashboard). So, /dashboard/profile will render Profile inside Dashboard’s Outlet.
  • index prop: When a child Route has the index prop, 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 useNavigate from react-router-dom.
  • We call useNavigate() inside the component to get the navigate function.
  • handleGoHome function calls navigate('/') to send the user to the root path.
  • handleGoBack function calls navigate(-1) to go back one entry in the browser’s history stack, just like hitting the browser’s back button. You can also use navigate(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:

  1. Create a new page component called UserList.tsx that displays a simple list of users (e.g., Alice, Bob, Charlie). Each user’s name should be a Link to their individual profile page.
  2. Create another page component called UserProfile.tsx. This component should:
    • Display the userId from 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 UserList page.
  3. Add the necessary Route definitions in App.tsx for /users (to show UserList) and /users/:userId (to show UserProfile).
  4. Add a NavLink to your main navigation in App.tsx for the “Users” page.

Hint:

  • Remember to use useParams in UserProfile.tsx to get the userId.
  • Use useNavigate in UserProfile.tsx for the “Back to Users” button.
  • Ensure your dynamic route path="/users/:userId" is correctly defined.

What to Observe/Learn:

  • How to combine Link for list navigation and useParams for detail view.
  • Practical application of useNavigate for 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:

  1. Forgetting BrowserRouter:

    • Pitfall: If your routing components (Routes, Route, Link, etc.) aren’t wrapped within a BrowserRouter (or another router like HashRouter for older browsers/static sites), they won’t have the necessary context and will often throw errors like “You used a Link outside of a Router.”
    • Troubleshooting: Always ensure your top-level App or the component containing your Routes is a child of BrowserRouter (usually in main.tsx/index.js).
  2. Using <a> tags instead of Link or NavLink for 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 Link or NavLink components provided by react-router-dom. Only use <a> for external links (e.g., to Google.com).
  3. Incorrect Route order, especially with dynamic or catch-all routes:

    • Pitfall: Routes renders the first Route that 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.
  4. Forgetting Outlet for 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., Dashboard in our example) contains <Outlet /> where you want the child content to appear.
  5. Relative vs. Absolute Paths in Link/NavLink and Route:

    • Pitfall: Confusing to="/path" (absolute) with to="path" (relative). For example, if you’re on /dashboard and to="settings", it will navigate to /dashboard/settings. If you use to="/settings", it will navigate to /settings from the root, which might not be what you intend if settings is a child of dashboard.
    • 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.

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 BrowserRouter component wraps your application, providing the routing context.
  • The Routes component acts as a container for your route definitions, rendering the first matching Route.
  • Route components map specific URL paths to React elements (your page components).
  • Link and NavLink are used for declarative navigation, with NavLink offering built-in active state styling.
  • Dynamic routes (e.g., /products/:productId) allow you to create flexible URLs, and the useParams hook lets you extract these dynamic segments.
  • Nested routes enable complex layouts where parent components render child routes via the Outlet component.
  • The useNavigate hook 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


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.