Skip to main content

Routing in React

Routing decides which React component should appear for the current URL.

/                  -> Home
/about -> About
/products/42 -> Product 42
/dashboard/profile -> Dashboard Profile

In a React Single Page Application (SPA), the app usually loads once. Internal navigation then changes the URL and renders the matching component without requesting a completely new HTML page.

One-line idea: URL changes → React Router finds a match → the matching component renders.

Core Concepts​

Why Routing Is Needed​

Without client-side routing, moving to another page generally causes the browser to request and load a new document.

With React routing:

User selects a link
↓
The URL changes
↓
React Router matches the path
↓
Only the relevant UI changes
Traditional page navigationReact SPA navigation
Requests a new HTML documentKeeps the current app running
Reloads the full pageRenders the matched component
Recreates the complete UIPreserves shared UI such as a navbar

Install React Router​

React does not include routing by default. For a browser-based React app, install react-router-dom:

npm install react-router-dom

Configure the Router​

Wrap the application with BrowserRouter so components inside it can use routing features.

main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
<BrowserRouter>
<App />
</BrowserRouter>,
);

Then map URL paths to elements using Routes and Route:

App.jsx
import { Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import Login from "./pages/Login";

export default function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/login" element={<Login />} />
</Routes>
);
}

How to read a route:

<Route path="/about" element={<About />} />
  • path is the URL pattern.
  • element is the UI rendered when that pattern matches.

Use Link for navigation between routes inside the React application.

Navbar.jsx
import { Link } from "react-router-dom";

export default function Navbar() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/login">Login</Link>
</nav>
);
}
UseBest for
<Link to="/about">Internal navigation without a full page reload
<a href="https://example.com">An external website or deliberate document navigation

Core React Router APIs​

APIPurpose
BrowserRouterConnects the app to the browser URL
RoutesHolds the route definitions
RouteMaps a path to an element
LinkNavigates to another internal route
OutletRenders the matched child route
useParamsReads dynamic values from the URL
NavigateRedirects during rendering
useNavigateNavigates from event or application logic
path="*"Matches an otherwise unmatched URL

Practical Routing Patterns​

Shared Layout with Outlet​

A navbar and footer should not be repeated in every page component. Place them in a layout and use Outlet as the position where child routes render.

Layout.jsx
import { Outlet } from "react-router-dom";
import Navbar from "./Navbar";
import Footer from "./Footer";

export default function Layout() {
return (
<>
<Navbar />

<main>
<Outlet />
</main>

<Footer />
</>
);
}
App.jsx
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Route>
</Routes>

Result:

Navbar  -> remains visible
Outlet -> changes between Home and About
Footer -> remains visible

Nested Routes​

Nested routes represent screens that belong to a common parent section.

/dashboard/profile
/dashboard/settings
/dashboard/orders

The parent component must contain an Outlet for its matched child.

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

export default function Dashboard() {
return (
<section>
<h1>Dashboard</h1>

<nav>
<Link to="profile">Profile</Link>
<Link to="settings">Settings</Link>
</nav>

<Outlet />
</section>
);
}
<Route path="/dashboard" element={<Dashboard />}>
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>

Notice that child paths are relative:

Parent: /dashboard
Child: profile
Final: /dashboard/profile

Dynamic Routes and useParams​

Use a dynamic segment when the same page structure displays different data.

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

Here, :id is a route parameter.

ProductDetails.jsx
import { useParams } from "react-router-dom";

export default function ProductDetails() {
const { id } = useParams();

return <h1>Product ID: {id}</h1>;
}

For /products/42, useParams() returns an object containing id: "42".

Common use cases:

  • product details: /products/:id
  • user profiles: /users/:userId
  • order details: /orders/:orderId
  • blog posts: /posts/:slug

Protected Routes with Navigate​

A protected route checks whether the user may enter a screen. If the check fails, render Navigate to redirect the user.

ProtectedRoute.jsx
import { Navigate } from "react-router-dom";

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

return children;
}
<Route
path="/dashboard"
element={
<ProtectedRoute isAuthenticated={isAuthenticated}>
<Dashboard />
</ProtectedRoute>
}
/>

Decision flow:

Authenticated?
├─ Yes -> render Dashboard
└─ No -> redirect to /login

A route guard controls frontend navigation. Protected data and actions must still be authorized by the backend.

Programmatic Navigation with useNavigate​

Use useNavigate when navigation should happen after an action such as login, form submission, or checkout.

Login.jsx
import { useNavigate } from "react-router-dom";

export default function Login() {
const navigate = useNavigate();

function handleLogin() {
// Complete authentication first.
navigate("/dashboard");
}

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

Remember:

Link        -> navigation initiated from rendered UI
useNavigate -> navigation initiated from JavaScript logic
Navigate -> redirect expressed during rendering

404 Route​

Place a wildcard route after the known route definitions to handle unmatched URLs.

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

This prevents users from seeing a blank screen when a URL does not exist.

Route-Level Lazy Loading​

Loading every page in the initial JavaScript bundle can slow down a large application. lazy loads a page component when it is needed, while Suspense displays fallback UI.

App.jsx
import { lazy, Suspense } from "react";
import { Route, Routes } from "react-router-dom";

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

export default function App() {
return (
<Suspense fallback={<p>Loading page...</p>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
Route is visited
↓
Its component bundle is requested
↓
Fallback appears while loading
↓
The page component renders

Routing and SEO​

A client-rendered SPA may initially return very little page content in its HTML:

<div id="root"></div>

The visible content is added after JavaScript runs. This can make indexing more difficult for search-sensitive pages.

Each important route should also define suitable metadata, such as:

  • page title
  • description
  • Open Graph information

For content where search visibility is important, route-specific metadata and server-rendered or pre-rendered HTML are commonly considered.

Benefits, Trade-Offs, and Common Mistakes​

Benefits

  • Smooth internal navigation without a complete page reload.
  • URLs can represent specific application screens.
  • Shared layouts keep common UI mounted.
  • Nested routes mirror sections such as dashboards.
  • Dynamic routes reuse one page for many records.
  • Route-level lazy loading can reduce the initial bundle.

Trade-offs

  • Routing adds configuration and route structure to maintain.
  • Large client-rendered apps can send more JavaScript initially.
  • Search-sensitive pages need deliberate metadata and rendering decisions.
  • A frontend protected route is not a replacement for backend authorization.

Common mistakes

MistakeBetter approach
Using <a> for every internal pageUse Link for SPA routes
Repeating navbar and footer on each pageCreate a layout with Outlet
Forgetting Outlet in a parent routeAdd it where child UI should render
Creating one route per productUse /products/:id
Calling navigation before login succeedsNavigate after successful authentication
Omitting an unmatched-route fallbackAdd <Route path="*" ... />
Loading every large page immediatelyLazy-load route components where useful

Summary​

Routing keeps the URL and visible React screen in sync without reloading the entire application.

BrowserRouter watches the URL
↓
Routes searches the route map
↓
Route selects the matching element
↓
Layout stays; Outlet changes
↓
Params provide dynamic URL data
↓
Navigate or useNavigate changes the route when required