← Back to Tutorials Chapter 8

Routing & Navigation in React

Client-Side Routing

React is a single-page application (SPA) library. Without routing, you can only render one view at a time. Client-side routing lets you map URL paths to components without a full page reload. The dominant library is react-router-dom (v6+).

Setting Up React Router

Install with npm install react-router-dom. The core components are:

import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

NavLink for Active States

NavLink adds a className or style callback when the link matches the current URL, making it easy to highlight the active page.

<NavLink
  to="/dashboard"
  className={({ isActive }) => (isActive ? "active" : "")}
>
  Dashboard
</NavLink>

Nested Routes

Routes can be nested to create layouts that share a common UI. Use <Outlet /> to render the child route inside a parent layout.

import { Outlet, Link } from "react-router-dom";

function DashboardLayout() {
  return (
    <div>
      <h2>Dashboard</h2>
      <nav>
        <Link to="profile">Profile</Link>
        <Link to="settings">Settings</Link>
      </nav>
      <Outlet />  {/* child routes render here */}
    </div>
  );
}

// Route definition
<Route path="dashboard" element={<DashboardLayout />}>
  <Route path="profile" element={<Profile />} />
  <Route path="settings" element={<Settings />} />
</Route>
Nested routes help you avoid repeating layout code. The parent component owns the shared chrome while Outlet injects the child content.

Dynamic Routes & useParams

Use a colon (:) in the path to define a parameter segment. The useParams hook returns an object of matched parameters.

import { useParams } from "react-router-dom";

function UserProfile() {
  const { username } = useParams();
  return <h1>Profile of {username}</h1>;
}

// Route: <Route path="/user/:username" element={<UserProfile />} />
// URL: /user/snehal  →  username = "snehal"

Programmatic Navigation with useNavigate

Sometimes you need to navigate after an event (e.g. form submission). The useNavigate hook returns a navigation function.

import { useNavigate } from "react-router-dom";

function LoginPage() {
  const navigate = useNavigate();

  const handleLogin = async () => {
    await login();
    navigate("/dashboard", { replace: true });
  };

  return <button onClick={handleLogin}>Log In</button>;
}

404 Pages (Catch-All Route)

A route with path="*" matches any undefined path. Place it last inside Routes.

<Route path="*" element={<NotFound />} />

Protected Routes (Auth Guards)

Create a wrapper component that checks authentication and either renders the child route or redirects to login.

import { Navigate } from "react-router-dom";

function ProtectedRoute({ children }) {
  const isAuth = useAuth(); // your auth hook
  return isAuth ? children : <Navigate to="/login" replace />;
}

// Usage
<Route path="/admin" element={
  <ProtectedRoute><Admin /></ProtectedRoute>
} />
React Router navigation diagram

Route Query Parameters

Use the useSearchParams hook to read and update query string parameters (e.g. ?page=2&sort=name).

import { useSearchParams } from "react-router-dom";

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const page = searchParams.get("page") || "1";

  const nextPage = () => {
    setSearchParams({ page: String(Number(page) + 1) });
  };

  return (
    <div>
      <h2>Products — Page {page}</h2>
      <button onClick={nextPage}>Next</button>
    </div>
  );
}

Lazy Loading Routes

Combine React.lazy and Suspense with React Router to code-split at the route level.

const Dashboard = React.lazy(() => import("./pages/Dashboard"));

<Routes>
  <Route path="/dashboard" element={
    <React.Suspense fallback={<div>Loading...</div>}>
      <Dashboard />
    </React.Suspense>
  } />
</Routes>
Lazy loading reduces the initial bundle size. Each route chunk is fetched only when the user navigates to that path.

Exercise: Build a Mini Wiki

  1. Set up a new React app with react-router-dom.
  2. Create routes for Home, About, and a dynamic /article/:slug page.
  3. Add a NavLink-based navigation bar with active styling.
  4. Create a ProtectedRoute that redirects to /login if a user state is null.
  5. Add a 404 catch-all page.

Challenge: Use useSearchParams to implement a search box that filters articles by title.