Chapter 14: Routing and Navigation in React
Objectives
In this chapter, readers will:
- Implement client-side routing using React Router v6
- Build Next.js applications with App Router and file-based routing
- Create dynamic routes and handle route parameters
- Protect routes with authentication and authorization
- Optimize navigation with prefetching and loading states
Chapter Outline
- Objectives
- Chapter Outline
- Introduction to React Routing
- React Router v6
- Next.js App Router
- Advanced Routing Patterns
- Authentication and Route Protection
- Performance Optimization
- Summary and Best Practices
Introduction to React Routing
Single Page Application (SPA) Routing: Unlike traditional multi-page websites where each URL loads a new HTML document from the server, React applications use client-side routing. This means:
- URLs change without full page reloads
- Faster navigation between pages
- State persists during navigation
- Better user experience
2025 Routing Solutions:
- React Router: Most popular routing library for React SPAs
- Next.js App Router: Modern file-based routing with advanced features
- TanStack Router: Type-safe routing (emerging option)
This chapter focuses on React Router (for Vite apps) and Next.js App Router.
React Router v6
React Router v6 is the standard routing solution for React single-page applications.
Installation and Setup
Step 1: Install React Router
npm install react-router-dom
Step 2: Basic App Structure
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
Basic Routing
Simple Route Configuration
// src/App.tsx
import { Routes, Route, Link } from 'react-router-dom';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import ContactPage from './pages/ContactPage';
import NotFoundPage from './pages/NotFoundPage';
function App() {
return (
<div className="app">
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</div>
);
}
export default App;
Navigation with NavLink
import { NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
Home
</NavLink>
<NavLink
to="/about"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? '#0066cc' : '#666',
})}
>
About
</NavLink>
</nav>
);
}
Route Parameters
Dynamic Routes
// src/App.tsx
<Routes>
<Route path="/blog/:slug" element={<BlogPost />} />
<Route path="/user/:userId" element={<UserProfile />} />
<Route path="/products/:category/:id" element={<ProductDetail />} />
</Routes>
Accessing Parameters
// src/pages/BlogPost.tsx
import { useParams, useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
interface BlogPost {
slug: string;
title: string;
content: string;
}
function BlogPost() {
const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate();
const [post, setPost] = useState<BlogPost | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPost() {
try {
const response = await fetch(`/api/posts/${slug}`);
if (!response.ok) {
navigate('/404');
return;
}
const data = await response.json();
setPost(data);
} catch (error) {
console.error('Failed to fetch post:', error);
} finally {
setLoading(false);
}
}
fetchPost();
}, [slug, navigate]);
if (loading) return <div>Loading...</div>;
if (!post) return null;
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Nested Routes
Layout with Nested Routes
// src/App.tsx
import { Routes, Route } from 'react-router-dom';
import DashboardLayout from './layouts/DashboardLayout';
import DashboardHome from './pages/Dashboard/Home';
import DashboardSettings from './pages/Dashboard/Settings';
import DashboardProfile from './pages/Dashboard/Profile';
function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<DashboardSettings />} />
<Route path="profile" element={<DashboardProfile />} />
</Route>
</Routes>
);
}
Layout Component with Outlet
// src/layouts/DashboardLayout.tsx
import { Outlet, NavLink } from 'react-router-dom';
function DashboardLayout() {
return (
<div className="dashboard-layout">
<aside className="sidebar">
<nav>
<NavLink to="/dashboard">Home</NavLink>
<NavLink to="/dashboard/settings">Settings</NavLink>
<NavLink to="/dashboard/profile">Profile</NavLink>
</nav>
</aside>
<main className="dashboard-content">
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}
export default DashboardLayout;
Navigation
Programmatic Navigation in Next.js
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Perform login
const success = await login();
if (success) {
// Navigate to dashboard after successful login
navigate('/dashboard');
// Or navigate with state
navigate('/dashboard', {
state: { message: 'Login successful!' },
replace: true, // Replace history entry
});
}
};
return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}
Accessing Navigation State
import { useLocation } from 'react-router-dom';
function Dashboard() {
const location = useLocation();
const message = location.state?.message;
return (
<div>
{message && <div className="alert">{message}</div>}
<h1>Dashboard</h1>
</div>
);
}
Protected Routes
Protected Route Component
// src/components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
interface ProtectedRouteProps {
children: React.ReactNode;
}
function ProtectedRoute({ children }: ProtectedRouteProps) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
// Redirect to login, saving the attempted location
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
export default ProtectedRoute;
Usage
// src/App.tsx
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<ProfilePage />
</ProtectedRoute>
}
/>
</Routes>
Next.js App Router
Next.js App Router (introduced in Next.js 13, stable in 15) provides a modern, file-based routing system.
File-Based Routing
Basic File Structure
src/app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
└── dashboard/
├── layout.tsx # Dashboard layout
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
Root Layout (Required)
// src/app/layout.tsx
export const metadata = {
title: 'My App',
description: 'App description',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
);
}
Page Component
// src/app/page.tsx
export default function HomePage() {
return (
<div>
<h1>Welcome to My App</h1>
<p>This is the home page</p>
</div>
);
}
Dynamic Routes in Next.js
Single Dynamic Segment
// src/app/blog/[slug]/page.tsx
interface PageProps {
params: { slug: string };
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = params;
const post = await fetchPost(slug);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// Generate static params for SSG
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// Generate metadata
export async function generateMetadata({ params }: PageProps) {
const post = await fetchPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
Multiple Dynamic Segments
// src/app/shop/[category]/[productId]/page.tsx
interface PageProps {
params: { category: string; productId: string };
}
export default async function ProductPage({ params }: PageProps) {
const { category, productId } = params;
const product = await fetchProduct(category, productId);
return (
<div>
<h1>{product.name}</h1>
<p>Category: {category}</p>
<p>Price: ${product.price}</p>
</div>
);
}
Catch-All Routes
// src/app/docs/[...slug]/page.tsx - Matches /docs/a, /docs/a/b, /docs/a/b/c
interface PageProps {
params: { slug: string[] };
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = params;
const path = slug.join('/');
const content = await fetchDocs(path);
return <div>{content}</div>;
}
// Optional catch-all: [[...slug]]/page.tsx - Also matches /docs
Route Groups
Route groups allow you to organize routes without affecting the URL structure:
src/app/
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ ├── page.tsx # / (home)
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
├── (shop)/
│ ├── layout.tsx # Shop layout
│ ├── products/
│ │ └── page.tsx # /products
│ └── cart/
│ └── page.tsx # /cart
└── (auth)/
├── login/
│ └── page.tsx # /login
└── register/
└── page.tsx # /register
// src/app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="marketing-layout">
<nav>{/* Marketing nav */}</nav>
{children}
<footer>{/* Marketing footer */}</footer>
</div>
);
}
Parallel Routes
Parallel routes allow you to render multiple pages simultaneously:
src/app/
└── dashboard/
├── layout.tsx
├── page.tsx
├── @analytics/ # Named slot
│ └── page.tsx
└── @team/ # Named slot
└── page.tsx
// src/app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="dashboard">
<div className="main">{children}</div>
<div className="sidebar">
<div className="analytics">{analytics}</div>
<div className="team">{team}</div>
</div>
</div>
);
}
Intercepting Routes
Intercepting routes allow you to show a route within the current layout:
src/app/
├── feed/
│ └── page.tsx
└── photo/
├── [id]/
│ └── page.tsx
└── (..)feed/ # Intercepts /feed from /photo
└── page.tsx
// src/app/photo/(..)feed/page.tsx
import Modal from '@/components/Modal';
export default function InterceptedFeed() {
return (
<Modal>
<h1>Feed (in modal)</h1>
{/* Feed content */}
</Modal>
);
}
Navigation in Next.js
Link Component
import Link from 'next/link';
function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
{/* With prefetching disabled */}
<Link href="/large-page" prefetch={false}>
Large Page
</Link>
{/* Dynamic route */}
<Link href={`/blog/${slug}`}>
View Post
</Link>
</nav>
);
}
Programmatic Navigation
'use client';
import { useRouter } from 'next/navigation';
function LoginForm() {
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const success = await login();
if (success) {
router.push('/dashboard');
// or router.replace('/dashboard') to replace history
}
};
return <form onSubmit={handleSubmit}>{/* form */}</form>;
}
Advanced Routing Patterns
Query Parameters
React Router Query Parameters
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') || '';
const page = Number(searchParams.get('page')) || 1;
const handleSearch = (newQuery: string) => {
setSearchParams({ q: newQuery, page: '1' });
};
return (
<div>
<input
value={query}
onChange={(e) => handleSearch(e.target.value)}
/>
<p>Page {page}</p>
</div>
);
}
Next.js Query Parameters
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
function SearchPage() {
const router = useRouter();
const searchParams = useSearchParams();
const query = searchParams.get('q') || '';
const handleSearch = (newQuery: string) => {
const params = new URLSearchParams(searchParams);
params.set('q', newQuery);
params.set('page', '1');
router.push(`/search?${params.toString()}`);
};
return <div>{/* search UI */}</div>;
}
404 and Error Pages
React Router
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
Next.js
// src/app/not-found.tsx
export default function NotFound() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
</div>
);
}
// src/app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Redirects
React Router Redirect Example
import { Navigate } from 'react-router-dom';
<Routes>
<Route path="/old-page" element={<Navigate to="/new-page" replace />} />
</Routes>
Next.js Redirect Example
// next.config.js
module.exports = {
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true,
},
];
},
};
// Or in a page
import { redirect } from 'next/navigation';
export default async function Page() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return <div>Protected content</div>;
}
Loading States
React Router with Suspense
import { Suspense, lazy } from 'react';
const BlogPost = lazy(() => import('./pages/BlogPost'));
<Routes>
<Route
path="/blog/:slug"
element={
<Suspense fallback={<div>Loading...</div>}>
<BlogPost />
</Suspense>
}
/>
</Routes>
Next.js Loading UI
// src/app/blog/loading.tsx
export default function Loading() {
return (
<div className="loading-skeleton">
<div className="skeleton-title" />
<div className="skeleton-content" />
</div>
);
}
Authentication and Route Protection
Protected Route Pattern
Comprehensive Protected Route
// src/components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: string[];
redirectTo?: string;
}
function ProtectedRoute({
children,
requiredRole,
redirectTo = '/login',
}: ProtectedRouteProps) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return <div>Checking authentication...</div>;
}
if (!user) {
return <Navigate to={redirectTo} state={{ from: location }} replace />;
}
if (requiredRole && !requiredRole.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return <>{children}</>;
}
export default ProtectedRoute;
Role-Based Access
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute requiredRole={['admin']}>
<AdminPage />
</ProtectedRoute>
}
/>
</Routes>
Redirect After Login
function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
const from = location.state?.from?.pathname || '/dashboard';
const handleSubmit = async (credentials: Credentials) => {
try {
await login(credentials);
navigate(from, { replace: true });
} catch (error) {
console.error('Login failed:', error);
}
};
return <form onSubmit={handleSubmit}>{/* form */}</form>;
}
Performance Optimization
Code Splitting Routes
React Router Code Splitting Routes
import { lazy, Suspense } from 'react';
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const BlogPage = lazy(() => import('./pages/BlogPage'));
function App() {
return (
<Routes>
<Route
path="/"
element={
<Suspense fallback={<div>Loading...</div>}>
<HomePage />
</Suspense>
}
/>
<Route
path="/about"
element={
<Suspense fallback={<div>Loading...</div>}>
<AboutPage />
</Suspense>
}
/>
</Routes>
);
}
Prefetching
Next.js Automatic Prefetching
// Links are automatically prefetched on hover
<Link href="/blog">Blog</Link>
// Disable prefetching
<Link href="/large-page" prefetch={false}>
Large Page
</Link>
// Programmatic prefetch
'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
function Component() {
const router = useRouter();
useEffect(() => {
// Prefetch on component mount
router.prefetch('/likely-next-page');
}, [router]);
}
Scroll Restoration
React Router Scroll Restoration
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
// Use in App
function App() {
return (
<>
<ScrollToTop />
<Routes>{/* routes */}</Routes>
</>
);
}
Next.js (Automatic)
Next.js automatically handles scroll restoration. To customize:
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
export function ScrollToTop() {
const pathname = usePathname();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
Summary and Best Practices
2025 Routing Guidelines
✅ Recommended Practices:
- Use React Router v6 for SPAs - Modern, type-safe, performant
- Use Next.js App Router for full apps - Built-in optimization, SEO
- Code split routes - Lazy load for better performance
- Protect sensitive routes - Implement authentication guards
- Handle loading states - Provide feedback during navigation
- Use TypeScript - Type-safe route parameters
- Prefetch strategically - Improve perceived performance
❌ Avoid These Patterns:
- Don’t use class components for routing - Use functional components with hooks
- Don’t skip loading states - Always provide user feedback
- Don’t hardcode URLs - Use route constants or type-safe routing
- Don’t ignore 404 pages - Handle not found routes gracefully
- Don’t skip authentication checks - Always protect sensitive routes
Quick Decision Guide
Use React Router when:
- Building a single-page application
- Don’t need server-side rendering
- Want maximum flexibility
- Using Vite or other non-Next.js tools
Use Next.js App Router when:
- Need SEO optimization (SSR/SSG)
- Want file-based routing
- Building full-featured applications
- Need advanced features (parallel routes, intercepting)
Next Steps
Now that you understand routing, continue with:
- Chapter 15: Forms and Input Handling
- Chapter 16: Testing React Applications
- Chapter 17: Deployment and Production
With proper routing in place, you can build complex, navigable React applications!