🚀 Lesson 6.2: Advanced Routing
Take your React Router skills to the next level with nested routes, dynamic parameters, and programmatic navigation. Build complex, scalable routing architectures for real-world applications.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Create and manage nested routes with shared layouts
- Use dynamic route parameters with useParams hook
- Navigate programmatically using the useNavigate hook
- Access location data with useLocation hook
- Build complex multi-level navigation structures
- Implement parent-child route relationships
- Type dynamic routes properly with TypeScript
Estimated Time: 75-90 minutes
Prerequisites: Lesson 6.1 - React Router Basics
📑 In This Lesson
🗂️ Understanding Nested Routes
Nested routes allow you to create complex layouts where some UI remains consistent while other parts change based on the route. This is perfect for dashboards, settings pages, and any application with shared layouts.
What Are Nested Routes?
Nested routes are routes defined inside other routes, creating a parent-child relationship. The parent route can have a layout that wraps all child routes.
Why Use Nested Routes?
- 🎨 Shared Layouts: Common UI elements (sidebars, headers) persist across child routes
- 📐 Logical Organization: Group related routes together
- 🔄 Partial Updates: Only the nested content re-renders, not the entire page
- 🗂️ Better Structure: Mirror your URL structure in your component hierarchy
- ⚡ Performance: Shared layouts don't unmount/remount
📖 Real-World Example
Think of a dashboard at /dashboard. The sidebar and header stay the same, but when you navigate to /dashboard/analytics or /dashboard/settings, only the main content area changes. That's nested routing!
The Outlet Component
The key to nested routes is the Outlet component. It acts as a placeholder where child routes will be rendered.
import { Outlet } from 'react-router-dom';
function DashboardLayout() {
return (
<div className="dashboard">
<aside className="sidebar">
<nav>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/analytics">Analytics</Link>
<Link to="/dashboard/settings">Settings</Link>
</nav>
</aside>
<main className="dashboard-content">
{/* Child routes render here! */}
<Outlet />
</main>
</div>
);
}
💡 How Outlet Works
When you navigate to a child route, React Router renders the parent component and replaces the <Outlet /> with the child route's component. The rest of the parent layout stays in place.
Creating Nested Routes
There are two ways to define nested routes in React Router v6:
Method 1: Route Nesting in JSX
import { Routes, Route } from 'react-router-dom';
import DashboardLayout from './layouts/DashboardLayout';
import Overview from './pages/dashboard/Overview';
import Analytics from './pages/dashboard/Analytics';
import Settings from './pages/dashboard/Settings';
function App() {
return (
<Routes>
{/* Parent route with nested children */}
<Route path="/dashboard" element={<DashboardLayout />}>
{/* Child routes */}
<Route index element={<Overview />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
);
}
📖 Understanding the Routes
<Route index>: Renders at/dashboard(matches parent exactly)path="analytics": Renders at/dashboard/analytics(relative path)path="settings": Renders at/dashboard/settings
Notice that child paths are relative to the parent path!
Method 2: Route Configuration Object
import { useRoutes } from 'react-router-dom';
import DashboardLayout from './layouts/DashboardLayout';
import Overview from './pages/dashboard/Overview';
import Analytics from './pages/dashboard/Analytics';
import Settings from './pages/dashboard/Settings';
function App() {
const routes = useRoutes([
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{ index: true, element: <Overview /> },
{ path: 'analytics', element: <Analytics /> },
{ path: 'settings', element: <Settings /> }
]
}
]);
return routes;
}
Index Routes Explained
An index route is a special type of child route that renders when the parent route matches exactly.
// Without index route:
// /dashboard → Shows DashboardLayout with empty Outlet
// /dashboard/analytics → Shows Analytics in the Outlet
// With index route:
// /dashboard → Shows DashboardLayout with Overview in the Outlet
// /dashboard/analytics → Shows DashboardLayout with Analytics in the Outlet
✅ Index Route Best Practices
- Use index routes for default content when visiting parent route
- Common use cases: overview pages, home tabs, default views
- Don't use a
pathprop on index routes (it's ignored) - Index routes inherit the parent's path exactly
Complete Example: Dashboard with Nested Routes
Let's build a complete dashboard with nested routes.
src/layouts/DashboardLayout.tsx:
import React from 'react';
import { Outlet, NavLink } from 'react-router-dom';
import './DashboardLayout.css';
function DashboardLayout() {
const navLinks = [
{ to: '/dashboard', label: 'Overview', end: true },
{ to: '/dashboard/analytics', label: 'Analytics' },
{ to: '/dashboard/reports', label: 'Reports' },
{ to: '/dashboard/settings', label: 'Settings' }
];
return (
<div className="dashboard-layout">
{/* Sidebar Navigation */}
<aside className="dashboard-sidebar">
<div className="sidebar-header">
<h2>📊 Dashboard</h2>
</div>
<nav className="sidebar-nav">
{navLinks.map(link => (
<NavLink
key={link.to}
to={link.to}
end={link.end}
className={({ isActive }) =>
isActive ? 'nav-item nav-item-active' : 'nav-item'
}
>
{link.label}
</NavLink>
))}
</nav>
</aside>
{/* Main Content Area */}
<main className="dashboard-main">
<div className="dashboard-container">
{/* Child routes render here */}
<Outlet />
</div>
</main>
</div>
);
}
export default DashboardLayout;
src/pages/dashboard/Overview.tsx:
import React from 'react';
function Overview() {
return (
<div>
<h1>Dashboard Overview</h1>
<p>Welcome to your dashboard! Here's a summary of your account.</p>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Users</h3>
<p className="stat-value">1,234</p>
</div>
<div className="stat-card">
<h3>Revenue</h3>
<p className="stat-value">$45,678</p>
</div>
<div className="stat-card">
<h3>Active Projects</h3>
<p className="stat-value">23</p>
</div>
</div>
</div>
);
}
export default Overview;
src/pages/dashboard/Analytics.tsx:
import React from 'react';
function Analytics() {
return (
<div>
<h1>Analytics</h1>
<p>View detailed analytics and insights about your application.</p>
<div className="charts">
<div className="chart-card">
<h3>User Growth</h3>
<p>📈 Chart placeholder</p>
</div>
<div className="chart-card">
<h3>Revenue Trends</h3>
<p>💰 Chart placeholder</p>
</div>
</div>
</div>
);
}
export default Analytics;
src/App.tsx:
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import DashboardLayout from './layouts/DashboardLayout';
import Overview from './pages/dashboard/Overview';
import Analytics from './pages/dashboard/Analytics';
import Reports from './pages/dashboard/Reports';
import Settings from './pages/dashboard/Settings';
import Home from './pages/Home';
import NotFound from './pages/NotFound';
function App() {
return (
<Routes>
{/* Regular routes */}
<Route path="/" element={<Home />} />
{/* Nested dashboard routes */}
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} />
<Route path="analytics" element={<Analytics />} />
<Route path="reports" element={<Reports />} />
<Route path="settings" element={<Settings />} />
</Route>
{/* 404 */}
<Route path="*" element={<NotFound />} />
</Routes>
);
}
export default App;
Styling the Dashboard
Create src/layouts/DashboardLayout.css:
.dashboard-layout {
display: flex;
min-height: 100vh;
}
.dashboard-sidebar {
width: 250px;
background: #2d3748;
color: white;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h2 {
margin: 0;
font-size: 1.25rem;
}
.sidebar-nav {
display: flex;
flex-direction: column;
padding: 1rem 0;
}
.nav-item {
padding: 0.75rem 1.5rem;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.05);
color: white;
}
.nav-item-active {
background: rgba(102, 126, 234, 0.1);
color: white;
border-left-color: #667eea;
}
.dashboard-main {
flex: 1;
background: #f7fafc;
overflow-y: auto;
}
.dashboard-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-card h3 {
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
color: #718096;
text-transform: uppercase;
}
.stat-value {
margin: 0;
font-size: 2rem;
font-weight: bold;
color: #667eea;
}
@media (max-width: 768px) {
.dashboard-layout {
flex-direction: column;
}
.dashboard-sidebar {
width: 100%;
}
.sidebar-nav {
flex-direction: row;
overflow-x: auto;
}
}
🏋️ Exercise: Add More Dashboard Pages
Add two more pages to the dashboard: "Users" and "Products". Create the page components and add them to the nested routes.
💡 Hint
Create Users.tsx and Products.tsx in pages/dashboard/, then add Route components inside the dashboard parent route with paths "users" and "products".
✅ Solution
// src/pages/dashboard/Users.tsx
function Users() {
return (
<div>
<h1>Users</h1>
<p>Manage your application users.</p>
</div>
);
}
// In App.tsx, add to dashboard routes:
<Route path="users" element={<Users />} />
<Route path="products" element={<Products />} />
// In DashboardLayout.tsx, add to navLinks:
{ to: '/dashboard/users', label: 'Users' },
{ to: '/dashboard/products', label: 'Products' }
Multiple Levels of Nesting
You can nest routes multiple levels deep:
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} />
{/* Settings has its own nested routes */}
<Route path="settings" element={<SettingsLayout />}>
<Route index element={<GeneralSettings />} />
<Route path="profile" element={<ProfileSettings />} />
<Route path="security" element={<SecuritySettings />} />
</Route>
</Route>
</Routes>
// URL structure:
// /dashboard/settings → GeneralSettings
// /dashboard/settings/profile → ProfileSettings
// /dashboard/settings/security → SecuritySettings
✅ Nested Routes Recap
- Parent routes render a layout with an
<Outlet /> - Child routes render inside the parent's Outlet
- Index routes render at the parent's exact path
- Child paths are relative to parent path
- You can nest routes as many levels deep as needed
- Shared UI stays mounted as you navigate between children
🎯 Dynamic Route Parameters
Dynamic route parameters allow you to create flexible routes that match patterns instead of exact paths. This is essential for applications with user profiles, blog posts, product details, and more.
What Are Route Parameters?
Route parameters are variable parts of a URL path, denoted by a colon (:) prefix. They capture values from the URL and make them available in your components.
// Static route (matches exactly one URL):
<Route path="/users" element={<UserList />} />
// Matches: /users
// Dynamic route (matches many URLs):
<Route path="/users/:userId" element={<UserProfile />} />
// Matches: /users/1, /users/123, /users/abc, etc.
// The userId part is captured as a parameter
Defining Dynamic Routes
Dynamic routes are defined just like regular routes, but with parameter segments:
import { Routes, Route } from 'react-router-dom';
import BlogList from './pages/BlogList';
import BlogPost from './pages/BlogPost';
import UserProfile from './pages/UserProfile';
import ProductDetail from './pages/ProductDetail';
function App() {
return (
<Routes>
{/* Blog routes */}
<Route path="/blog" element={<BlogList />} />
<Route path="/blog/:postId" element={<BlogPost />} />
{/* User routes */}
<Route path="/users/:userId" element={<UserProfile />} />
{/* Product routes */}
<Route path="/products/:productId" element={<ProductDetail />} />
{/* Multiple parameters */}
<Route path="/category/:categoryId/product/:productId" element={<Product />} />
</Routes>
);
}
📖 Parameter Naming
Parameter names can be anything, but use descriptive names that make sense:
- ✅
:userId,:postId,:productId - ✅
:username,:slug,:id - ❌
:param,:x,:value(too vague)
Multiple Parameters
Routes can have multiple parameters:
// Two parameters in one route
<Route path="/blog/:year/:month/:slug" element={<BlogPost />} />
// Matches: /blog/2024/01/react-router-guide
// Captures: year=2024, month=01, slug=react-router-guide
// Nested segments
<Route path="/store/:storeId/product/:productId" element={<Product />} />
// Matches: /store/amazon/product/iphone-15
// Captures: storeId=amazon, productId=iphone-15
Optional Parameters
Make a parameter optional by adding a question mark:
// Optional page parameter
<Route path="/blog/:category/:page?" element={<BlogCategory />} />
// Matches: /blog/tech (page is undefined)
// Matches: /blog/tech/2 (page is "2")
⚠️ Optional Parameters Gotcha
Optional parameters should come at the end of the route path. Putting them in the middle can lead to ambiguous route matching:
// ❌ Confusing
/blog/:category?/:postId
// ✅ Clear
/blog/:postId/:page?
Wildcard Parameters
Use an asterisk (*) to match remaining path segments:
// Catch-all route
<Route path="/docs/*" element={<Documentation />} />
// Matches: /docs/getting-started
// Matches: /docs/api/components/button
// Matches: /docs/anything/here
// Named wildcard (React Router v6.4+)
<Route path="/files/:filename/*" element={<FileViewer />} />
// Matches: /files/document.pdf/preview
// Captures: filename=document.pdf, *=/preview
🎯 The useParams Hook
The useParams hook is how you access dynamic route parameters in your components. It returns an object containing all parameter values from the current URL.
Basic useParams Usage
Here's how to use useParams to access route parameters:
import { useParams } from 'react-router-dom';
// Route definition: /users/:userId
function UserProfile() {
// Get the userId from the URL
const { userId } = useParams();
return (
<div>
<h1>User Profile</h1>
<p>Viewing profile for user: {userId}</p>
</div>
);
}
// When user navigates to /users/42
// userId will be "42"
⚠️ Parameters Are Always Strings
All route parameters come through as strings, even if they look like numbers in the URL:
const { userId } = useParams();
// userId is "42", not 42
// Convert to number if needed:
const userIdNumber = Number(userId);
// or
const userIdNumber = parseInt(userId, 10);
Typing useParams with TypeScript
TypeScript needs to know what parameters to expect. Here's how to type useParams properly:
import { useParams } from 'react-router-dom';
// Option 1: Define a type for your params
type UserParams = {
userId: string;
};
function UserProfile() {
const { userId } = useParams<UserParams>();
// TypeScript knows userId is string | undefined
// Handle the case where userId might be undefined
if (!userId) {
return <div>No user ID provided</div>;
}
return <div>User ID: {userId}</div>;
}
// Option 2: Inline type
function BlogPost() {
const { postId } = useParams<{ postId: string }>();
return <div>Post ID: {postId}</div>;
}
📖 React Router v6.4+ Typing
In React Router v6.4+, useParams returns parameters as string | undefined by default. This is safer because params might not always be present:
// Type returned by useParams
const params: Readonly<Params<string>>
// Which means each param is: string | undefined
const { userId } = useParams();
// userId: string | undefined
Multiple Parameters
Access multiple parameters from the same route:
import { useParams } from 'react-router-dom';
// Route: /blog/:year/:month/:slug
type BlogPostParams = {
year: string;
month: string;
slug: string;
};
function BlogPost() {
const { year, month, slug } = useParams<BlogPostParams>();
// Validate and convert
if (!year || !month || !slug) {
return <div>Invalid blog post URL</div>;
}
const yearNum = parseInt(year, 10);
const monthNum = parseInt(month, 10);
return (
<article>
<h1>{slug.replace(/-/g, ' ')}</h1>
<p>Published: {monthNum}/{yearNum}</p>
</article>
);
}
Practical Example: Product Detail Page
Let's build a complete example with data fetching based on route parameters:
import { useParams } from 'react-router-dom';
import { useState, useEffect } from 'react';
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
}
type ProductParams = {
productId: string;
};
function ProductDetail() {
const { productId } = useParams<ProductParams>();
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Validate parameter
if (!productId) {
setError('No product ID provided');
setLoading(false);
return;
}
// Fetch product data
const fetchProduct = async () => {
try {
const response = await fetch(
`https://api.example.com/products/${productId}`
);
if (!response.ok) {
throw new Error('Product not found');
}
const data = await response.json();
setProduct(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load product');
} finally {
setLoading(false);
}
};
fetchProduct();
}, [productId]);
if (loading) {
return <div>Loading product...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
if (!product) {
return <div>Product not found</div>;
}
return (
<div className="product-detail">
<img src={product.imageUrl} alt={product.name} />
<h1>{product.name}</h1>
<p className="price">${product.price.toFixed(2)}</p>
<p>{product.description}</p>
</div>
);
}
✅ Best Practices
- Always validate parameters - Check if they exist before using them
- Convert types as needed - Parse strings to numbers when required
- Handle missing params - Show appropriate error messages
- Use TypeScript types - Define param shapes for type safety
- Sanitize input - Validate parameter formats and values
🏋️ Exercise: User Profile with Tabs
Create a user profile page that uses route parameters for both the user ID and the active tab.
Requirements:
- Route pattern:
/users/:userId/:tab? - Extract both
userIdand optionaltabparameters - Default to "overview" tab if no tab specified
- Display different content for each tab: overview, posts, photos, settings
- Show user's name based on the ID (can use mock data)
- Properly type the parameters with TypeScript
💡 Hint
Define your params type with both required and optional parameters:
type UserProfileParams = {
userId: string;
tab?: string; // Optional parameter
};
Use a default value when destructuring:
const { userId, tab = 'overview' } = useParams<UserProfileParams>();
✅ Solution
import { useParams } from 'react-router-dom';
type UserProfileParams = {
userId: string;
tab?: string;
};
// Mock user data
const users: Record<string, { name: string; email: string }> = {
'1': { name: 'Alice Johnson', email: 'alice@example.com' },
'2': { name: 'Bob Smith', email: 'bob@example.com' },
'3': { name: 'Charlie Davis', email: 'charlie@example.com' },
};
function UserProfile() {
const { userId, tab = 'overview' } = useParams<UserProfileParams>();
// Validate userId
if (!userId || !users[userId]) {
return (
<div>
<h1>User Not Found</h1>
<p>The user ID {userId} doesn't exist.</p>
</div>
);
}
const user = users[userId];
// Render different content based on active tab
const renderTabContent = () => {
switch (tab) {
case 'overview':
return (
<div>
<h3>Overview</h3>
<p>Email: {user.email}</p>
<p>Member since: January 2024</p>
</div>
);
case 'posts':
return (
<div>
<h3>Posts</h3>
<p>{user.name} hasn't posted anything yet.</p>
</div>
);
case 'photos':
return (
<div>
<h3>Photos</h3>
<p>{user.name} hasn't uploaded any photos.</p>
</div>
);
case 'settings':
return (
<div>
<h3>Settings</h3>
<p>Privacy settings and preferences.</p>
</div>
);
default:
return <p>Invalid tab: {tab}</p>;
}
};
return (
<div className="user-profile">
<header>
<h1>{user.name}</h1>
<p>User ID: {userId}</p>
</header>
<nav className="profile-tabs">
<a
href={`/users/${userId}/overview`}
className={tab === 'overview' ? 'active' : ''}
>
Overview
</a>
<a
href={`/users/${userId}/posts`}
className={tab === 'posts' ? 'active' : ''}
>
Posts
</a>
<a
href={`/users/${userId}/photos`}
className={tab === 'photos' ? 'active' : ''}
>
Photos
</a>
<a
href={`/users/${userId}/settings`}
className={tab === 'settings' ? 'active' : ''}
>
Settings
</a>
</nav>
<main>
{renderTabContent()}
</main>
</div>
);
}
🧭 Programmatic Navigation with useNavigate
The useNavigate hook allows you to navigate to different routes programmatically in response to user actions or application logic, rather than through link clicks.
📖 When to Use useNavigate
- After form submission - Navigate to success page after saving data
- Authentication flows - Redirect after login/logout
- Conditional navigation - Navigate based on user role or permissions
- Timeout redirects - Navigate after a delay
- Error handling - Redirect on errors
Basic useNavigate Usage
The useNavigate hook returns a navigation function:
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
// Perform login logic...
const success = await performLogin();
if (success) {
// Navigate to dashboard after successful login
navigate('/dashboard');
}
};
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
);
}
Navigation Options
The navigate function accepts several options:
import { useNavigate } from 'react-router-dom';
function MyComponent() {
const navigate = useNavigate();
// 1. Simple navigation to a path
navigate('/about');
// 2. Navigate with relative path
navigate('../parent'); // Go up one level
navigate('child'); // Go to child route
// 3. Navigate with replace (replaces history entry)
navigate('/login', { replace: true });
// 4. Navigate backward/forward
navigate(-1); // Go back one page (like browser back)
navigate(-2); // Go back two pages
navigate(1); // Go forward one page
// 5. Navigate with state
navigate('/profile', {
state: { from: '/settings', userId: 42 }
});
}
The Replace Option
Using replace: true replaces the current history entry instead of adding a new one:
function RedirectExample() {
const navigate = useNavigate();
useEffect(() => {
// Check authentication
const isAuthenticated = checkAuth();
if (!isAuthenticated) {
// Replace current history entry
// User can't go "back" to protected page
navigate('/login', { replace: true });
}
}, [navigate]);
return <div>Protected Content</div>;
}
💡 Replace vs. Push
| Behavior | Default (Push) | Replace |
|---|---|---|
| History stack | Adds new entry | Replaces current entry |
| Back button | Goes to previous page | Skips replaced page |
| Use case | Normal navigation | Redirects, auth flows |
Navigating with State
Pass data to the next route using the state option:
import { useNavigate } from 'react-router-dom';
function ProductList() {
const navigate = useNavigate();
const handleProductClick = (product: Product) => {
// Navigate and pass product data
navigate(`/products/${product.id}`, {
state: {
product,
fromPage: 'listing',
searchQuery: 'laptops'
}
});
};
return (
<div>
{products.map(product => (
<button
key={product.id}
onClick={() => handleProductClick(product)}
>
{product.name}
</button>
))}
</div>
);
}
Practical Example: Multi-Step Form
Here's a complete example showing programmatic navigation in a multi-step form:
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
interface FormData {
step1: {
email: string;
password: string;
};
step2: {
firstName: string;
lastName: string;
};
step3: {
address: string;
city: string;
};
}
function RegistrationStep1() {
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validate
if (!email || !password) {
alert('Please fill all fields');
return;
}
// Navigate to next step with data
navigate('/register/step2', {
state: {
step1: { email, password }
}
});
};
return (
<form onSubmit={handleSubmit}>
<h2>Step 1: Account Details</h2>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit">Next Step →</button>
</form>
);
}
function RegistrationStep2() {
const navigate = useNavigate();
const location = useLocation();
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// Get data from previous step
const previousData = location.state as { step1: FormData['step1'] };
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Navigate to next step with accumulated data
navigate('/register/step3', {
state: {
...previousData,
step2: { firstName, lastName }
}
});
};
const handleBack = () => {
// Go back but preserve data
navigate(-1);
};
return (
<form onSubmit={handleSubmit}>
<h2>Step 2: Personal Information</h2>
<input
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="First Name"
required
/>
<input
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Last Name"
required
/>
<div className="button-group">
<button type="button" onClick={handleBack}>
← Back
</button>
<button type="submit">
Next Step →
</button>
</div>
</form>
);
}
✅ useNavigate Best Practices
- Use replace for redirects - Prevent users from going back to redirect pages
- Don't navigate in render - Only navigate in event handlers or effects
- Handle navigation errors - Wrap navigate calls in try-catch when needed
- Pass minimal state - Only pass necessary data through location state
- Type your state - Define TypeScript interfaces for location state
🏋️ Exercise: Shopping Cart Checkout
Build a checkout flow that uses programmatic navigation to guide users through the purchase process.
Requirements:
- Create three routes: /cart, /checkout, /success
- From cart, navigate to checkout with cart items in state
- On successful payment, navigate to success page with order details
- Use replace: true when navigating to success page
- Add a "Back to Cart" button on checkout page
- Properly type all navigation state with TypeScript
💡 Hint
Define your state types first:
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CheckoutState {
items: CartItem[];
total: number;
}
interface OrderState extends CheckoutState {
orderId: string;
orderDate: string;
}
✅ Solution
import { useNavigate, useLocation } from 'react-router-dom';
import { useState } from 'react';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CheckoutState {
items: CartItem[];
total: number;
}
// Cart Component
function Cart() {
const navigate = useNavigate();
const [items] = useState<CartItem[]>([
{ id: 1, name: 'Laptop', price: 999, quantity: 1 },
{ id: 2, name: 'Mouse', price: 29, quantity: 2 }
]);
const total = items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const handleCheckout = () => {
navigate('/checkout', {
state: { items, total } as CheckoutState
});
};
return (
<div>
<h1>Shopping Cart</h1>
{items.map(item => (
<div key={item.id}>
{item.name} - ${item.price} x {item.quantity}
</div>
))}
<p>Total: ${total.toFixed(2)}</p>
<button onClick={handleCheckout}>
Proceed to Checkout
</button>
</div>
);
}
// Checkout Component
function Checkout() {
const navigate = useNavigate();
const location = useLocation();
const state = location.state as CheckoutState;
if (!state || !state.items) {
// Redirect if accessed directly
navigate('/cart', { replace: true });
return null;
}
const handlePayment = () => {
// Simulate payment processing
const orderId = `ORD-${Date.now()}`;
navigate('/success', {
replace: true, // Can't go back to checkout
state: {
...state,
orderId,
orderDate: new Date().toISOString()
}
});
};
const handleBackToCart = () => {
navigate('/cart');
};
return (
<div>
<h1>Checkout</h1>
<h3>Order Summary</h3>
{state.items.map(item => (
<div key={item.id}>
{item.name} - ${item.price} x {item.quantity}
</div>
))}
<p>Total: ${state.total.toFixed(2)}</p>
<div className="button-group">
<button onClick={handleBackToCart}>
← Back to Cart
</button>
<button onClick={handlePayment}>
Complete Payment
</button>
</div>
</div>
);
}
// Success Component
function OrderSuccess() {
const location = useLocation();
const state = location.state as CheckoutState & {
orderId: string;
orderDate: string;
};
if (!state) {
// Redirect if accessed directly
navigate('/cart', { replace: true });
return null;
}
return (
<div>
<h1>✅ Order Complete!</h1>
<p>Order ID: {state.orderId}</p>
<p>Date: {new Date(state.orderDate).toLocaleDateString()}</p>
<p>Total: ${state.total.toFixed(2)}</p>
</div>
);
}
📍 The useLocation Hook
The useLocation hook gives you access to the current location object, which contains information about the current URL, state passed through navigation, and more.
📖 Location Object Properties
- pathname - The path of the URL (e.g., "/users/42")
- search - The query string (e.g., "?sort=name&order=asc")
- hash - The URL hash (e.g., "#section3")
- state - State passed from previous navigation
- key - Unique key for this location entry
Basic useLocation Usage
Access location information in your components:
import { useLocation } from 'react-router-dom';
function CurrentPath() {
const location = useLocation();
return (
<div>
<h3>Current Location Info</h3>
<p>Pathname: {location.pathname}</p>
<p>Search: {location.search}</p>
<p>Hash: {location.hash}</p>
<p>Key: {location.key}</p>
</div>
);
}
// Example URL: /products?category=electronics#reviews
// Pathname: /products
// Search: ?category=electronics
// Hash: #reviews
Accessing Navigation State
Retrieve state that was passed during navigation:
import { useLocation, useNavigate } from 'react-router-dom';
// Component that navigates WITH state
function ProductList() {
const navigate = useNavigate();
const handleProductClick = (productId: number) => {
navigate(`/products/${productId}`, {
state: {
fromPage: 'listing',
category: 'electronics',
scrollPosition: window.scrollY
}
});
};
return <div>{/* Product list */}</div>;
}
// Component that receives the state
interface LocationState {
fromPage?: string;
category?: string;
scrollPosition?: number;
}
function ProductDetail() {
const location = useLocation();
const navigate = useNavigate();
// Type assertion for the state
const state = location.state as LocationState;
const handleBack = () => {
if (state?.fromPage === 'listing') {
// Go back to listing
navigate('/products', {
state: {
scrollTo: state.scrollPosition
}
});
} else {
// Default back behavior
navigate(-1);
}
};
return (
<div>
<button onClick={handleBack}>
{state?.fromPage ? `← Back to ${state.fromPage}` : '← Back'}
</button>
{state?.category && (
<p>Category: {state.category}</p>
)}
</div>
);
}
⚠️ Location State Warning
Location state is stored in browser memory and will be lost on page refresh. Don't rely on it for critical data:
- ✅ Good: UI state, scroll position, previous page info
- ❌ Bad: User data, authentication tokens, form inputs
For persistent data, use URL parameters, localStorage, or fetch data based on URL params.
Conditional Rendering Based on Location
Use location to render different UI based on the current route:
import { useLocation } from 'react-router-dom';
function Navigation() {
const location = useLocation();
// Highlight active navigation item
const isActive = (path: string) => {
return location.pathname === path;
};
// Check if we're in a specific section
const inAdminSection = location.pathname.startsWith('/admin');
return (
<nav>
<a
href="/"
className={isActive('/') ? 'active' : ''}
>
Home
</a>
<a
href="/products"
className={isActive('/products') ? 'active' : ''}
>
Products
</a>
<a
href="/about"
className={isActive('/about') ? 'active' : ''}
>
About
</a>
{inAdminSection && (
<a href="/admin/dashboard">
Admin Dashboard
</a>
)}
</nav>
);
}
Tracking Location Changes
React to location changes with useEffect:
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';
function PageTracker() {
const location = useLocation();
useEffect(() => {
// Track page view
console.log('Page view:', location.pathname);
// Send analytics
if (window.gtag) {
window.gtag('config', 'GA_MEASUREMENT_ID', {
page_path: location.pathname + location.search
});
}
// Scroll to top on route change
window.scrollTo(0, 0);
// Or scroll to hash if present
if (location.hash) {
const element = document.querySelector(location.hash);
element?.scrollIntoView({ behavior: 'smooth' });
}
}, [location]);
return null; // This component doesn't render anything
}
Practical Example: Breadcrumbs
Build dynamic breadcrumbs based on the current location:
import { useLocation, Link } from 'react-router-dom';
function Breadcrumbs() {
const location = useLocation();
// Split pathname into segments
const pathnames = location.pathname.split('/').filter(x => x);
// Build breadcrumb items
const breadcrumbs = pathnames.map((segment, index) => {
// Build the path up to this segment
const path = `/${pathnames.slice(0, index + 1).join('/')}`;
// Format the segment name
const name = segment
.replace(/-/g, ' ')
.replace(/\b\w/g, char => char.toUpperCase());
return { path, name };
});
return (
<nav aria-label="Breadcrumb">
<ol className="breadcrumb">
<li>
<Link to="/">Home</Link>
</li>
{breadcrumbs.map((crumb, index) => {
const isLast = index === breadcrumbs.length - 1;
return (
<li key={crumb.path}>
{isLast ? (
<span aria-current="page">{crumb.name}</span>
) : (
<Link to={crumb.path}>{crumb.name}</Link>
)}
</li>
);
})}
</ol>
</nav>
);
}
// Example usage:
// URL: /products/electronics/laptops
// Renders: Home > Products > Electronics > Laptops
Location with Authentication
Store and retrieve redirect paths for authentication flows:
import { useLocation, useNavigate } from 'react-router-dom';
import { useEffect } from 'react';
// Protected Route Component
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const location = useLocation();
const navigate = useNavigate();
const isAuthenticated = checkAuth(); // Your auth check
useEffect(() => {
if (!isAuthenticated) {
// Redirect to login, storing the intended destination
navigate('/login', {
state: { from: location.pathname },
replace: true
});
}
}, [isAuthenticated, location, navigate]);
return isAuthenticated ? <>{children}</> : null;
}
// Login Component
interface LoginLocationState {
from?: string;
}
function Login() {
const location = useLocation();
const navigate = useNavigate();
const state = location.state as LoginLocationState;
const from = state?.from || '/dashboard';
const handleLogin = async (credentials: LoginCredentials) => {
const success = await performLogin(credentials);
if (success) {
// Navigate to the page they were trying to access
navigate(from, { replace: true });
}
};
return (
<div>
<h1>Login</h1>
{state?.from && (
<p>Please log in to access {state.from}</p>
)}
<LoginForm onSubmit={handleLogin} />
</div>
);
}
✅ useLocation Best Practices
- Type your state - Always define TypeScript interfaces for location state
- Handle missing state - Check if state exists before using it
- Use for ephemeral data - Only pass temporary UI state, not critical data
- Track in useEffect - Use location as dependency for side effects
- Combine with other hooks - Often used together with useNavigate and useParams
🏋️ Exercise: Return-to-Previous Functionality
Create a "Return to where you came from" feature that remembers the previous page.
Requirements:
- Create a SearchResults component that passes search context
- Create a ProductDetail component that receives the context
- Add a smart "Back" button that shows different text based on where user came from
- If from search, show "Back to Search Results"
- If from category, show "Back to Category"
- Otherwise, show generic "Back"
- Properly type all location state
💡 Hint
Define a state interface with optional properties:
interface NavigationState {
from?: 'search' | 'category' | 'home';
searchQuery?: string;
categoryName?: string;
}
✅ Solution
import { useLocation, useNavigate, Link } from 'react-router-dom';
interface NavigationState {
from?: 'search' | 'category' | 'home';
searchQuery?: string;
categoryName?: string;
}
// Search Results Component
function SearchResults() {
const [query] = useState('laptop');
const products = [
{ id: 1, name: 'Gaming Laptop' },
{ id: 2, name: 'Business Laptop' }
];
return (
<div>
<h1>Search Results for "{query}"</h1>
{products.map(product => (
<Link
key={product.id}
to={`/products/${product.id}`}
state={{
from: 'search',
searchQuery: query
} as NavigationState}
>
{product.name}
</Link>
))}
</div>
);
}
// Category Component
function Category() {
const categoryName = 'Electronics';
const products = [
{ id: 1, name: 'Gaming Laptop' },
{ id: 3, name: 'Wireless Mouse' }
];
return (
<div>
<h1>{categoryName}</h1>
{products.map(product => (
<Link
key={product.id}
to={`/products/${product.id}`}
state={{
from: 'category',
categoryName
} as NavigationState}
>
{product.name}
</Link>
))}
</div>
);
}
// Product Detail Component
function ProductDetail() {
const location = useLocation();
const navigate = useNavigate();
const { productId } = useParams<{ productId: string }>();
const state = location.state as NavigationState;
// Determine back button text and action
const getBackButton = () => {
if (state?.from === 'search') {
return {
text: `← Back to Search Results "${state.searchQuery}"`,
path: `/search?q=${state.searchQuery}`
};
}
if (state?.from === 'category') {
return {
text: `← Back to ${state.categoryName}`,
path: `/categories/${state.categoryName?.toLowerCase()}`
};
}
if (state?.from === 'home') {
return {
text: '← Back to Home',
path: '/'
};
}
// Default fallback
return {
text: '← Back',
path: null // Will use navigate(-1)
};
};
const backButton = getBackButton();
const handleBack = () => {
if (backButton.path) {
navigate(backButton.path);
} else {
navigate(-1);
}
};
return (
<div>
<button onClick={handleBack}>
{backButton.text}
</button>
<h1>Product {productId}</h1>
<p>Product details here...</p>
{state?.from && (
<p className="breadcrumb-info">
You came from: {state.from}
</p>
)}
</div>
);
}
📝 Building a Blog with Advanced Routing
Let's put everything together by building a complete blog application that demonstrates nested routes, dynamic parameters, and programmatic navigation.
Blog Application Structure
Our blog will have the following route structure:
// Route structure:
/ // Home page
/blog // Blog listing
/blog/:postId // Individual post
/blog/category/:category // Posts by category
/blog/author/:authorId // Posts by author
/blog/edit/:postId // Edit post
/blog/new // Create new post
Complete Blog Application
Here's the full implementation with all advanced routing features:
import {
BrowserRouter,
Routes,
Route,
Link,
useParams,
useNavigate,
useLocation
} from 'react-router-dom';
import { useState, useEffect } from 'react';
// Types
interface BlogPost {
id: number;
title: string;
content: string;
category: string;
authorId: number;
date: string;
}
interface Author {
id: number;
name: string;
}
// Mock data
const authors: Record<number, Author> = {
1: { id: 1, name: 'Jane Smith' },
2: { id: 2, name: 'John Doe' }
};
const mockPosts: BlogPost[] = [
{
id: 1,
title: 'Getting Started with React Router',
content: 'React Router is a powerful routing library...',
category: 'React',
authorId: 1,
date: '2024-01-15'
},
{
id: 2,
title: 'TypeScript Best Practices',
content: 'TypeScript helps us write safer code...',
category: 'TypeScript',
authorId: 2,
date: '2024-01-20'
},
{
id: 3,
title: 'Advanced Hooks Patterns',
content: 'Custom hooks are reusable pieces...',
category: 'React',
authorId: 1,
date: '2024-01-25'
}
];
// Blog Home - Shows all posts
function BlogHome() {
const [posts] = useState<BlogPost[]>(mockPosts);
// Get unique categories
const categories = [...new Set(posts.map(p => p.category))];
return (
<div className="blog-home">
<h1>📝 My Blog</h1>
<nav className="category-nav">
<h3>Categories</h3>
{categories.map(category => (
<Link
key={category}
to={`/blog/category/${category}`}
>
{category}
</Link>
))}
</nav>
<div className="posts-list">
<h2>Recent Posts</h2>
{posts.map(post => (
<article key={post.id} className="post-preview">
<h3>
<Link
to={`/blog/${post.id}`}
state={{ from: 'home' }}
>
{post.title}
</Link>
</h3>
<p>{post.content.substring(0, 100)}...</p>
<div className="post-meta">
<span>{post.category}</span>
<span>By {authors[post.authorId].name}</span>
<span>{post.date}</span>
</div>
</article>
))}
</div>
<Link to="/blog/new" className="create-post-btn">
➕ Create New Post
</Link>
</div>
);
}
// Individual Post View
function BlogPost() {
const { postId } = useParams<{ postId: string }>();
const location = useLocation();
const navigate = useNavigate();
const [post, setPost] = useState<BlogPost | null>(null);
const state = location.state as { from?: string };
useEffect(() => {
if (!postId) return;
const postIdNum = parseInt(postId, 10);
const foundPost = mockPosts.find(p => p.id === postIdNum);
setPost(foundPost || null);
}, [postId]);
if (!post) {
return <div>Post not found</div>;
}
const author = authors[post.authorId];
const handleEdit = () => {
navigate(`/blog/edit/${post.id}`, {
state: { post }
});
};
const handleBack = () => {
if (state?.from) {
navigate(-1);
} else {
navigate('/blog');
}
};
return (
<article className="blog-post">
<button onClick={handleBack}>← Back</button>
<header>
<h1>{post.title}</h1>
<div className="post-meta">
<Link to={`/blog/category/${post.category}`}>
{post.category}
</Link>
<span> | </span>
<Link to={`/blog/author/${post.authorId}`}>
{author.name}
</Link>
<span> | {post.date}</span>
</div>
</header>
<div className="post-content">
{post.content}
</div>
<button onClick={handleEdit}>
✏️ Edit Post
</button>
</article>
);
}
// Posts by Category
function CategoryPosts() {
const { category } = useParams<{ category: string }>();
const [posts, setPosts] = useState<BlogPost[]>([]);
useEffect(() => {
if (!category) return;
const filtered = mockPosts.filter(
p => p.category.toLowerCase() === category.toLowerCase()
);
setPosts(filtered);
}, [category]);
return (
<div>
<Link to="/blog">← Back to Blog</Link>
<h1>Posts in {category}</h1>
{posts.length === 0 ? (
<p>No posts in this category yet.</p>
) : (
<div className="posts-list">
{posts.map(post => (
<article key={post.id}>
<h3>
<Link
to={`/blog/${post.id}`}
state={{ from: `category-${category}` }}
>
{post.title}
</Link>
</h3>
<p>{post.content.substring(0, 100)}...</p>
</article>
))}
</div>
)}
</div>
);
}
// Edit Post
interface EditPostLocationState {
post?: BlogPost;
}
function EditPost() {
const { postId } = useParams<{ postId: string }>();
const location = useLocation();
const navigate = useNavigate();
const state = location.state as EditPostLocationState;
const [title, setTitle] = useState(state?.post?.title || '');
const [content, setContent] = useState(state?.post?.content || '');
useEffect(() => {
// If no state, fetch the post
if (!state?.post && postId) {
const postIdNum = parseInt(postId, 10);
const post = mockPosts.find(p => p.id === postIdNum);
if (post) {
setTitle(post.title);
setContent(post.content);
}
}
}, [postId, state]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Save post logic here
console.log('Saving post:', { title, content });
// Navigate back to post with success message
navigate(`/blog/${postId}`, {
state: { message: 'Post updated successfully!' }
});
};
return (
<div className="edit-post">
<h1>Edit Post</h1>
<form onSubmit={handleSubmit}>
<div>
<label>Title</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div>
<label>Content</label>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
rows={10}
required
/>
</div>
<div className="button-group">
<button
type="button"
onClick={() => navigate(-1)}
>
Cancel
</button>
<button type="submit">
Save Changes
</button>
</div>
</form>
</div>
);
}
// App with all routes
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/blog" element={<BlogHome />} />
<Route path="/blog/:postId" element={<BlogPost />} />
<Route path="/blog/category/:category" element={<CategoryPosts />} />
<Route path="/blog/edit/:postId" element={<EditPost />} />
</Routes>
</BrowserRouter>
);
}
✅ Key Patterns Demonstrated
- Dynamic routes -
:postId,:categoryparameters - useParams - Accessing route parameters in components
- useNavigate - Programmatic navigation after actions
- useLocation - Passing and retrieving navigation state
- Smart back buttons - Context-aware navigation
- TypeScript typing - Properly typed params and state
🎨 Advanced Patterns and Techniques
Now that we've covered the core hooks, let's explore some advanced patterns and techniques for building sophisticated routing solutions.
Combining All Three Hooks
Often, you'll use useParams, useNavigate, and useLocation together for complex navigation scenarios:
import { useParams, useNavigate, useLocation } from 'react-router-dom';
interface ProductDetailState {
from?: string;
category?: string;
filters?: Record<string, string>;
}
function ProductDetail() {
const { productId } = useParams<{ productId: string }>();
const navigate = useNavigate();
const location = useLocation();
const state = location.state as ProductDetailState;
const [product, setProduct] = useState<Product | null>(null);
// Fetch product using params
useEffect(() => {
if (!productId) return;
fetchProduct(productId).then(setProduct);
}, [productId]);
// Handle adding to cart with navigation
const handleAddToCart = () => {
addToCart(product);
// Navigate to cart, preserving where they came from
navigate('/cart', {
state: {
from: location.pathname,
returnTo: state?.from || '/products'
}
});
};
// Smart back button using location state
const handleBack = () => {
if (state?.from === 'search' && state?.filters) {
// Return to search with filters
const params = new URLSearchParams(state.filters);
navigate(`/search?${params.toString()}`);
} else if (state?.category) {
// Return to category
navigate(`/category/${state.category}`);
} else {
// Default back
navigate(-1);
}
};
if (!product) {
return <div>Loading...</div>;
}
return (
<div>
<button onClick={handleBack}>
{state?.from ? `← Back to ${state.from}` : '← Back'}
</button>
<h1>{product.name}</h1>
<p>${product.price}</p>
<button onClick={handleAddToCart}>
Add to Cart
</button>
</div>
);
}
Type-Safe Route Parameters
Create a centralized route configuration with TypeScript to ensure type safety across your app:
// routes.ts - Centralized route definitions
export const ROUTES = {
home: '/',
products: {
list: '/products',
detail: (id: number | string) => `/products/${id}`,
edit: (id: number | string) => `/products/${id}/edit`,
category: (category: string) => `/products/category/${category}`,
},
user: {
profile: (userId: number | string) => `/users/${userId}`,
settings: '/settings',
},
blog: {
list: '/blog',
post: (postId: number | string) => `/blog/${postId}`,
category: (category: string) => `/blog/category/${category}`,
author: (authorId: number | string) => `/blog/author/${authorId}`,
}
} as const;
// Usage in components
import { ROUTES } from './routes';
import { useNavigate } from 'react-router-dom';
function ProductList() {
const navigate = useNavigate();
const handleProductClick = (productId: number) => {
// Type-safe navigation
navigate(ROUTES.products.detail(productId));
};
return (
<div>
<Link to={ROUTES.products.list}>All Products</Link>
<Link to={ROUTES.products.category('electronics')}>
Electronics
</Link>
</div>
);
}
📖 Benefits of Centralized Routes
- Type safety - Catch invalid routes at compile time
- Refactoring - Change URLs in one place
- Autocomplete - IDE suggestions for all routes
- Documentation - Clear overview of all routes
- Consistency - Enforce URL structure patterns
Custom Navigation Hook
Create a custom hook that wraps useNavigate with common navigation patterns:
import { useNavigate, NavigateOptions } from 'react-router-dom';
import { useCallback } from 'react';
interface NavigationState {
from?: string;
[key: string]: unknown;
}
export function useAppNavigation() {
const navigate = useNavigate();
// Navigate with automatic "from" tracking
const navigateTo = useCallback((
path: string,
state?: NavigationState,
options?: NavigateOptions
) => {
navigate(path, {
...options,
state: {
from: window.location.pathname,
...state
}
});
}, [navigate]);
// Navigate back with fallback
const navigateBack = useCallback((fallbackPath: string = '/') => {
if (window.history.length > 1) {
navigate(-1);
} else {
navigate(fallbackPath);
}
}, [navigate]);
// Navigate and scroll to top
const navigateAndScroll = useCallback((
path: string,
state?: NavigationState
) => {
navigate(path, { state });
window.scrollTo(0, 0);
}, [navigate]);
// Replace current route (for redirects)
const redirect = useCallback((path: string) => {
navigate(path, { replace: true });
}, [navigate]);
return {
navigateTo,
navigateBack,
navigateAndScroll,
redirect,
navigate // Still expose raw navigate if needed
};
}
// Usage
function MyComponent() {
const { navigateTo, navigateBack } = useAppNavigation();
const handleAction = () => {
// Automatically tracks "from" location
navigateTo('/products/123', {
category: 'electronics'
});
};
return (
<div>
<button onClick={handleAction}>View Product</button>
<button onClick={() => navigateBack('/home')}>
Back
</button>
</div>
);
}
Route Parameter Validation
Validate and parse route parameters with a custom hook:
import { useParams, useNavigate } from 'react-router-dom';
import { useEffect } from 'react';
// Generic parameter validator
function useValidatedParams<T extends Record<string, unknown>>(
validators: {
[K in keyof T]: (value: string | undefined) => T[K] | null;
}
): T | null {
const params = useParams();
const navigate = useNavigate();
const validated = Object.entries(validators).reduce(
(acc, [key, validator]) => {
const value = params[key];
const validated = validator(value);
if (validated === null) {
return null;
}
return { ...acc, [key]: validated };
},
{} as T | null
);
useEffect(() => {
if (validated === null) {
// Redirect on invalid params
navigate('/404', { replace: true });
}
}, [validated, navigate]);
return validated;
}
// Validator functions
const validators = {
productId: (value: string | undefined): number | null => {
if (!value) return null;
const num = parseInt(value, 10);
return !isNaN(num) && num > 0 ? num : null;
},
userId: (value: string | undefined): number | null => {
if (!value) return null;
const num = parseInt(value, 10);
return !isNaN(num) && num > 0 ? num : null;
},
slug: (value: string | undefined): string | null => {
if (!value) return null;
// Validate slug format: lowercase, hyphens, alphanumeric
return /^[a-z0-9-]+$/.test(value) ? value : null;
}
};
// Usage
interface ProductParams {
productId: number;
slug: string;
}
function ProductDetail() {
const params = useValidatedParams<ProductParams>({
productId: validators.productId,
slug: validators.slug
});
if (!params) {
return null; // Will redirect to 404
}
// TypeScript knows productId is number and slug is string
return (
<div>
<h1>Product {params.productId}</h1>
<p>Slug: {params.slug}</p>
</div>
);
}
Navigation Guards
Implement navigation guards to control access to routes:
import { useLocation, useNavigate } from 'react-router-dom';
import { useEffect } from 'react';
interface GuardOptions {
requireAuth?: boolean;
requireRole?: string[];
redirectTo?: string;
}
function useNavigationGuard(options: GuardOptions) {
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
const checkAccess = () => {
// Check authentication
if (options.requireAuth) {
const isAuthenticated = checkAuth(); // Your auth check
if (!isAuthenticated) {
navigate(options.redirectTo || '/login', {
state: { from: location.pathname },
replace: true
});
return false;
}
}
// Check roles
if (options.requireRole) {
const userRole = getUserRole(); // Your role check
if (!options.requireRole.includes(userRole)) {
navigate('/unauthorized', { replace: true });
return false;
}
}
return true;
};
checkAccess();
}, [location, navigate, options]);
}
// Usage
function AdminDashboard() {
useNavigationGuard({
requireAuth: true,
requireRole: ['admin', 'moderator'],
redirectTo: '/login'
});
return <div>Admin Dashboard</div>;
}
function UserProfile() {
useNavigationGuard({
requireAuth: true,
redirectTo: '/login'
});
return <div>User Profile</div>;
}
Breadcrumb Hook
Create a reusable breadcrumb hook:
import { useLocation } from 'react-router-dom';
import { useMemo } from 'react';
interface Breadcrumb {
path: string;
label: string;
isLast: boolean;
}
// Route label mapping
const routeLabels: Record<string, string> = {
'/products': 'Products',
'/users': 'Users',
'/settings': 'Settings',
'/blog': 'Blog',
'/admin': 'Administration',
'/dashboard': 'Dashboard'
};
function useBreadcrumbs(): Breadcrumb[] {
const location = useLocation();
const breadcrumbs = useMemo(() => {
const pathnames = location.pathname.split('/').filter(x => x);
if (pathnames.length === 0) {
return [];
}
return pathnames.map((segment, index) => {
const path = `/${pathnames.slice(0, index + 1).join('/')}`;
const isLast = index === pathnames.length - 1;
// Try to get custom label, or format the segment
const label = routeLabels[path] ||
segment
.replace(/-/g, ' ')
.replace(/\b\w/g, char => char.toUpperCase());
return { path, label, isLast };
});
}, [location.pathname]);
return breadcrumbs;
}
// Usage
function BreadcrumbNav() {
const breadcrumbs = useBreadcrumbs();
if (breadcrumbs.length === 0) {
return null;
}
return (
<nav className="breadcrumb">
<Link to="/">Home</Link>
{breadcrumbs.map(crumb => (
<span key={crumb.path}>
<span className="separator"> / </span>
{crumb.isLast ? (
<span>{crumb.label}</span>
) : (
<Link to={crumb.path}>{crumb.label}</Link>
)}
</span>
))}
</nav>
);
}
✅ Advanced Pattern Best Practices
- Centralize route definitions - Use constants for all routes
- Create custom hooks - Wrap common patterns in reusable hooks
- Validate parameters - Don't trust URL parameters, validate them
- Handle edge cases - Always have fallbacks for missing data
- Type everything - Use TypeScript for all route-related code
- Guard routes - Protect sensitive routes with navigation guards
🏋️ Exercise: Advanced Blog Navigation
Enhance the blog application with advanced navigation features.
Requirements:
- Create a centralized ROUTES object with all blog routes
- Create a custom
useBlogNavigationhook - Add route parameter validation for postId (must be positive integer)
- Implement a breadcrumb component using
useLocation - Add navigation guards for the edit post route
- Track navigation history and show "recently viewed posts"
💡 Hint
Structure your solution in steps:
- Define ROUTES object first
- Create custom navigation hook
- Add validation logic
- Implement breadcrumbs
- Add guards last
✅ Solution
// routes.ts
export const ROUTES = {
home: '/',
blog: {
home: '/blog',
post: (id: number | string) => `/blog/${id}`,
category: (cat: string) => `/blog/category/${cat}`,
edit: (id: number | string) => `/blog/edit/${id}`,
new: '/blog/new'
}
} as const;
// useBlogNavigation.ts
export function useBlogNavigation() {
const navigate = useNavigate();
const location = useLocation();
const toPost = useCallback((
postId: number,
from?: string
) => {
navigate(ROUTES.blog.post(postId), {
state: { from: from || location.pathname }
});
}, [navigate, location]);
const toCategory = useCallback((category: string) => {
navigate(ROUTES.blog.category(category));
}, [navigate]);
const toEdit = useCallback((postId: number) => {
navigate(ROUTES.blog.edit(postId));
}, [navigate]);
return { toPost, toCategory, toEdit };
}
// useValidatedPostId.ts
function useValidatedPostId(): number | null {
const { postId } = useParams<{ postId: string }>();
const navigate = useNavigate();
const validated = useMemo(() => {
if (!postId) return null;
const num = parseInt(postId, 10);
return !isNaN(num) && num > 0 ? num : null;
}, [postId]);
useEffect(() => {
if (validated === null) {
navigate('/blog', { replace: true });
}
}, [validated, navigate]);
return validated;
}
// BlogPost.tsx
function BlogPost() {
const postId = useValidatedPostId();
const { toEdit } = useBlogNavigation();
if (!postId) return null;
return (
<article>
<BlogBreadcrumbs />
<h1>Post {postId}</h1>
<button onClick={() => toEdit(postId)}>
Edit
</button>
</article>
);
}
// BlogBreadcrumbs.tsx
function BlogBreadcrumbs() {
const location = useLocation();
const segments = location.pathname.split('/').filter(x => x);
return (
<nav>
<Link to={ROUTES.home}>Home</Link>
{segments.map((seg, i) => (
<span key={i}>
{' / '}
<Link to={`/${segments.slice(0, i + 1).join('/')}`}>
{seg}
</Link>
</span>
))}
</nav>
);
}
// EditPost.tsx
function EditPost() {
const postId = useValidatedPostId();
// Navigation guard
useEffect(() => {
const canEdit = checkEditPermissions(postId);
if (!canEdit) {
navigate('/unauthorized', { replace: true });
}
}, [postId]);
if (!postId) return null;
return <div>Edit Post {postId}</div>;
}
🎯 Summary and Next Steps
What You've Learned
In this lesson, you've mastered advanced React Router concepts:
📚 Key Concepts Covered
- Nested Routes - Building hierarchical route structures with shared layouts
- Dynamic Parameters - Creating flexible routes with URL parameters
- useParams Hook - Accessing and typing route parameters with TypeScript
- useNavigate Hook - Programmatic navigation and state passing
- useLocation Hook - Accessing current location and navigation state
- Advanced Patterns - Custom hooks, validation, guards, and centralized routes
Quick Reference
| Hook | Purpose | Common Use Cases |
|---|---|---|
useParams |
Access URL parameters | User IDs, post slugs, product IDs |
useNavigate |
Navigate programmatically | Form submissions, redirects, after actions |
useLocation |
Access current location | Breadcrumbs, analytics, state passing |
Common Patterns Cheat Sheet
// Pattern 1: Get URL parameter
const { userId } = useParams<{ userId: string }>();
// Pattern 2: Navigate after action
const navigate = useNavigate();
const handleSubmit = () => {
// ... save data
navigate('/success');
};
// Pattern 3: Navigate with state
navigate('/products/123', {
state: { from: 'search', query: 'laptop' }
});
// Pattern 4: Navigate with replace
navigate('/login', { replace: true });
// Pattern 5: Go back
navigate(-1);
// Pattern 6: Access location state
const location = useLocation();
const state = location.state as MyStateType;
// Pattern 7: Conditional back button
const handleBack = () => {
if (state?.from) {
navigate(state.from);
} else {
navigate(-1);
}
};
// Pattern 8: Validate parameters
const postId = parseInt(useParams().postId || '', 10);
if (isNaN(postId)) {
navigate('/404', { replace: true });
}
Best Practices Checklist
✅ Do's
- Always type your route parameters with TypeScript
- Validate URL parameters before using them
- Handle missing or invalid parameters gracefully
- Use
replace: truefor redirects - Create custom hooks for common patterns
- Centralize route definitions
- Pass minimal state through navigation
- Provide fallback navigation for edge cases
❌ Don'ts
- Don't assume parameters are always present
- Don't store sensitive data in location state
- Don't navigate during render (use useEffect)
- Don't use
anytype for parameters - Don't rely on location state after refresh
- Don't forget to handle navigation errors
- Don't hardcode URLs (use constants)
Common Gotchas
⚠️ Watch Out For
1. Parameters are always strings
const { userId } = useParams();
// userId is "42", not 42
const userIdNum = parseInt(userId || '0', 10);
2. Location state disappears on refresh
// ❌ Don't rely on this after refresh
const state = location.state;
// ✅ Always have a fallback
const userId = state?.userId || fetchFromUrl();
3. Optional parameters
// Route: /users/:userId/:tab?
const { tab = 'overview' } = useParams();
// Provide default for optional params
4. Navigation in render causes errors
// ❌ Don't do this
if (!user) navigate('/login');
// ✅ Use useEffect
useEffect(() => {
if (!user) navigate('/login');
}, [user]);
Practice Projects
Solidify your understanding with these practice projects:
-
E-commerce Product Catalog
- Product listing with category filters
- Product detail pages with dynamic routes
- Shopping cart with navigation state
- Checkout flow with multi-step navigation
-
User Dashboard
- User profile with tabbed navigation
- Settings pages with nested routes
- Activity feed with pagination in URL
- Smart breadcrumbs
-
Documentation Site
- Hierarchical documentation structure
- Search with URL query parameters
- Version selection in routes
- Previous/next page navigation
Next Lesson Preview
🔜 Coming Up: Lesson 6.3 - Route Protection and Loading
In the next lesson, you'll learn:
- Protected routes and authentication
- Route guards and permissions
- Lazy loading routes for better performance
- React Suspense with routing
- Loading states and error boundaries
- Redirect patterns and strategies
Additional Resources
- React Router: useParams Documentation
- React Router: useNavigate Documentation
- React Router: useLocation Documentation
- TypeScript: Type Narrowing
- React: Context (for comparison with routing state)
🎉 Congratulations!
You've completed Lesson 6.2 and now have a solid understanding of advanced React Router concepts. You can:
- ✅ Build complex nested route structures
- ✅ Work with dynamic route parameters
- ✅ Navigate programmatically with full control
- ✅ Manage navigation state effectively
- ✅ Create type-safe routing with TypeScript
- ✅ Implement advanced routing patterns
Keep practicing and experimenting with these patterns!