đ Lesson 6.3: Route Protection and Loading
Secure your application routes with authentication guards, optimize performance with lazy loading, and create smooth user experiences with loading states and error boundaries. Build production-ready routing architectures for real-world applications.
đ¯ Learning Objectives
By the end of this lesson, you will be able to:
- Implement protected routes that require authentication
- Create flexible redirect patterns for unauthorized access
- Use React.lazy() and Suspense for code splitting
- Optimize bundle size with lazy-loaded routes
- Implement loading states for better user experience
- Handle routing errors with error boundaries
- Build role-based access control for routes
- Type authentication and loading states properly
Estimated Time: 75-90 minutes
Prerequisites: Lessons 6.1 & 6.2 - React Router Basics and Advanced Routing
đ In This Lesson
đĄī¸ Protected Routes Overview
Protected routes are essential for securing parts of your application that require authentication or specific permissions. They control access to sensitive content and redirect unauthorized users appropriately.
đ Why Protected Routes Matter
- Security - Prevent unauthorized access to sensitive data
- User Experience - Guide users to login when needed
- Authorization - Control access based on user roles or permissions
- Data Protection - Ensure only authenticated users can view/modify data
- Session Management - Handle expired sessions gracefully
The Protected Route Pattern
A protected route checks authentication status before rendering. If the user is not authenticated, they're redirected to a login page:
Common Protected Route Scenarios
| Scenario | Protection Type | Example |
|---|---|---|
| User Dashboard | Authentication Required | /dashboard, /profile, /settings |
| Admin Panel | Role-Based Access | /admin/users, /admin/settings |
| Premium Content | Subscription Status | /premium/courses, /exclusive |
| Age-Restricted | Age Verification | /adult-content |
| Beta Features | Feature Flag | /beta/new-feature |
Basic Protected Route Concept
Here's a simple conceptual example of how protected routes work:
// Conceptual example - we'll build this properly in the next section
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = checkAuthStatus(); // Check if user is logged in
if (!isAuthenticated) {
// Redirect to login if not authenticated
return <Navigate to="/login" replace />;
}
// If authenticated, render the protected content
return <>{children}</>;
}
// Usage in routes
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
đĄ Key Concepts
- Authentication Check - Verify user is logged in before rendering
- Conditional Rendering - Show protected content or redirect based on auth state
- Preserve Destination - Remember where user wanted to go for post-login redirect
- Graceful Degradation - Provide clear feedback when access is denied
đ Implementing Authentication Guards
Let's build a production-ready protected route system with TypeScript, proper authentication checks, and redirect handling.
Setting Up Authentication Context
First, we'll create an authentication context to manage auth state across the application:
// contexts/AuthContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface User {
id: number;
email: string;
name: string;
role: 'user' | 'admin' | 'moderator';
}
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Check authentication status on mount
useEffect(() => {
const checkAuth = async () => {
try {
// Check if user has valid token in localStorage
const token = localStorage.getItem('authToken');
if (token) {
// Validate token with backend
const response = await fetch('/api/auth/validate', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
// Invalid token, clear it
localStorage.removeItem('authToken');
}
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setIsLoading(false);
}
};
checkAuth();
}, []);
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
const { user, token } = await response.json();
// Store token
localStorage.setItem('authToken', token);
// Update state
setUser(user);
} catch (error) {
console.error('Login error:', error);
throw error;
} finally {
setIsLoading(false);
}
};
const logout = () => {
localStorage.removeItem('authToken');
setUser(null);
};
const value: AuthContextType = {
user,
isAuthenticated: !!user,
isLoading,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Custom hook to use auth context
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Creating the ProtectedRoute Component
Now let's create a reusable ProtectedRoute component with proper TypeScript typing:
// components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
redirectTo?: string;
}
export function ProtectedRoute({
children,
redirectTo = '/login'
}: ProtectedRouteProps) {
const { isAuthenticated, isLoading } = useAuth();
const location = useLocation();
// Show loading spinner while checking authentication
if (isLoading) {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>Checking authentication...</p>
</div>
);
}
// If not authenticated, redirect to login
if (!isAuthenticated) {
// Save the location they were trying to access
return <Navigate to={redirectTo} state={{ from: location }} replace />;
}
// User is authenticated, render the protected content
return <>{children}</>;
}
â Key Features of This Implementation
- Loading State - Shows spinner while checking auth status
- Location Preservation - Saves where user wanted to go
- Customizable Redirect - Can specify different login routes
- Replace Navigation - Prevents back button issues
- TypeScript Safety - Properly typed props and context
Using Protected Routes in Your App
Here's how to set up your application with authentication and protected routes:
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
// Page components
import Home from './pages/Home';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import Profile from './pages/Profile';
import Settings from './pages/Settings';
import AdminPanel from './pages/AdminPanel';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* Protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
{/* Admin route - we'll add role checking later */}
<Route
path="/admin"
element={
<ProtectedRoute>
<AdminPanel />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
Creating the Login Component
The login component handles authentication and redirects users back to their intended destination:
// pages/Login.tsx
import { useState, FormEvent } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface LocationState {
from?: {
pathname: string;
};
}
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
// Get the location they came from, or default to dashboard
const state = location.state as LocationState;
const from = state?.from?.pathname || '/dashboard';
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
await login(email, password);
// Redirect to the page they were trying to access
navigate(from, { replace: true });
} catch (err) {
setError('Invalid email or password');
} finally {
setIsLoading(false);
}
};
return (
<div className="login-container">
<h1>Login</h1>
{state?.from && (
<p className="info-message">
Please log in to access {state.from.pathname}
</p>
)}
<form onSubmit={handleSubmit}>
{error && (
<div className="error-message">{error}</div>
)}
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isLoading}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isLoading}
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
);
}
â ī¸ Security Considerations
- Never trust client-side auth alone - Always validate on the server
- Use HTTPS - Protect tokens and credentials in transit
- Token expiration - Implement token refresh or re-authentication
- Secure storage - Consider httpOnly cookies instead of localStorage for tokens
- Input validation - Validate and sanitize all login inputs
đī¸ Exercise: Build Protected User Profile
Create a protected profile page that requires authentication.
Requirements:
- Create a UserProfile component that displays user information
- Wrap it in a ProtectedRoute
- Show loading state while checking authentication
- Redirect to /login if not authenticated
- After login, redirect back to profile page
- Add a logout button that clears auth and redirects to home
đĄ Hint
Use the useAuth hook to access user data and logout function:
const { user, logout } = useAuth();
const handleLogout = () => {
logout();
navigate('/');
};
â Solution
// pages/UserProfile.tsx
import { useAuth } from '../contexts/AuthContext';
import { useNavigate } from 'react-router-dom';
export default function UserProfile() {
const { user, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate('/', { replace: true });
};
if (!user) {
return <div>Loading user data...</div>;
}
return (
<div className="profile-container">
<h1>đ¤ User Profile</h1>
<div className="profile-info">
<div className="info-item">
<label>Name:</label>
<span>{user.name}</span>
</div>
<div className="info-item">
<label>Email:</label>
<span>{user.email}</span>
</div>
<div className="info-item">
<label>Role:</label>
<span>{user.role}</span>
</div>
<div className="info-item">
<label>User ID:</label>
<span>{user.id}</span>
</div>
</div>
<button onClick={handleLogout} className="logout-btn">
đĒ Logout
</button>
</div>
);
}
// App.tsx - Add this route
<Route
path="/profile"
element={
<ProtectedRoute>
<UserProfile />
</ProtectedRoute>
}
/>
đ Redirect Patterns and Strategies
Beyond basic authentication, there are many scenarios where you need to redirect users. Let's explore common redirect patterns and best practices.
Common Redirect Scenarios
| Scenario | When to Use | Pattern |
|---|---|---|
| Post-Login | After successful authentication | Redirect to intended destination or dashboard |
| Post-Logout | After user logs out | Redirect to home or login page |
| Already Authenticated | User visits login while logged in | Redirect to dashboard |
| Insufficient Permissions | User lacks required role | Redirect to unauthorized page |
| Not Found | Route doesn't exist | Redirect to 404 page |
| Session Expired | Auth token expired during navigation | Redirect to login with message |
Preventing Login Page Access When Authenticated
Users who are already logged in shouldn't see the login page:
// components/PublicRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface PublicRouteProps {
children: React.ReactNode;
redirectTo?: string;
}
export function PublicRoute({
children,
redirectTo = '/dashboard'
}: PublicRouteProps) {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return (
<div className="loading-container">
<div className="spinner"></div>
</div>
);
}
// If already authenticated, redirect to dashboard
if (isAuthenticated) {
return <Navigate to={redirectTo} replace />;
}
// Not authenticated, show the public page (login/register)
return <>{children}</>;
}
// Usage
<Route
path="/login"
element={
<PublicRoute>
<Login />
</PublicRoute>
}
/>
<Route
path="/register"
element={
<PublicRoute>
<Register />
</PublicRoute>
}
/>
Handling Session Expiration
Detect and handle expired sessions gracefully:
// hooks/useAuthInterceptor.ts
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function useAuthInterceptor() {
const navigate = useNavigate();
const { logout } = useAuth();
useEffect(() => {
// Intercept API responses
const handleUnauthorized = (event: CustomEvent) => {
if (event.detail.status === 401) {
// Session expired
logout();
navigate('/login', {
state: {
message: 'Your session has expired. Please log in again.',
from: window.location.pathname
},
replace: true
});
}
};
window.addEventListener('unauthorized' as any, handleUnauthorized);
return () => {
window.removeEventListener('unauthorized' as any, handleUnauthorized);
};
}, [navigate, logout]);
}
// In your API utility
export async function fetchWithAuth(url: string, options: RequestInit = {}) {
const token = localStorage.getItem('authToken');
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
// Dispatch custom event for session expiration
window.dispatchEvent(
new CustomEvent('unauthorized', {
detail: { status: 401 }
})
);
}
return response;
}
Conditional Redirects Based on User State
Redirect users based on multiple conditions:
// components/ConditionalRedirect.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface ConditionalRedirectProps {
children: React.ReactNode;
}
export function ConditionalRedirect({ children }: ConditionalRedirectProps) {
const { user, isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <div>Loading...</div>;
}
// Not authenticated - redirect to login
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
// Check if profile is complete
if (!user?.profileComplete) {
return <Navigate to="/complete-profile" replace />;
}
// Check if email is verified
if (!user?.emailVerified) {
return <Navigate to="/verify-email" replace />;
}
// Check if subscription is active (for premium features)
if (!user?.subscriptionActive) {
return <Navigate to="/subscription" replace />;
}
// All checks passed, render protected content
return <>{children}</>;
}
â Redirect Best Practices
- Use replace: true - Prevents users from going back to redirect pages
- Preserve state - Pass context about why redirect happened
- Show messages - Inform users why they were redirected
- Handle loading - Show loading state during auth checks
- Chain carefully - Avoid redirect loops
404 and Fallback Routes
Handle unknown routes with a catch-all redirect:
// pages/NotFound.tsx
import { Link } from 'react-router-dom';
export default function NotFound() {
return (
<div className="not-found">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link to="/">Go Home</Link>
</div>
);
}
// In App.tsx
<Routes>
<Route path="/" element={<Home />} />
{/* ... other routes */}
{/* Catch-all route - must be last */}
<Route path="*" element={<NotFound />} />
</Routes>
⥠Lazy Loading Routes
Lazy loading splits your application into smaller chunks that are loaded on-demand, significantly improving initial load time and performance.
Why Lazy Load Routes?
đ Performance Benefits
- Smaller Initial Bundle - Faster first page load
- Better Performance - Only load code when needed
- Improved Caching - Chunks can be cached separately
- Bandwidth Savings - Users only download what they use
- Progressive Loading - App becomes interactive faster
Basic React.lazy() Usage
Use React.lazy() to dynamically import components:
// Traditional import (not lazy)
import Dashboard from './pages/Dashboard';
// Lazy import
const Dashboard = lazy(() => import('./pages/Dashboard'));
Setting Up Lazy Routes
Here's how to implement lazy loading in your application:
// App.tsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
// Eager-loaded components (needed immediately)
import Home from './pages/Home';
import Login from './pages/Login';
// Lazy-loaded components (loaded on demand)
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
const Reports = lazy(() => import('./pages/Reports'));
// Loading fallback component
function LoadingFallback() {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>Loading page...</p>
</div>
);
}
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Suspense fallback={<LoadingFallback />}>
<Routes>
{/* Eager-loaded routes */}
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* Lazy-loaded protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute>
<AdminPanel />
</ProtectedRoute>
}
/>
<Route
path="/reports"
element={
<ProtectedRoute>
<Reports />
</ProtectedRoute>
}
/>
</Routes>
</Suspense>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
â ī¸ Important: Suspense Wrapper
Lazy-loaded components must be wrapped in a <Suspense> component. The fallback prop specifies what to show while the component is loading.
<Suspense fallback={<LoadingFallback />}>
{/* Lazy routes go here */}
</Suspense>
Custom Loading Fallback
Create a professional loading component for better UX:
// components/LoadingFallback.tsx
export function LoadingFallback() {
return (
<div className="page-loading">
<div className="loading-content">
<div className="spinner-large"></div>
<h2>Loading...</h2>
<p>Please wait while we prepare your page</p>
</div>
</div>
);
}
// With skeleton loading
export function SkeletonLoading() {
return (
<div className="skeleton-container">
<div className="skeleton-header"></div>
<div className="skeleton-content">
<div className="skeleton-line"></div>
<div className="skeleton-line"></div>
<div className="skeleton-line short"></div>
</div>
</div>
);
}
Lazy Loading with Error Handling
Handle loading errors gracefully:
// utils/lazyWithRetry.ts
import { lazy, ComponentType } from 'react';
export function lazyWithRetry<T extends ComponentType<any>>(
importFunc: () => Promise<{ default: T }>,
retries = 3
) {
return lazy(() => {
return new Promise<{ default: T }>((resolve, reject) => {
// Track the number of attempts
let attempts = 0;
const attemptImport = () => {
attempts++;
importFunc()
.then(resolve)
.catch((error) => {
if (attempts >= retries) {
reject(error);
} else {
// Wait before retrying (exponential backoff)
const delay = Math.min(1000 * Math.pow(2, attempts), 5000);
setTimeout(attemptImport, delay);
}
});
};
attemptImport();
});
});
}
// Usage
const Dashboard = lazyWithRetry(() => import('./pages/Dashboard'));
Preloading Routes
Preload routes that users are likely to visit next:
// Preload a route
const DashboardImport = () => import('./pages/Dashboard');
const Dashboard = lazy(DashboardImport);
// In your component
function Navigation() {
const handleMouseEnter = () => {
// Preload dashboard when user hovers over link
DashboardImport();
};
return (
<nav>
<Link
to="/dashboard"
onMouseEnter={handleMouseEnter}
>
Dashboard
</Link>
</nav>
);
}
â Lazy Loading Best Practices
- Lazy load by route - Each route should be its own chunk
- Keep critical paths eager - Don't lazy load login or home pages
- Group related features - Bundle related pages together
- Provide good loading UX - Use skeletons or meaningful loading states
- Test on slow connections - Ensure good experience on 3G
- Monitor chunk sizes - Keep chunks under 200KB when possible
What to Eager Load vs Lazy Load
| Eager Load (Immediate) | Lazy Load (On Demand) |
|---|---|
| Home page | Dashboard pages |
| Login/Register pages | Admin panels |
| Critical navigation | Settings pages |
| Error boundaries | Reports and analytics |
| Auth context | User profile editor |
| Loading components | Modal dialogs |
đī¸ Exercise: Implement Lazy Loading
Convert a multi-route application to use lazy loading for better performance.
Requirements:
- Create 5 different page components (Home, About, Products, Contact, Dashboard)
- Eager load Home and About pages
- Lazy load Products, Contact, and Dashboard
- Add a custom loading fallback with a spinner
- Wrap lazy routes in Suspense
- Add preloading to the navigation links
đĄ Hint
Structure your imports like this:
// Eager
import Home from './pages/Home';
// Lazy
const Products = lazy(() => import('./pages/Products'));
// With preload
const ProductsImport = () => import('./pages/Products');
const Products = lazy(ProductsImport);
â Solution
// App.tsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
// Eager-loaded pages
import Home from './pages/Home';
import About from './pages/About';
// Lazy-loaded pages with preload functions
const ProductsImport = () => import('./pages/Products');
const Products = lazy(ProductsImport);
const ContactImport = () => import('./pages/Contact');
const Contact = lazy(ContactImport);
const DashboardImport = () => import('./pages/Dashboard');
const Dashboard = lazy(DashboardImport);
// Loading component
function LoadingSpinner() {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '50vh'
}}>
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
}
// Navigation with preloading
function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link
to="/products"
onMouseEnter={() => ProductsImport()}
>
Products
</Link>
<Link
to="/contact"
onMouseEnter={() => ContactImport()}
>
Contact
</Link>
<Link
to="/dashboard"
onMouseEnter={() => DashboardImport()}
>
Dashboard
</Link>
</nav>
);
}
function App() {
return (
<BrowserRouter>
<Navigation />
<Suspense fallback={<LoadingSpinner />}>
<Routes>
{/* Eager-loaded routes */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* Lazy-loaded routes */}
<Route path="/products" element={<Products />} />
<Route path="/contact" element={<Contact />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
export default App;
âŗ React Suspense for Code Splitting
React Suspense is a powerful feature that lets you declaratively specify loading states while waiting for code to load. It's essential for lazy loading and provides a clean way to handle asynchronous operations.
đ What is Suspense?
Suspense is a React component that lets you "wait" for some code to load and declaratively specify a loading state while waiting. It's currently supported for:
- Lazy loading components with
React.lazy() - Data fetching with frameworks like Relay or Next.js
- Loading any async resource (experimental)
Basic Suspense Usage
The simplest Suspense implementation:
import { Suspense, lazy } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
Nested Suspense Boundaries
You can have multiple Suspense boundaries for fine-grained loading control:
import { Suspense, lazy } from 'react';
const Sidebar = lazy(() => import('./Sidebar'));
const MainContent = lazy(() => import('./MainContent'));
const Comments = lazy(() => import('./Comments'));
function Page() {
return (
<div className="page">
{/* Separate loading state for sidebar */}
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
<div className="main">
{/* Separate loading state for main content */}
<Suspense fallback={<div>Loading content...</div>}>
<MainContent />
</Suspense>
{/* Separate loading state for comments */}
<Suspense fallback={<div>Loading comments...</div>}>
<Comments />
</Suspense>
</div>
</div>
);
}
Suspense with Multiple Components
A single Suspense boundary can handle multiple lazy components:
import { Suspense, lazy } from 'react';
const UserProfile = lazy(() => import('./UserProfile'));
const UserPosts = lazy(() => import('./UserPosts'));
const UserActivity = lazy(() => import('./UserActivity'));
function UserDashboard() {
return (
<Suspense fallback={<div>Loading dashboard...</div>}>
{/* All three components share the same loading state */}
<UserProfile />
<UserPosts />
<UserActivity />
</Suspense>
);
}
// This will show "Loading dashboard..." until ALL three components are loaded
Advanced Suspense Patterns
Create a reusable SuspenseRoute component:
// components/SuspenseRoute.tsx
import { Suspense, ComponentType } from 'react';
interface SuspenseRouteProps {
component: ComponentType<any>;
fallback?: React.ReactNode;
}
export function SuspenseRoute({
component: Component,
fallback = <LoadingFallback />
}: SuspenseRouteProps) {
return (
<Suspense fallback={fallback}>
<Component />
</Suspense>
);
}
// Usage
const Dashboard = lazy(() => import('./pages/Dashboard'));
<Route
path="/dashboard"
element={<SuspenseRoute component={Dashboard} />}
/>
Suspense with Error Boundaries
Combine Suspense with Error Boundaries for robust loading:
import { Suspense, lazy } from 'react';
import { ErrorBoundary } from './ErrorBoundary';
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<LoadingPage />}>
<Dashboard />
</Suspense>
</ErrorBoundary>
);
}
// If Dashboard fails to load: ErrorPage is shown
// While Dashboard is loading: LoadingPage is shown
// When Dashboard loads successfully: Dashboard is rendered
Transitions with Suspense
Use useTransition to keep old UI visible during loading:
import { Suspense, lazy, useTransition, useState } from 'react';
const TabA = lazy(() => import('./TabA'));
const TabB = lazy(() => import('./TabB'));
const TabC = lazy(() => import('./TabC'));
function TabbedInterface() {
const [activeTab, setActiveTab] = useState('a');
const [isPending, startTransition] = useTransition();
const selectTab = (tab: string) => {
startTransition(() => {
setActiveTab(tab);
});
};
return (
<div>
<div className="tabs">
<button
onClick={() => selectTab('a')}
disabled={isPending}
>
Tab A {isPending && activeTab === 'a' && 'âŗ'}
</button>
<button
onClick={() => selectTab('b')}
disabled={isPending}
>
Tab B {isPending && activeTab === 'b' && 'âŗ'}
</button>
<button
onClick={() => selectTab('c')}
disabled={isPending}
>
Tab C {isPending && activeTab === 'c' && 'âŗ'}
</button>
</div>
<Suspense fallback={<div>Loading tab...</div>}>
{activeTab === 'a' && <TabA />}
{activeTab === 'b' && <TabB />}
{activeTab === 'c' && <TabC />}
</Suspense>
</div>
);
}
â Suspense Best Practices
- Granular boundaries - Use multiple Suspense for independent sections
- Meaningful fallbacks - Show context-appropriate loading states
- Avoid too many boundaries - Don't overuse, it can feel janky
- Position carefully - Place Suspense where loading makes sense
- Combine with ErrorBoundary - Always handle loading failures
- Test slow networks - See how Suspense feels on 3G
đ Loading States and User Feedback
Providing clear, informative loading states is crucial for good user experience. Let's explore various techniques for communicating loading status to users.
Types of Loading Indicators
| Type | When to Use | Example |
|---|---|---|
| Spinner | Unknown duration, small area | Loading a component or API call |
| Progress Bar | Known duration or percentage | File upload, multi-step process |
| Skeleton Screen | Content with known layout | Social media feeds, cards |
| Shimmer Effect | Content loading, modern look | Lists, grids, cards |
| Text Message | Complex operations | "Processing payment...", "Saving..." |
| Inline Indicator | Button actions | Saving button, submit forms |
Simple Spinner Component
A basic, reusable loading spinner:
// components/LoadingSpinner.tsx
interface LoadingSpinnerProps {
size?: 'small' | 'medium' | 'large';
message?: string;
}
export function LoadingSpinner({
size = 'medium',
message
}: LoadingSpinnerProps) {
const sizeClasses = {
small: 'w-4 h-4',
medium: 'w-8 h-8',
large: 'w-12 h-12'
};
return (
<div className="flex flex-col items-center justify-center p-4">
<div
className={`
${sizeClasses[size]}
border-4
border-gray-200
border-t-blue-500
rounded-full
animate-spin
`}
role="status"
aria-label="Loading"
></div>
{message && (
<p className="mt-2 text-sm text-gray-600">{message}</p>
)}
</div>
);
}
// Usage
<LoadingSpinner size="large" message="Loading your dashboard..." />
Skeleton Screen Component
Create content placeholders that match your layout:
// components/SkeletonCard.tsx
export function SkeletonCard() {
return (
<div className="border rounded-lg p-4 animate-pulse">
{/* Header skeleton */}
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
<div className="flex-1">
<div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
{/* Content skeleton */}
<div className="space-y-2">
<div className="h-3 bg-gray-300 rounded"></div>
<div className="h-3 bg-gray-300 rounded"></div>
<div className="h-3 bg-gray-300 rounded w-5/6"></div>
</div>
{/* Image skeleton */}
<div className="mt-4 h-48 bg-gray-300 rounded"></div>
{/* Footer skeleton */}
<div className="mt-4 flex justify-between">
<div className="h-8 bg-gray-300 rounded w-20"></div>
<div className="h-8 bg-gray-300 rounded w-20"></div>
</div>
</div>
);
}
// Usage
function PostList() {
const { posts, isLoading } = usePosts();
if (isLoading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Progress Bar Component
Show progress for operations with known completion:
// components/ProgressBar.tsx
interface ProgressBarProps {
progress: number; // 0-100
message?: string;
showPercentage?: boolean;
}
export function ProgressBar({
progress,
message,
showPercentage = true
}: ProgressBarProps) {
return (
<div className="w-full">
{message && (
<div className="flex justify-between mb-1">
<span className="text-sm font-medium">{message}</span>
{showPercentage && (
<span className="text-sm font-medium">{progress}%</span>
)}
</div>
)}
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
role="progressbar"
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
></div>
</div>
</div>
);
}
// Usage
function FileUpload() {
const [uploadProgress, setUploadProgress] = useState(0);
return (
<ProgressBar
progress={uploadProgress}
message="Uploading file..."
/>
);
}
Full Page Loading Component
For route transitions and major loading states:
// components/PageLoading.tsx
interface PageLoadingProps {
message?: string;
}
export function PageLoading({
message = 'Loading...'
}: PageLoadingProps) {
return (
<div className="fixed inset-0 bg-white flex items-center justify-center z-50">
<div className="text-center">
{/* Logo or brand */}
<div className="mb-6">
<img
src="/logo.svg"
alt="Logo"
className="w-16 h-16 mx-auto"
/>
</div>
{/* Spinner */}
<div className="flex justify-center mb-4">
<div className="w-12 h-12 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
{/* Message */}
<p className="text-lg font-medium text-gray-700">{message}</p>
<p className="text-sm text-gray-500 mt-2">
This won't take long...
</p>
</div>
</div>
);
}
Loading Button States
Show loading state on buttons during async operations:
// components/LoadingButton.tsx
interface LoadingButtonProps {
isLoading: boolean;
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
}
export function LoadingButton({
isLoading,
children,
onClick,
disabled,
type = 'button'
}: LoadingButtonProps) {
return (
<button
type={type}
onClick={onClick}
disabled={isLoading || disabled}
className={`
px-4 py-2 rounded
${isLoading ? 'opacity-75 cursor-not-allowed' : ''}
bg-blue-500 hover:bg-blue-600 text-white
flex items-center justify-center
`}
>
{isLoading && (
<span className="mr-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
</span>
)}
{children}
</button>
);
}
// Usage
function SaveForm() {
const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => {
setIsSaving(true);
await saveData();
setIsSaving(false);
};
return (
<LoadingButton isLoading={isSaving} onClick={handleSave}>
{isSaving ? 'Saving...' : 'Save Changes'}
</LoadingButton>
);
}
đĄ Loading UX Best Practices
- Be specific - "Loading products..." vs "Loading..."
- Match the content - Use skeletons that look like final content
- Avoid flash - Don't show spinners for very quick operations (<200ms)
- Provide context - Explain what's happening and why
- Keep UI responsive - Allow users to cancel or navigate away
- Progressive disclosure - Show content as it loads when possible
đ Error Boundaries with Routing
Error boundaries catch JavaScript errors anywhere in the component tree and display fallback UI instead of crashing the entire application. They're essential for handling route loading errors.
đ What Are Error Boundaries?
Error boundaries are React components that:
- Catch JavaScript errors in child components
- Log error information
- Display fallback UI instead of crashing
- Allow the rest of the app to continue working
Note: Error boundaries must be class components in React (until React 19).
Creating an Error Boundary
Build a reusable error boundary component:
// components/ErrorBoundary.tsx
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null
};
}
static getDerivedStateFromError(error: Error): State {
// Update state so next render shows fallback UI
return {
hasError: true,
error
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log error to error reporting service
console.error('Error boundary caught:', error, errorInfo);
// Call optional error handler
this.props.onError?.(error, errorInfo);
// You can also log to services like Sentry
// logErrorToService(error, errorInfo);
}
handleReset = () => {
this.setState({
hasError: false,
error: null
});
};
render() {
if (this.state.hasError) {
// Custom fallback UI if provided
if (this.props.fallback) {
return this.props.fallback;
}
// Default fallback UI
return (
<div className="error-container">
<h1>â ī¸ Something went wrong</h1>
<p>We're sorry, but something unexpected happened.</p>
{this.state.error && (
<details className="error-details">
<summary>Error details</summary>
<pre>{this.state.error.toString()}</pre>
</details>
)}
<button onClick={this.handleReset}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
Using Error Boundaries with Routes
Wrap routes in error boundaries to catch loading and rendering errors:
// App.tsx
import { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from './components/ErrorBoundary';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
function App() {
return (
<BrowserRouter>
<ErrorBoundary>
<Suspense fallback={<LoadingPage />}>
<Routes>
<Route path="/" element={<Home />} />
{/* Each route is protected by the error boundary */}
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</ErrorBoundary>
</BrowserRouter>
);
}
Route-Specific Error Boundaries
Create error boundaries for individual routes or route groups:
// Different error handling for different sections
function App() {
return (
<BrowserRouter>
<Routes>
{/* Public routes - simple error handling */}
<Route
path="/"
element={
<ErrorBoundary fallback={<PublicErrorPage />}>
<Home />
</ErrorBoundary>
}
/>
{/* User routes - show support contact */}
<Route
path="/dashboard"
element={
<ErrorBoundary
fallback={<UserErrorPage />}
onError={(error) => logToAnalytics(error)}
>
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
</ErrorBoundary>
}
/>
{/* Admin routes - detailed error info */}
<Route
path="/admin/*"
element={
<ErrorBoundary fallback={<AdminErrorPage />}>
<AdminLayout />
</ErrorBoundary>
}
/>
</Routes>
</BrowserRouter>
);
}
Enhanced Error Page Component
Create a user-friendly error page with recovery options:
// pages/ErrorPage.tsx
import { useNavigate } from 'react-router-dom';
interface ErrorPageProps {
error?: Error;
resetError?: () => void;
}
export function ErrorPage({ error, resetError }: ErrorPageProps) {
const navigate = useNavigate();
const handleGoHome = () => {
resetError?.();
navigate('/');
};
const handleReload = () => {
resetError?.();
window.location.reload();
};
return (
<div className="error-page">
<div className="error-content">
<div className="error-icon">â ī¸</div>
<h1>Oops! Something went wrong</h1>
<p className="error-message">
We encountered an unexpected error. Don't worry,
your data is safe.
</p>
{error && process.env.NODE_ENV === 'development' && (
<details className="error-details">
<summary>Technical Details</summary>
<pre>
{error.name}: {error.message}
{'\n\n'}
{error.stack}
</pre>
</details>
)}
<div className="error-actions">
<button onClick={handleGoHome} className="btn-primary">
đ Go to Home
</button>
<button onClick={handleReload} className="btn-secondary">
đ Reload Page
</button>
{resetError && (
<button onClick={resetError} className="btn-secondary">
âŠī¸ Try Again
</button>
)}
</div>
<div className="error-help">
<p>Need help?</p>
<a href="/support">Contact Support</a>
<span> âĸ </span>
<a href="/status">System Status</a>
</div>
</div>
</div>
);
}
Handling Lazy Loading Errors
Specifically handle errors that occur during lazy loading:
// utils/lazyWithErrorHandling.ts
import { lazy, ComponentType } from 'react';
export function lazyWithErrorHandling<T extends ComponentType<any>>(
importFunc: () => Promise<{ default: T }>,
componentName: string
) {
return lazy(() =>
importFunc().catch((error) => {
console.error(`Failed to load ${componentName}:`, error);
// Return a fallback component
return {
default: (() => (
<div className="lazy-error">
<h2>Failed to load {componentName}</h2>
<p>Please check your internet connection and try again.</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
)) as T
};
})
);
}
// Usage
const Dashboard = lazyWithErrorHandling(
() => import('./pages/Dashboard'),
'Dashboard'
);
â Error Boundary Best Practices
- Place strategically - Wrap major sections, not every component
- Log errors - Send to error tracking service (Sentry, LogRocket)
- Provide recovery - Allow users to retry or navigate away
- Show less in production - Hide stack traces from users
- Test error states - Throw test errors to verify boundaries work
- Don't catch everything - Event handlers need try-catch
What Error Boundaries Don't Catch
â ī¸ Limitations
Error boundaries do NOT catch errors in:
- Event handlers (use try-catch)
- Asynchronous code (setTimeout, promises)
- Server-side rendering
- Errors thrown in the error boundary itself
For these cases, use traditional try-catch blocks.
đĨ Role-Based Access Control
Role-Based Access Control (RBAC) restricts route access based on user roles or permissions. This is essential for applications with different user types (admin, moderator, user, etc.).
Understanding RBAC
Role-Based Protected Route
Create a component that checks both authentication and roles:
// components/RoleProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface RoleProtectedRouteProps {
children: React.ReactNode;
allowedRoles: string[];
redirectTo?: string;
}
export function RoleProtectedRoute({
children,
allowedRoles,
redirectTo = '/unauthorized'
}: RoleProtectedRouteProps) {
const { user, isAuthenticated, isLoading } = useAuth();
const location = useLocation();
// Show loading while checking auth
if (isLoading) {
return <LoadingPage />;
}
// Not authenticated - redirect to login
if (!isAuthenticated) {
return (
<Navigate
to="/login"
state={{ from: location }}
replace
/>
);
}
// Authenticated but wrong role - show unauthorized
if (!allowedRoles.includes(user!.role)) {
return (
<Navigate
to={redirectTo}
state={{ requiredRoles: allowedRoles }}
replace
/>
);
}
// User has correct role
return <>{children}</>;
}
Using Role-Based Routes
Apply role restrictions to your routes:
// App.tsx
import { RoleProtectedRoute } from './components/RoleProtectedRoute';
function App() {
return (
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* User routes - any authenticated user */}
<Route
path="/dashboard"
element={
<RoleProtectedRoute allowedRoles={['user', 'moderator', 'admin']}>
<Dashboard />
</RoleProtectedRoute>
}
/>
{/* Moderator routes */}
<Route
path="/moderation"
element={
<RoleProtectedRoute allowedRoles={['moderator', 'admin']}>
<ModerationPanel />
</RoleProtectedRoute>
}
/>
{/* Admin-only routes */}
<Route
path="/admin/*"
element={
<RoleProtectedRoute allowedRoles={['admin']}>
<AdminLayout />
</RoleProtectedRoute>
}
/>
{/* Unauthorized page */}
<Route path="/unauthorized" element={<UnauthorizedPage />} />
</Routes>
</BrowserRouter>
);
}
Unauthorized Page
Create a clear unauthorized access page:
// pages/UnauthorizedPage.tsx
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface LocationState {
requiredRoles?: string[];
}
export function UnauthorizedPage() {
const location = useLocation();
const navigate = useNavigate();
const { user, logout } = useAuth();
const state = location.state as LocationState;
const handleGoBack = () => {
navigate(-1);
};
const handleGoHome = () => {
navigate('/');
};
const handleSwitchAccount = () => {
logout();
navigate('/login');
};
return (
<div className="unauthorized-page">
<div className="unauthorized-content">
<div className="icon">đĢ</div>
<h1>Access Denied</h1>
<p>
You don't have permission to access this page.
</p>
{state?.requiredRoles && (
<div className="required-roles">
<p><strong>Required roles:</strong></p>
<ul>
{state.requiredRoles.map(role => (
<li key={role}>{role}</li>
))}
</ul>
</div>
)}
{user && (
<div className="current-role">
<p>Your current role: <strong>{user.role}</strong></p>
</div>
)}
<div className="actions">
<button onClick={handleGoBack} className="btn-secondary">
â Go Back
</button>
<button onClick={handleGoHome} className="btn-primary">
đ Go Home
</button>
<button onClick={handleSwitchAccount} className="btn-secondary">
đ Switch Account
</button>
</div>
<div className="help-text">
<p>
Need higher access? Contact your administrator.
</p>
</div>
</div>
</div>
);
}
Permission-Based Access Control
For more granular control, use permissions instead of roles:
// types/auth.ts
export interface User {
id: number;
email: string;
name: string;
role: 'user' | 'moderator' | 'admin';
permissions: string[]; // e.g., ['users.read', 'users.write', 'posts.delete']
}
// components/PermissionProtectedRoute.tsx
interface PermissionProtectedRouteProps {
children: React.ReactNode;
requiredPermissions: string[];
requireAll?: boolean; // true = need all permissions, false = need any
}
export function PermissionProtectedRoute({
children,
requiredPermissions,
requireAll = true
}: PermissionProtectedRouteProps) {
const { user, isAuthenticated, isLoading } = useAuth();
const location = useLocation();
if (isLoading) return <LoadingPage />;
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
// Check permissions
const hasPermission = requireAll
? requiredPermissions.every(perm => user!.permissions.includes(perm))
: requiredPermissions.some(perm => user!.permissions.includes(perm));
if (!hasPermission) {
return (
<Navigate
to="/unauthorized"
state={{ requiredPermissions }}
replace
/>
);
}
return <>{children}</>;
}
// Usage
<Route
path="/users/edit/:id"
element={
<PermissionProtectedRoute
requiredPermissions={['users.write']}
>
<EditUser />
</PermissionProtectedRoute>
}
/>
Conditional Navigation Based on Role
Show/hide navigation items based on user permissions:
// components/Navigation.tsx
import { Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export function Navigation() {
const { user, isAuthenticated } = useAuth();
const hasRole = (roles: string[]) => {
return user && roles.includes(user.role);
};
return (
<nav>
<Link to="/">Home</Link>
{isAuthenticated && (
<>
<Link to="/dashboard">Dashboard</Link>
<Link to="/profile">Profile</Link>
</>
)}
{hasRole(['moderator', 'admin']) && (
<Link to="/moderation">Moderation</Link>
)}
{hasRole(['admin']) && (
<Link to="/admin">Admin Panel</Link>
)}
</nav>
);
}
â ī¸ Security Warning
Client-side route protection is NOT enough!
- Always validate permissions on the server/API
- Client-side checks are for UX only
- Malicious users can bypass client-side restrictions
- Backend must enforce all authorization rules
đī¸ Exercise: Implement Role-Based Dashboard
Create a multi-role application with role-specific access.
Requirements:
- Create routes for: /dashboard, /users, /reports, /admin
- Dashboard: accessible to all authenticated users
- Users: accessible to moderators and admins
- Reports: accessible to admins only
- Create an unauthorized page
- Show different navigation items based on role
đĄ Hint
Use the RoleProtectedRoute component with different role arrays for each route.
â Solution
// See the complete implementation in the downloadable course files
đ¯ Summary and Next Steps
What You've Learned
In this lesson, you've mastered route protection, lazy loading, and error handling:
đ Key Concepts Covered
- Protected Routes - Securing routes with authentication
- Authentication Guards - Implementing auth context and redirect logic
- Redirect Patterns - Handling various redirect scenarios
- Lazy Loading - Code splitting with React.lazy()
- Suspense - Managing loading states declaratively
- Loading UX - Creating spinners, skeletons, and progress indicators
- Error Boundaries - Catching and handling route errors
- Role-Based Access - Implementing RBAC for routes
Quick Reference
| Concept | Use Case | Key Component |
|---|---|---|
| Protected Routes | Require authentication | ProtectedRoute wrapper |
| Role-Based Routes | Restrict by user role | RoleProtectedRoute |
| Lazy Loading | Split code, improve performance | React.lazy() |
| Suspense | Handle loading states | <Suspense fallback={...}> |
| Error Boundaries | Catch route errors | ErrorBoundary class |
Best Practices Checklist
â Production Checklist
- â Protected routes redirect to login with return path
- â Loading states for all async operations
- â Error boundaries around major app sections
- â Lazy loading for routes and large components
- â Role-based access control where needed
- â Server-side auth validation (never trust client)
- â Clear unauthorized and error pages
- â Proper TypeScript typing throughout
- â Testing for protected routes and error states
Common Patterns Summary
// Protected Route Pattern
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
// Lazy Loading Pattern
const Dashboard = lazy(() => import('./Dashboard'));
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
// Error Boundary Pattern
<ErrorBoundary fallback={<ErrorPage />}>
<Routes>
{/* routes */}
</Routes>
</ErrorBoundary>
// Role-Based Pattern
<RoleProtectedRoute allowedRoles={['admin']}>
<AdminPanel />
</RoleProtectedRoute>
Performance Tips
⥠Optimization Strategies
- Lazy load by route - Each route = separate chunk
- Preload on hover - Load next likely route
- Use skeleton screens - Better than spinners
- Minimize error boundaries - Don't wrap everything
- Cache auth state - Reduce auth checks
- Progressive loading - Show content as it loads
Next Lesson Preview
đ Coming Up: Lesson 6.4 - Search and Query Parameters
In the next lesson, you'll learn:
- Using the useSearchParams hook
- Managing URL query strings
- Building search and filter functionality
- Syncing state with URL parameters
- Pagination with URL state
- TypeScript patterns for search params
Practice Projects
-
Authenticated Blog Platform
- Public blog listing (no auth required)
- Protected post creation/editing
- Admin-only user management
- Lazy loaded dashboard and editor
-
E-commerce Admin Panel
- Role-based product management
- Order processing (moderator+)
- Analytics dashboard (admin only)
- Error handling for failed operations
-
Team Collaboration Tool
- Project-based permissions
- Lazy loaded project views
- Session management and expiration
- Comprehensive error boundaries
Additional Resources
- React: lazy() Reference
- React: Suspense Reference
- React: Error Boundaries
- React Router: Overview
- Web.dev: Code Splitting with Suspense
đ Congratulations!
You've completed Lesson 6.3 and now have a comprehensive understanding of route protection and performance optimization. You can:
- â Implement secure authentication flows
- â Create role-based access control systems
- â Optimize applications with lazy loading
- â Handle errors gracefully with boundaries
- â Build production-ready routing architectures
- â Provide excellent loading UX for users
Ready to add search and filtering? Let's continue!