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 

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 

Install React Router:

npm install react-router 

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; 

AboutPage.jsx

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

ContactPage.jsx

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

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; 

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 />} /> 

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> 

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; 

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> ); } 

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 

Creating a separate route for every product would be impractical.

Instead, define a dynamic route:

<Route path="/products/:productId" element={<ProductDetails />} /> 

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; 

When the user visits:

/products/42 

The component receives:

productId === "42" 

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>; } 

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; 

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 

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; 

The important part is:

<Outlet /> 

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> ); } 

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 

What Is an Index Route?

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

<Route index element={<DashboardHome />} /> 

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> ); } 

Replace the Current History Entry

You can replace the current history entry:

navigate("/dashboard", { replace: true }); 

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); 

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> ); } 

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> ); } 

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 

A query parameter appears after ?:

/products?category=electronics&sort=price 

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> ); } 

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 />; } 

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> 

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 />; } 

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 n...