DEV Community

Cover image for React Mastery Series — Part 10: React Router — Routing, Nested Routes & Navigation
Hamed Farazi
Hamed Farazi

Posted on

React Mastery Series — Part 10: React Router — Routing, Nested Routes & Navigation

So far, we've built React applications using components, props, state, Hooks, and TypeScript.

But most real-world applications have more than one page.

Think about an e-commerce application:

  • / — Home page
  • /products — Product listing
  • /products/42 — Product details
  • /cart — Shopping cart
  • /login — Login page
  • /dashboard — User dashboard

How can we navigate between these pages without reloading the entire application?

The answer is React Router.

React Router is a routing library that allows React applications to display different components based on the current URL.

In this guide, we'll learn how to configure React Router, create dynamic routes, build nested layouts, protect routes, navigate programmatically, and handle common routing scenarios.

Let's get started!


1. What Is React Router?

React Router connects URLs to React components.

For example:

/                 → HomePage
/about            → AboutPage
/products         → ProductsPage
/products/42      → ProductDetailsPage
/contact          → ContactPage
Enter fullscreen mode Exit fullscreen mode

When a user visits a URL, React Router matches it against your route definitions and renders the appropriate component.

Without a routing library, you would need to manually manage URL changes, browser history, and conditional rendering.

React Router handles these responsibilities for you.

Why Use React Router?

Some important benefits include:

  • Client-side navigation without full-page reloads.
  • Dynamic route parameters.
  • Nested routes and shared layouts.
  • Browser back and forward navigation.
  • URL-based application state.
  • Navigation links and redirects.
  • Route-level loading and error handling.

2. Installing React Router

For a new React application, you can start with Vite:

npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
Enter fullscreen mode Exit fullscreen mode

Install React Router:

npm install react-router
Enter fullscreen mode Exit fullscreen mode

For current React Router declarative examples, we'll import the APIs from react-router.

If you're maintaining an older project that uses react-router-dom, check its version-specific documentation before mixing APIs. React Router's package organization has evolved across major versions.


3. Creating Your First Routes

Let's create a basic application with three pages:

  • Home
  • About
  • Contact

First, create the page components.

HomePage.jsx

function HomePage() {
  return <h1>Welcome to the Home Page</h1>;
}

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

AboutPage.jsx

function AboutPage() {
  return <h1>About Us</h1>;
}

export default AboutPage;
Enter fullscreen mode Exit fullscreen mode

ContactPage.jsx

function ContactPage() {
  return <h1>Contact Us</h1>;
}

export default ContactPage;
Enter fullscreen mode Exit fullscreen mode

Now configure the router in App.jsx:

import {
  BrowserRouter,
  Routes,
  Route
} from "react-router";

import HomePage from "./pages/HomePage";
import AboutPage from "./pages/AboutPage";
import ContactPage from "./pages/ContactPage";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/contact" element={<ContactPage />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Let's understand the main components.

BrowserRouter

BrowserRouter enables client-side routing using the browser's History API.

It provides routing context to the components inside it.

Routes

Routes contains your route definitions.

React Router matches the current URL against the available routes.

Route

Each Route connects a URL pattern to a React element.

For example:

<Route path="/about" element={<AboutPage />} />
Enter fullscreen mode Exit fullscreen mode

When the URL matches /about, React Router renders AboutPage.


4. Navigation with Link

You could use a normal HTML anchor:

<a href="/about">About</a>
Enter fullscreen mode Exit fullscreen mode

However, a regular anchor performs a full browser navigation.

For client-side navigation, use React Router's Link component.

import { Link } from "react-router";

function Navbar() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/contact">Contact</Link>
    </nav>
  );
}

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

The Link component changes the URL and lets React Router render the matching route without a full-page reload.

Active Navigation with NavLink

Sometimes you want to style the current navigation item.

Use NavLink:

import { NavLink } from "react-router";

