React.js Fundamentals

React Router: Advanced Routing

18 min Lesson 17 of 40

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 ProtectedRoute component
  • Protect /dashboard and /profile routes
  • 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 useSearchParams to 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 useNavigate for 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 Navigate component
  • 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.