DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

React Routing with React Router: A Practical Guide to Building SPAs

Routing is one of the core building blocks of a modern React application. It allows users to navigate between pages, share URLs, and use browser navigation without forcing the entire application to reload every time they click a link.

React Router is a popular routing library that makes this possible by connecting URL paths to React components. Whether you are building a dashboard, an e-commerce platform, or a content-driven application, understanding routing helps you create a more maintainable and user-friendly experience.

In this guide, we will explore route configuration, navigation, dynamic parameters, nested routes, and fallback pages using React Router. We will also walk through a complete JavaScript example that demonstrates how these concepts fit together in a practical application.

Understanding React Router: Routes, Navigation, and Dynamic Pages

1. What Is React Router?

React Router is a routing solution for React applications that maps URL paths to components. Instead of manually checking the browser URL and deciding which component to render, you define routes declaratively and let the router handle navigation and matching.

For a modern React application, the react-router package provides the routing APIs used in this example. Install it with npm install react-router and use BrowserRouter, Routes, and Route to configure a browser-based routing system.

2. Configuring Routes

A route connects a URL pattern to a component. For example, / can display a home page, /about can display an about page, and /products can display a product listing.

The Routes component selects the best matching route, while each Route defines its path and the element to render. A wildcard route using path="*" provides a convenient way to display a custom not-found page when no configured path matches.

3. Navigating Without Reloading

Traditional links can trigger full-page navigation, which may reload the application and discard in-memory UI state. React Router's Link component provides client-side navigation, allowing the application to update the URL and render the matching page without a full document reload.

For navigation triggered by application logic, use the useNavigate hook. For example, after completing a form or saving a record, you can navigate users to a confirmation page programmatically.

4. Working with Dynamic Parameters

Many applications need routes that depend on data, such as /products/42 or /users/ansh. React Router supports dynamic segments by defining paths such as /products/:productId.

Inside the matching component, useParams returns the values captured from the URL. You can use those values to fetch records from an API, display specific content, or select the appropriate data from a local collection. Remember to validate parameters before using them in API requests or database queries.

5. Nested Routes and Shared Layouts

Nested routing is useful when multiple pages share a common layout. An admin dashboard, for example, may contain a persistent sidebar and header while its overview, users, and settings pages change independently.

A parent route can render an Outlet where its matching child route appears. This keeps shared UI in one place and prevents repeated layout code across multiple pages.

6. A Practical Implementation

The following runnable example demonstrates the fundamental routing concepts using React and React Router. It includes navigation links, dynamic product details, a shared dashboard layout, programmatic navigation, and a fallback route.

Save the example as App.jsx in a React project that already has a configured entry point rendering App with createRoot. The example uses JSX in a JavaScript source file and requires the react and react-router packages.

import React from "react";
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  NavLink,
  Outlet,
  useNavigate,
  useParams,
} from "react-router";

// Sample data keeps this example self-contained.
const products = [
  { id: "1", name: "Mechanical Keyboard", price: 2499 },
  { id: "2", name: "Wireless Mouse", price: 999 },
  { id: "3", name: "USB-C Hub", price: 1799 },
];

function Home() {
  console.log("Step 1: Rendering the home page");
  return (
    <main>
      <h1>Welcome to the Store</h1>
      <p>Explore products using client-side navigation.</p>
      <Link to="/products">Browse products</Link>
    </main>
  );
}

function About() {
  console.log("Step 2: Rendering the about page");
  return <h1>About Our Store</h1>;
}

function ProductList() {
  console.log("Step 3: Rendering the product listing");
  return (
    <main>
      <h1>Products</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            <Link to={`/products/${product.id}`}>
              {product.name} - ₹{product.price}
            </Link>
          </li>
        ))}
      </ul>
    </main>
  );
}

function ProductDetails() {
  // Read the dynamic segment from the current URL.
  const { productId } = useParams();
  const product = products.find((item) => item.id === productId);

  console.log("Step 4: Matching product ID:", productId);

  if (!product) {
    return <h2>Product not found.</h2>;
  }

  return (
    <main>
      <h1>{product.name}</h1>
      <p>Price: ₹{product.price}</p>
      <Link to="/products">Back to products</Link>
    </main>
  );
}

function DashboardLayout() {
  console.log("Step 5: Rendering the shared dashboard layout");
  return (
    <main>
      <h1>Admin Dashboard</h1>
      <nav>
        <NavLink to="overview">Overview</NavLink>{" | "}
        <NavLink to="settings">Settings</NavLink>
      </nav>
      {/* The matching nested route renders inside this outlet. */}
      <Outlet />
    </main>
  );
}

function DashboardOverview() {
  return <p>Review application activity and performance.</p>;
}

function DashboardSettings() {
  return <p>Manage your application settings here.</p>;
}

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

  function completeCheckout() {
    console.log("Step 6: Checkout completed; navigating home");
    // Navigate programmatically after the simulated operation.
    navigate("/");
  }

  return (
    <main>
      <h1>Checkout</h1>
      <button onClick={completeCheckout}>Complete checkout</button>
    </main>
  );
}

function NotFound() {
  console.log("Step 7: No route matched; showing the fallback page");
  return (
    <main>
      <h1>404 - Page Not Found</h1>
      <Link to="/">Return home</Link>
    </main>
  );
}

function AppRoutes() {
  return (
    <>
      <header>
        <nav aria-label="Main navigation">
          <NavLink to="/">Home</NavLink>{" | "}
          <NavLink to="/about">About</NavLink>{" | "}
          <NavLink to="/products">Products</NavLink>{" | "}
          <NavLink to="/dashboard/overview">Dashboard</NavLink>{" | "}
          <NavLink to="/checkout">Checkout</NavLink>
        </nav>
      </header>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/products" element={<ProductList />} />
        <Route path="/products/:productId" element={<ProductDetails />} />
        <Route path="/checkout" element={<Checkout />} />

        {/* Child routes share the dashboard layout. */}
        <Route path="/dashboard" element={<DashboardLayout />}>
          <Route path="overview" element={<DashboardOverview />} />
          <Route path="settings" element={<DashboardSettings />} />
        </Route>

        {/* Catch unknown paths with a friendly fallback. */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </>
  );
}

export default function App() {
  console.log("Step 0: Initializing the React Router application");
  return (
    <BrowserRouter>
      <AppRoutes />
    </BrowserRouter>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

React Router gives React applications a structured way to manage navigation, URL parameters, shared layouts, and browser history. By combining Routes, Route, Link, useParams, useNavigate, and Outlet, you can build applications that remain organized as the number of pages and features grows.

For production applications, consider adding route-level loading states, error boundaries, authentication checks, and proper server configuration for SPA fallback routes. Also remember that hiding a page in the client does not secure its data; enforce authorization on your backend APIs.

Start with a small route configuration and introduce nested routes when your application develops shared layouts or more complex navigation requirements. Which React Router feature has made the biggest difference in your projects: dynamic routes, nested layouts, or programmatic navigation?

Top comments (0)