React Router: Advanced Routing
Introduction to Advanced Routing
This lesson covers advanced React Router techniques including programmatic navigation, route parameters extraction, protected routes, redirects, and code splitting with lazy loading. These patterns are essential for building complex, production-ready applications.
Programmatic Navigation with useNavigate
The useNavigate hook allows you to navigate programmatically in response to events or logic:
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const [credentials, setCredentials] = useState({
email: '',
password: ''
});
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (response.ok) {
const user = await response.json();
localStorage.setItem('user', JSON.stringify(user));
// Navigate to dashboard after successful login
navigate('/dashboard');
// Navigate with state
// navigate('/dashboard', { state: { from: 'login' } });
// Navigate and replace history (no back button)
// navigate('/dashboard', { replace: true });
}
} catch (error) {
console.error('Login failed:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={credentials.email}
onChange={(e) => setCredentials({ ...credentials, email: e.target.value })}
/>
<input
type="password"
value={credentials.password}
onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
/>
<button type="submit">Login</button>
{/* Programmatic navigation with Link alternative */}
<button type="button" onClick={() => navigate(-1)}>Go Back</button>
<button type="button" onClick={() => navigate(-2)}>Go Back 2 Pages</button>
</form>
);
}
Tip: Use navigate(-1) to go back one page, navigate(-2) for two pages, etc. Positive numbers go forward in history.
useLocation: Accessing Current Location
The useLocation hook provides access to the current URL location object:
import { useLocation, useNavigate } from 'react-router-dom';
function Dashboard() {
const location = useLocation();
const navigate = useNavigate();
// Access state passed during navigation
const fromPage = location.state?.from;
// Access search params
const searchParams = new URLSearchParams(location.search);
const tab = searchParams.get('tab') || 'overview';
console.log({
pathname: location.pathname, // '/dashboard'
search: location.search, // '?tab=profile'
hash: location.hash, // '#section1'
state: location.state, // { from: 'login' }
key: location.key // Unique key for this location
});
return (
<div>
<h1>Dashboard</h1>
{fromPage && <p>Welcome back from {fromPage}!</p>}
<div>
<button onClick={() => navigate('?tab=overview')}>Overview</button>
<button onClick={() => navigate('?tab=profile')}>Profile</button>
<button onClick={() => navigate('?tab=settings')}>Settings</button>
</div>
{tab === 'overview' && <Overview />}
{tab === 'profile' && <Profile />}
{tab === 'settings' && <Settings />}
</div>
);
}
// Using location for conditional rendering
function Navigation() {
const location = useLocation();
const isActive = (path) => location.pathname === path;
return (
<nav>
<Link
to="/"
className={isActive('/') ? 'active' : ''}
>
Home
</Link>
<Link
to="/about"
className={isActive('/about') ? 'active' : ''}
>
About
</Link>
</nav>
);
}
Protected Routes (Authentication)
Implement route protection to restrict access to authenticated users:
// src/components/ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
function ProtectedRoute({ children }) {
const location = useLocation();
const isAuthenticated = checkAuth(); // Your auth check function
if (!isAuthenticated) {
// Redirect to login, save attempted location
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// Auth check helper
function checkAuth() {
const user = localStorage.getItem('user');
return !!user;
}
// src/App.jsx
import { Routes, Route } from 'react-router-dom';
import ProtectedRoute from './components/ProtectedRoute';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* Protected single route */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
{/* Protected nested routes */}
<Route
path="/admin"
element={
<ProtectedRoute>
<AdminLayout />
</ProtectedRoute>
}
>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<Users />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
);
}
Note: The replace prop prevents the protected route from appearing in browser history, so users can't navigate back to it after logout.
Role-Based Route Protection
Extend protected routes to check user roles:
// src/components/RoleProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
function RoleProtectedRoute({ children, allowedRoles }) {
const user = JSON.parse(localStorage.getItem('user') || '{}');
const isAuthenticated = !!user.id;
const hasRequiredRole = allowedRoles.includes(user.role);
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
if (!hasRequiredRole) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
// Usage in App.jsx
function App() {
return (
<Routes>
{/* Admin-only routes */}
<Route
path="/admin/*"
element={
<RoleProtectedRoute allowedRoles={['admin']}>
<AdminPanel />
</RoleProtectedRoute>
}
/>
{/* Editor and Admin routes */}
<Route
path="/editor/*"
element={
<RoleProtectedRoute allowedRoles={['editor', 'admin']}>
<EditorPanel />
</RoleProtectedRoute>
}
/>
{/* Any authenticated user */}
<Route
path="/profile"
element={
<RoleProtectedRoute allowedRoles={['user', 'editor', 'admin']}>
<Profile />
</RoleProtectedRoute>
}
/>
</Routes>
);
}
Redirects and Navigate Component
Use the Navigate component for declarative redirects:
import { Routes, Route, Navigate } from 'react-router-dom';
function App() {
const isAuthenticated = checkAuth();
return (
<Routes>
{/* Redirect from old URL to new URL */}
<Route path="/old-about" element={<Navigate to="/about" replace />} />
{/* Conditional redirect */}
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/dashboard" /> : <Login />}
/>
{/* Redirect with state */}
<Route
path="/checkout"
element={
isAuthenticated
? <Checkout />
: <Navigate to="/login" state={{ returnTo: '/checkout' }} />
}
/>
{/* Redirect root to specific page */}
<Route path="/" element={<Navigate to="/home" replace />} />
<Route path="/home" element={<Home />} />
</Routes>
);
}
Warning: Avoid creating redirect loops. Always ensure your redirect logic has a clear termination point.
Lazy Loading Routes
Code-split your routes for better performance using React's lazy and Suspense:
import { Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// Eager loading (default)
import Home from './pages/Home';
import About from './pages/About';
// Lazy loading - loaded only when route is accessed
const Dashboard = lazy(() => import('./pages/Dashboard'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const Settings = lazy(() => import('./pages/Settings'));
// Loading component
function LoadingSpinner() {
return (
<div className="loading">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
}
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
{/* Eager loaded routes */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* Lazy loaded routes */}
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/profile/:userId" element={<UserProfile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Best Practice: Lazy load heavy components (dashboards, admin panels) and eagerly load critical routes (home, login) for optimal initial load time.
Search Params with useSearchParams
Manage query parameters with the useSearchParams hook:
import { useSearchParams } from 'react-router-dom';
function ProductsList() {
const [searchParams, setSearchParams] = useSearchParams();
// Read search params
const category = searchParams.get('category') || 'all';
const sort = searchParams.get('sort') || 'name';
const page = parseInt(searchParams.get('page') || '1', 10);
// Update search params
const handleCategoryChange = (newCategory) => {
setSearchParams({
category: newCategory,
sort,
page: '1' // Reset to page 1 on filter change
});
};
const handleSortChange = (newSort) => {
setSearchParams({
category,
sort: newSort,
page: String(page)
});
};
const handlePageChange = (newPage) => {
setSearchParams({
category,
sort,
page: String(newPage)
});
};
// Delete a param
const clearFilters = () => {
setSearchParams({});
};
return (
<div>
<h1>Products</h1>
{/* Current URL: /products?category=electronics&sort=price&page=2 */}
<div>
<select value={category} onChange={(e) => handleCategoryChange(e.target.value)}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<select value={sort} onChange={(e) => handleSortChange(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="date">Date</option>
</select>
<button onClick={clearFilters}>Clear Filters</button>
</div>
{/* Product list based on filters */}
<ProductGrid category={category} sort={sort} page={page} />
{/* Pagination */}
<button onClick={() => handlePageChange(page - 1)} disabled={page === 1}>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => handlePageChange(page + 1)}>
Next
</button>
</div>
);
}
Exercise 1: Authentication Flow
Task: Implement a complete authentication flow:
- Create a login page that stores a fake user object in localStorage
- Implement a
ProtectedRoutecomponent - Protect
/dashboardand/profileroutes - Redirect to login if not authenticated, saving the attempted route
- After login, redirect to the originally attempted route (or dashboard)
- Add a logout button that clears localStorage and redirects to home
Exercise 2: Advanced Search with URL Params
Task: Build a product search page with filters:
- Use
useSearchParamsto manage category, price range, and sort order - All filters should update the URL query string
- The page should work correctly when URL is shared (filters persist)
- Add a "Clear All Filters" button
- Implement pagination with page numbers in URL
Exercise 3: Lazy Loading Dashboard
Task: Optimize a dashboard application with lazy loading:
- Create separate components for Dashboard, Analytics, Reports, and Settings
- Lazy load all dashboard-related components
- Create a custom loading component with skeleton UI
- Eagerly load Home and Login pages
- Test the network tab to verify code splitting
Summary
In this lesson, you mastered advanced React Router techniques:
- Programmatic navigation with
useNavigatefor dynamic routing - Accessing current location data with
useLocation - Implementing protected routes with authentication checks
- Role-based access control for different user types
- Declarative redirects with the
Navigatecomponent - Code splitting and lazy loading for performance optimization
- Managing URL search parameters with
useSearchParams
In the next lesson, we'll explore different styling approaches in React, including CSS Modules, styled-components, and Tailwind CSS.