L13: Create Route Guard Component
Protect routes that require authentication
Let's build a reusable component to protect routes that require authentication! 🔒
Why Route Guards?
Problem without guards:
// Anyone can access favorites page
<Route path="/favorites" element={<FavoritesPage />} />
// Unauthenticated user visits /favorites
→ Page loads
→ API calls fail (no token)
→ Error messages everywhere
→ Poor user experience ❌Solution with guards:
// Only authenticated users can access
<Route
path="/favorites"
element={<Protected><FavoritesPage /></Protected>}
/>
// Unauthenticated user visits /favorites
→ Guard checks authentication
→ No user found
→ Redirect to /sign-in
→ After sign-in, redirect back to /favorites
→ Great user experience ✅Understanding Route Protection
Three states to handle:
1. Loading (checking authentication)
→ Show loading spinner
2. Authenticated (user signed in)
→ Show protected component
3. Not authenticated (no user)
→ Redirect to sign-in pageFlow diagram:
User visits /favorites
↓
<Protected> component renders
↓
Is loading? → Yes → Show spinner
↓ No
Is authenticated? → Yes → Show FavoritesPage ✅
↓ No
Redirect to /sign-in ➡️
↓
Save intended destination (/favorites)
↓
After sign-in, redirect to /favoritesCreate Protected Component
Create a reusable route guard:
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
function Protected({ children }) {
const { user, isLoading } = useAuth();
const location = useLocation();
// Still checking authentication
if (isLoading) {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>Verifying authentication...</p>
</div>
);
}
// Not authenticated, redirect to sign-in
if (!user) {
// Save the location they were trying to access
return <Navigate to="/sign-in" state={{ from: location }} replace />;
}
// Authenticated, render protected content
return children;
}
export default Protected;Understanding the Code
Passing state with navigation:
<Navigate
to="/sign-in"
state={{ from: location }}
replace
/>What state contains:
location = {
pathname: '/favorites',
search: '?sort=recent',
hash: '#top',
state: null,
key: 'abc123'
};
// Passed as state to /sign-in
state = {
from: {
pathname: '/favorites',
search: '?sort=recent',
// ...
}
};Access in SignInPage:
import { useLocation, useNavigate } from 'react-router-dom';
function SignInPage() {
const location = useLocation();
const navigate = useNavigate();
// Get the page they were trying to access
const from = location.state?.from?.pathname || '/';
const handleSignIn = async () => {
await signIn(email, password);
// Redirect back to where they came from
navigate(from, { replace: true });
};
return <SignInForm onSubmit={handleSignIn} />;
}User flow:
1. User visits /favorites (not signed in)
2. Protected redirects to /sign-in with state={{ from: '/favorites' }}
3. User signs in
4. SignInPage reads state.from = '/favorites'
5. Navigates to /favorites
6. User sees favorites page ✅Without state:
1. User visits /favorites
2. Redirects to /sign-in
3. User signs in
4. No idea where they wanted to go
5. Navigates to / (homepage)
6. User has to navigate to /favorites again 😞Why check isLoading?
// Initial state
user: null
isLoading: true
// Problem without loading check
if (!user) {
return <Navigate to="/sign-in" />;
}
// Always redirects during loading! ❌
// Solution: Wait for loading to complete
if (isLoading) {
return <LoadingSpinner />;
}
if (!user) {
return <Navigate to="/sign-in" />;
}
// Only redirects if actually not authenticated ✅Timeline:
0ms: Component mounts
isLoading: true
user: null
→ Show loading spinner
500ms: Token fetch completes
isLoading: false
user: { data }
→ Show protected content
Alternative (no session):
500ms: Token fetch fails
isLoading: false
user: null
→ Redirect to /sign-inLoading UI variations:
// Simple spinner
if (isLoading) {
return <div className="spinner"></div>;
}
// Spinner with message
if (isLoading) {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>Verifying authentication...</p>
</div>
);
}
// Skeleton screen (better UX)
if (isLoading) {
return <FavoritesPageSkeleton />;
}
// Progress bar
if (isLoading) {
return <LoadingBar progress={50} />;
}Understanding children prop:
function Protected({ children }) {
// children = whatever is inside <Protected>...</Protected>
if (authenticated) {
return children; // Render the children
}
return <Navigate to="/sign-in" />;
}How it works:
// JSX
<Protected>
<FavoritesPage />
</Protected>
// Equivalent to
<Protected children={<FavoritesPage />} />
// Inside Protected component
const Protected = ({ children }) => {
// children = <FavoritesPage />
return children; // Renders <FavoritesPage />
};Multiple children:
<Protected>
<h1>Title</h1>
<p>Content</p>
<FavoritesPage />
</Protected>
// children is an array:
// [<h1>Title</h1>, <p>Content</p>, <FavoritesPage />]
// Rendered as-is:
return children;
// All three elements renderWhy children pattern?
// ❌ Without children (not reusable)
function ProtectedFavorites() {
if (!user) return <Navigate to="/sign-in" />;
return <FavoritesPage />;
}
function ProtectedProfile() {
if (!user) return <Navigate to="/sign-in" />;
return <ProfilePage />;
}
// Repeat for every protected page!
// ✅ With children (reusable)
function Protected({ children }) {
if (!user) return <Navigate to="/sign-in" />;
return children;
}
// Use anywhere
<Protected><FavoritesPage /></Protected>
<Protected><ProfilePage /></Protected>
<Protected><AnyPage /></Protected>Add Styling for Loading State
Ensure loading spinner is styled:
/* Loading Container (full screen center) */
.loading-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
}
/* Spinner Animation */
.spinner {
width: 48px;
height: 48px;
border: 4px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Loading Text */
.loading-container p {
color: #6b7280;
font-size: 1rem;
margin: 0;
}Usage Examples
Protect a single route:
import { Routes, Route } from 'react-router-dom';
import Protected from '@/components/Protected';
import HomePage from '@/pages/HomePage';
import FavoritesPage from '@/pages/FavoritesPage';
import SignInPage from '@/pages/SignInPage';
function App() {
return (
<Routes>
{/* Public routes */}
<Route path="/" element={<HomePage />} />
<Route path="/sign-in" element={<SignInPage />} />
{/* Protected routes */}
<Route
path="/favorites"
element={
<Protected>
<FavoritesPage />
</Protected>
}
/>
</Routes>
);
}User flow:
Authenticated user:
/favorites → Shows FavoritesPage ✅
Unauthenticated user:
/favorites → Redirects to /sign-in ➡️Protect multiple routes at once:
<Routes>
{/* Public */}
<Route path="/" element={<HomePage />} />
<Route path="/sign-in" element={<SignInPage />} />
{/* Protected group */}
<Route element={<Protected><Outlet /></Protected>}>
<Route path="/favorites" element={<FavoritesPage />} />
<Route path="/bookings" element={<BookingsPage />} />
<Route path="/profile" element={<ProfilePage />} />
</Route>
</Routes>All nested routes are protected:
/favorites → Protected
/bookings → Protected
/profile → ProtectedWith layout:
<Route element={<Protected><DashboardLayout /></Protected>}>
<Route path="/dashboard" element={<DashboardHome />} />
<Route path="/dashboard/settings" element={<Settings />} />
<Route path="/dashboard/profile" element={<Profile />} />
</Route>All dashboard routes share DashboardLayout and protection!
Protect parts of a component:
function HomePage() {
return (
<div className="home-page">
<h1>Welcome to Holidaze</h1>
{/* Public content */}
<section className="venues">
<VenueGrid venues={allVenues} />
</section>
{/* Protected content */}
<Protected>
<section className="personalized">
<h2>Your Recommendations</h2>
<VenueGrid venues={recommendedVenues} />
</section>
</Protected>
</div>
);
}Behavior:
Unauthenticated:
→ Shows public content
→ Hides recommendations
→ Stays on same page
Authenticated:
→ Shows all contentNote: This redirects the whole page. For hiding content, use conditional rendering instead:
{user && (
<section className="personalized">
<h2>Your Recommendations</h2>
</section>
)}Multiple levels of protection:
// Admin protection
function AdminProtected({ children }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/sign-in" />;
}
if (!user.isAdmin) {
return <Navigate to="/" />;
}
return children;
}
// Usage
<Routes>
{/* User must be signed in */}
<Route
path="/dashboard"
element={<Protected><Dashboard /></Protected>}
/>
{/* User must be admin */}
<Route
path="/admin"
element={
<Protected>
<AdminProtected>
<AdminPanel />
</AdminProtected>
</Protected>
}
/>
</Routes>Checks in order:
/admin
↓
Protected checks: Signed in? → Yes
↓
AdminProtected checks: Is admin? → Yes
↓
Render AdminPanel ✅
/admin (non-admin user)
↓
Protected checks: Signed in? → Yes
↓
AdminProtected checks: Is admin? → No
↓
Redirect to / ➡️Testing Protected Routes
Test unauthenticated access:
- Ensure you're signed out
- Visit
/favoritesdirectly - Should:
- Show loading spinner briefly
- Redirect to
/sign-in - URL changes to
/sign-in
✅ Redirect working!
Test authenticated access:
- Sign in
- Visit
/favorites - Should:
- Show loading spinner briefly
- Display FavoritesPage
- Stay on
/favorites
✅ Protected route accessible!
Test redirect back:
- Sign out
- Visit
/favorites(gets redirected to/sign-in) - Sign in
- Should automatically redirect to
/favorites
✅ Redirect back working!
Test loading state:
- Slow down network in DevTools (Slow 3G)
- Refresh page on protected route
- Should see loading spinner longer
- Eventually shows page or redirects
✅ Loading state handled!
Advanced Patterns
What's Next?
In Lesson 14, we'll:
- Update all routes with protection
- Organize public vs protected routes
- Add proper navigation structure
- Handle edge cases
✅ Lesson Complete! Route guard component created!
Key Takeaways
- ✅
<Protected>component wraps routes requiring authentication - ✅
<Navigate>component handles redirects declaratively - ✅ Location state preserves intended destination
- ✅ Loading check prevents premature redirects
- ✅ Children prop makes component reusable
- ✅
replaceprop prevents back-button issues - ✅ Role-based protection extends basic pattern
- ✅ Nested protection allows multiple checks
- ✅ Skeleton screens improve perceived performance
- ✅ User experience is seamless with proper loading states