function Navbar() {
  return (
    <nav>
      <NavLink
        to="/"
        className={({ isActive }) =>
          isActive ? "active" : ""
        }
        end
      >
        Home
      </NavLink>

      <NavLink to="/about">
        About
      </NavLink>

      <NavLink to="/contact">
        Contact
      </NavLink>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

NavLink provides an isActive value that tells you whether the link matches the current route.

The end prop makes the home link match only the exact / route.


5. Dynamic Routes and URL Parameters

Real applications often need dynamic URLs.

For example:

/products/1
/products/2
/products/3
Enter fullscreen mode Exit fullscreen mode

Creating a separate route for every product would be impractical.

Instead, define a dynamic route:

<Route
  path="/products/:productId"
  element={<ProductDetails />}
/>
Enter fullscreen mode Exit fullscreen mode

The :productId section represents a URL parameter.

Now use useParams to access it:

import { useParams } from "react-router";

function ProductDetails() {
  const { productId } = useParams();

  return (
    <div>
      <h1>Product Details</h1>
      <p>Product ID: {productId}</p>
    </div>
  );
}

export default ProductDetails;
Enter fullscreen mode Exit fullscreen mode

When the user visits:

/products/42
Enter fullscreen mode Exit fullscreen mode

The component receives:

productId === "42"
Enter fullscreen mode Exit fullscreen mode

Remember that URL parameters are strings, even when they contain numbers.

If you need a number, validate and convert it:

const id = Number(productId);

if (!Number.isInteger(id)) {
  return <p>Invalid product ID</p>;
}
Enter fullscreen mode Exit fullscreen mode

6. Using URL Parameters to Fetch Data

Let's make the product details page more realistic.

import { useEffect, useState } from "react";
import { useParams } from "react-router";

function ProductDetails() {
  const { productId } = useParams();

  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    const controller = new AbortController();

    async function loadProduct() {
      try {
        setLoading(true);
        setError("");

        const response = await fetch(
          `/api/products/${productId}`,
          { signal: controller.signal }
        );

        if (!response.ok) {
          throw new Error("Product not found");
        }

        const data = await response.json();

        setProduct(data);
      } catch (err) {
        if (err.name !== "AbortError") {
          setError(err.message || "Something went wrong");
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    }

    loadProduct();

    return () => controller.abort();
  }, [productId]);

  if (loading) return <p>Loading product...</p>;
  if (error) return <p>{error}</p>;
  if (!product) return <p>Product not found.</p>;

  return (
    <article>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <p>{product.description}</p>
    </article>
  );
}

export default ProductDetails;
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • Reading a dynamic URL parameter.
  • Fetching data based on that parameter.
  • Displaying loading and error states.
  • Cancelling outdated requests when the route changes or the component unmounts.

For more complex applications, React Router's data APIs or a data-fetching library can handle these responsibilities more cleanly.


7. Nested Routes

Nested routes are useful when several pages share the same layout.

Imagine a dashboard with:

/dashboard
/dashboard/profile
/dashboard/settings
/dashboard/orders
Enter fullscreen mode Exit fullscreen mode

All these pages should share a sidebar and header.

Instead of repeating the layout in every page, define a parent route.

DashboardLayout.jsx

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

function DashboardLayout() {
  return (
    <div className="dashboard">
      <aside>
        <h2>Dashboard</h2>

        <nav>
          <Link to="/dashboard">Overview</Link>
          <Link to="/dashboard/profile">Profile</Link>
          <Link to="/dashboard/settings">Settings</Link>
          <Link to="/dashboard/orders">Orders</Link>
        </nav>
      </aside>

      <main>
        <Outlet />
      </main>
    </div>
  );
}

export default DashboardLayout;
Enter fullscreen mode Exit fullscreen mode

The important part is:

<Outlet />
Enter fullscreen mode Exit fullscreen mode

Outlet renders the matching child route inside the parent layout.

Now configure the nested routes:

import {
  BrowserRouter,
  Routes,
  Route
} from "react-router";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route
          path="/dashboard"
          element={<DashboardLayout />}
        >
          <Route
            index
            element={<DashboardHome />}
          />

          <Route
            path="profile"
            element={<ProfilePage />}
          />

          <Route
            path="settings"
            element={<SettingsPage />}
          />

          <Route
            path="orders"
            element={<OrdersPage />}
          />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}
Enter fullscreen mode Exit fullscreen mode

Notice that child paths don't begin with /.

React Router combines them with the parent path.

For example:

Parent: /dashboard
Child:  profile

Result: /dashboard/profile
Enter fullscreen mode Exit fullscreen mode

What Is an Index Route?

An index route is the default child page rendered when the parent route matches exactly.

<Route index element={<DashboardHome />} />
Enter fullscreen mode Exit fullscreen mode

This means DashboardHome appears at /dashboard.


8. Programmatic Navigation with useNavigate

Sometimes navigation needs to happen after an action.

For example:

  • After a successful login.
  • After submitting a form.
  • After deleting an item.
  • After completing a checkout.

Use the useNavigate Hook.

import { useNavigate } from "react-router";

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

  function handleLogin() {
    // Perform authentication here.

    navigate("/dashboard");
  }

  return (
    <button onClick={handleLogin}>
      Login
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Replace the Current History Entry

You can replace the current history entry:

navigate("/dashboard", { replace: true });
Enter fullscreen mode Exit fullscreen mode

This is useful when users shouldn't return to the previous page using the browser's back button, such as after certain authentication transitions.

Navigate Back

navigate(-1);
Enter fullscreen mode Exit fullscreen mode

This moves back one entry in the browser history.

Be careful: the user may not have a previous application page to return to.


9. Passing State During Navigation

Sometimes you need to pass temporary data to another route.

For example:

import { useNavigate } from "react-router";

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

  function openProduct() {
    navigate("/products/42", {
      state: {
        from: "featured-products"
      }
    });
  }

  return (
    <button onClick={openProduct}>
      View Product
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Read the state in the destination component:

import { useLocation } from "react-router";

function ProductDetails() {
  const location = useLocation();

  const source = location.state?.from;

  return (
    <div>
      <h1>Product Details</h1>
      <p>Navigation source: {source || "Direct visit"}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Navigation state is useful for temporary navigation context.

However, it is not a replacement for URL parameters or persistent storage.

A user may open the destination URL directly, in which case the navigation state may not exist.


10. Working with Query Parameters

URL parameters and query parameters are different.

A route parameter is part of the route path:

/products/42
Enter fullscreen mode Exit fullscreen mode

A query parameter appears after ?:

/products?category=electronics&sort=price
Enter fullscreen mode Exit fullscreen mode

Use useSearchParams to read and update query parameters.

import { useSearchParams } from "react-router";

function ProductsPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  const category = searchParams.get("category") || "all";
  const sort = searchParams.get("sort") || "default";

  function updateCategory(event) {
    setSearchParams({
      category: event.target.value,
      sort
    });
  }

  return (
    <div>
      <h1>Products</h1>

      <select
        value={category}
        onChange={updateCategory}
      >
        <option value="all">All</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>

      <p>Current sort: {sort}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Query parameters are particularly useful for:

  • Search filters.
  • Sorting.
  • Pagination.
  • Category selection.
  • Shareable application states.

Because they're part of the URL, users can bookmark or share the current filtered view.


11. Protected Routes

Many applications have pages that should only be available to authenticated users.

Examples include:

  • Dashboard.
  • Account settings.
  • Order history.
  • Admin pages.

A simple protected route component can check whether the user is authenticated.

import { Navigate, Outlet } from "react-router";

function ProtectedRoute({ isAuthenticated }) {
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  return <Outlet />;
}
Enter fullscreen mode Exit fullscreen mode

Configure it:

<Routes>
  <Route path="/login" element={<LoginPage />} />

  <Route element={<ProtectedRoute isAuthenticated={true} />}>
    <Route path="/dashboard" element={<DashboardPage />} />
    <Route path="/settings" element={<SettingsPage />} />
  </Route>
</Routes>
Enter fullscreen mode Exit fullscreen mode

In a real application, isAuthenticated should come from your authentication system or application state.

You might also want to preserve the page the user originally requested:

function ProtectedRoute({ isAuthenticated }) {
  const location = useLocation();

  if (!isAuthenticated) {
    return (
      <Navigate
        to="/login"
        state={{ from: location }}
        replace
      />
    );
  }

  return <Outlet />;
}
Enter fullscreen mode Exit fullscreen mode

After login, you can navigate back to the requested page.

Important Security Note

A protected React route is only a user-interface restriction.

It does not secure your backend.

Your API must independently verify authentication and authorization for every protected operation. Never rely on client-side route checks to protect sensitive data.


12. Handling 404 Pages

What happens when a user visits a URL that doesn't match any route?

You should show a custom not-found page.

Create a component:

function NotFoundPage() {
  return (
    <div>
      <h1>404</h1>
      <p>Sorry, the page you're looking for doesn't exist.</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Add a wildcard route:

<Routes>
  <Route path="/" element={<HomePage />} />
  <Route path="/about" element={<AboutPage />} />
  <Route path="/contact" element={<ContactPage />} />

  <Route path="*" element={<NotFoundPage />} />
</Routes>
Enter fullscreen mode Exit fullscreen mode

The wildcard path matches URLs that don't match another route.

This creates a better user experience than leaving users on a blank page.


13. Layout Routes

A layout route allows several pages to share a common interface.

For example, an application might have:

  • A public layout with a navbar and footer.
  • An authenticated layout with a sidebar.
  • An admin layout with administrative navigation.

A public layout could look like this:

import { Outlet } from "react-router";

function MainLayout() {
  return (
    <>
      <header>
        <h1>My Application</h1>
      </header>

      <main>
        <Outlet />
      </main>

      <footer>
        <p>Copyright 2026</p>
      </footer>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Configure it:

<Routes>
  <Route element={<MainLayout />}>
    <Route path="/" element={<HomePage />} />
    <Route path="/about" element={<AboutPage />} />
    <Route path="/contact" element={<ContactPage />} />
  </Route>
</Routes>
Enter fullscreen mode Exit fullscreen mode

The layout remains mounted while the child route changes, allowing shared UI to stay consistent.


14. React Router with TypeScript

If you're using TypeScript, you can type route parameters and component props.

For example:

import { useParams } from "react-router";

function ProductDetails() {
  const { productId } = useParams<"productId">();

  if (!productId) {
    return <p>Product ID is missing.</p>;
  }

  return <h1>Product ID: {productId}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

The generic parameter describes the name of the route parameter. It does not guarantee that the URL contains a valid product ID.

You still need to validate and convert it when necessary:

const id = Number(productId);

if (!Number.isInteger(id) || id <= 0) {
  return <p>Invalid product ID.</p>;
}
Enter fullscreen mode Exit fullscreen mode

For navigation state, define an explicit type:

type NavigationState = {
  from?: string;
};
Enter fullscreen mode Exit fullscreen mode

Then validate or narrow the state when reading it, because navigation state may be absent or may not have the shape you expect.


15. Data Routers: Loaders and Actions

React Router also provides a data-router approach.

Instead of fetching data inside every component with useEffect, you can define a loader for a route.

A loader runs as part of route navigation and provides data to the route component.

Example:

import {
  createBrowserRouter,
  RouterProvider,
  useLoaderData
} from "react-router";

async function productLoader({ params }) {
  const response = await fetch(
    `/api/products/${params.productId}`
  );

  if (!response.ok) {
    throw new Response("Product not found", {
      status: 404
    });
  }

  return response.json();
}

function ProductDetails() {
  const product = useLoaderData();

  return (
    <article>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

Create the router:

const router = createBrowserRouter([
  {
    path: "/products/:productId",
    element: <ProductDetails />,
    loader: productLoader
  }
]);

function App() {
  return <RouterProvider router={router} />;
}
Enter fullscreen mode Exit fullscreen mode

This is a simplified example. In production, validate the loader's external data and handle route errors appropriately.

What Are Actions?

Actions handle mutations associated with route navigation, such as:

  • Creating a product.
  • Updating a profile.
  • Submitting a form.
  • Deleting an item.

They can work with React Router's <Form> component and route data APIs.

A simplified action:

async function contactAction({ request }) {
  const formData = await request.formData();

  const email = formData.get("email");

  const response = await fetch("/api/contact", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ email })
  });

  if (!response.ok) {
    throw new Response("Unable to submit form", {
      status: response.status
    });
  }

  return { success: true };
}
Enter fullscreen mode Exit fullscreen mode

Data routers are especially useful when your application has complex route-based data loading and mutations.


16. Handling Route Errors

Data routers support route-level error boundaries.

You can define an ErrorBoundary for a route:

import { isRouteErrorResponse, useRouteError } from "react-router";

function RouteErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status}</h1>
        <p>{error.statusText}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>Something went wrong</h1>
      <p>Please try again later.</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Attach it to a route:

{
  path: "/products/:productId",
  loader: productLoader,
  element: <ProductDetails />,
  ErrorBoundary: RouteErrorBoundary
}
Enter fullscreen mode Exit fullscreen mode

This lets you show useful error UI when a loader or route rendering fails.

For production applications, you should also log unexpected errors and provide a way for users to recover.


17. Common React Router Mistakes

Here are some mistakes worth avoiding.

Mistake 1: Using Anchor Tags for Every Internal Link

Avoid:

<a href="/about">About</a>
Enter fullscreen mode Exit fullscreen mode

Prefer:

<Link to="/about">About</Link>
Enter fullscreen mode Exit fullscreen mode

Use regular anchors for external URLs or situations where a full document navigation is intended.

Mistake 2: Forgetting Outlet in Nested Layouts

If a parent route uses child routes but doesn't render <Outlet />, the child content won't appear inside that layout.

Mistake 3: Treating URL Parameters as Numbers

Remember:

const { productId } = useParams();
Enter fullscreen mode Exit fullscreen mode

Returns a string or undefined, not a number.

Validate and convert the value before using it as a numeric identifier.

Mistake 4: Using Navigation State as Permanent Data

Navigation state is temporary context.

Use route parameters, query parameters, or persistent storage when data needs to survive direct visits or page reloads.

Mistake 5: Treating Protected Routes as Backend Security

Client-side route protection is not authentication security.

Your backend must verify permissions independently.

Mistake 6: Forgetting Server Configuration

In production, a client-side application may need server configuration so that direct requests to routes such as /products/42 return the application's entry HTML.

Without a proper fallback, refreshing a nested route may produce a server-side 404 even though client-side navigation works.


18. Recommended Project Structure

As your application grows, organizing route definitions and page components becomes important.

A practical structure could look like this:

src/
├── components/
│   ├── Navbar.jsx
│   └── Footer.jsx
├── layouts/
│   ├── MainLayout.jsx
│   └── DashboardLayout.jsx
├── pages/
│   ├── HomePage.jsx
│   ├── AboutPage.jsx
│   ├── ProductsPage.jsx
│   ├── ProductDetails.jsx
│   ├── LoginPage.jsx
│   └── NotFoundPage.jsx
├── routes/
│   └── router.jsx
├── services/
│   └── productApi.js
├── App.jsx
└── main.jsx
Enter fullscreen mode Exit fullscreen mode

For larger projects, keep route configuration separate from individual page components.

This makes the routing system easier to understand and maintain.


19. Best Practices

Here are some recommendations for building maintainable routing systems:

  1. Use Link and NavLink for internal navigation.
  2. Keep route definitions organized.
  3. Use nested routes for shared layouts.
  4. Use dynamic parameters for resource-specific pages.
  5. Use query parameters for filters, sorting, and pagination.
  6. Validate URL parameters before using them.
  7. Use navigation state only for temporary context.
  8. Handle unknown routes with a 404 page.
  9. Use route-level error handling for data routers.
  10. Protect sensitive operations on the backend.
  11. Configure your production server for client-side route fallback.
  12. Choose declarative routing or data routers based on your application's needs.

20. Practice Project: Build a Small E-Commerce Router

Now it's your turn to put these concepts together.

Build a small e-commerce application with the following routes:

Route Page
/ Home
/products Product list
/products/:productId Product details
/cart Shopping cart
/login Login
/dashboard Protected dashboard
* Not found

Your application should include:

  • A navigation bar.
  • Dynamic product detail pages.
  • Product category filters using query parameters.
  • A shared dashboard layout.
  • A protected dashboard route.
  • A custom 404 page.
  • Browser back and forward navigation.

Challenge: Add a product search field that updates the URL query parameters so users can share their search results.


Final Thoughts

React Router is an essential tool for building modern React applications.

It gives your application a clear navigation structure and makes it possible to build complex interfaces with multiple pages, shared layouts, and dynamic content.

Start with basic routes and Link.

Then learn dynamic parameters, nested layouts, and navigation state.

Finally, explore loaders, actions, and route-level error handling when your application needs more advanced data management.

Remember: good routing isn't just about changing URLs. It's about creating a predictable navigation experience for your users.


What's Next?

In Part 11: State Management in React — Context API, Redux Toolkit & Zustand, we'll explore how to manage shared application state as React projects become larger.

We'll cover:

  • Local state vs global state
  • React Context API
  • Prop drilling and how to avoid it
  • Redux Toolkit fundamentals
  • Zustand for lightweight state management
  • Managing asynchronous state
  • Choosing the right state management solution

Keep learning, keep building, and see you in the next part! 🚀

Top comments (0)