Step 12: Create Protected Route
Time: ~8 minutes | Type: Coding | Concepts: Route Guards, Navigate Component, Children Prop
What We're Building
A ProtectedRoute component that checks authentication status before allowing access to protected pages, with proper loading states and redirects.
Let's Build It
Prompt: Create ProtectedRoute Component
Create a ProtectedRoute component for my React + TypeScript app.
Requirements:
1. File location: src/components/Auth/ProtectedRoute.tsx
2. Accept children prop (the page component to protect)
3. Use useAuth() hook to get user and loading state
4. Logic:
- If loading is true, show a loading message or spinner
- If user is null (not logged in), redirect to /login using Navigate from react-router-dom
- If user exists (logged in), render the children
5. Use TypeScript with proper types for the children prop
After creating the code, explain:
- What the children prop is and how it works
- Why we check loading before checking user
- What Navigate component does
- How this differs from useNavigate hookWhat to expect:
- New file
src/components/Auth/ProtectedRoute.tsx - Component accepting children prop
- useAuth hook usage
- Conditional rendering: loading → redirect → children
- Import Navigate from react-router-dom
- TypeScript interface for props
Files you'll create:
src/components/Auth/ProtectedRoute.tsx
Prompt: Wrap Dashboard Route
Update App.tsx to protect the Dashboard route with ProtectedRoute.
The /dashboard route should:
1. Import ProtectedRoute component
2. Wrap <DashboardPage /> with <ProtectedRoute>
3. Keep other routes public (Home, Login, Register)
Show me the updated Routes section of App.tsx.What to expect:
- Import statement for ProtectedRoute
- Dashboard route wrapped:
<ProtectedRoute><DashboardPage /></ProtectedRoute> - Other routes unchanged
Files you'll modify:
src/App.tsx
Understanding What You Built
After AI creates the code, make sure you understand the pattern:
💡 Ask AI to Explain:
Walk me through the ProtectedRoute component: 1. What is the children prop and what does it contain? 2. Why do we check loading BEFORE checking user? 3. What's the difference between <Navigate> and useNavigate()? 4. What happens when Navigate renders? 5. How does this component protect routes from unauthorized access?
Key concepts to understand:
childrenprop — Whatever is wrapped inside ProtectedRoute- Check loading first — Prevents redirect flash during auth check
<Navigate>— Component that redirects when rendereduseNavigate()— Hook that returns function to navigate programmatically- ProtectedRoute renders BEFORE the page, so it can block access
Verify It Works
Manual Testing:
Run the app:
bashnpm run devTest protected route while logged out:
- Make sure you're logged out (clear cookies or use incognito)
- Manually go to:
http://localhost:5173/dashboard
What should happen:
- Brief "Loading..." message (might be too fast to see)
- Redirects to
/login - URL changes to
http://localhost:5173/login - Dashboard does NOT render
Test public routes while logged out:
- Go to
/→ Should work (no redirect) - Go to
/login→ Should work - Go to
/register→ Should work
- Go to
Test protected route while logged in:
- Log in with
test@example.com/password123 - Should redirect to
/dashboardafter login - Dashboard page should show (not redirect to login)
- Stay on dashboard page (no redirect loop)
- Log in with
Test direct navigation while logged in:
- While logged in, go to
/(home) - Click "Dashboard" in nav
- Should navigate to dashboard
- No loading flash
- Dashboard renders
- While logged in, go to
Test auth persistence:
- While logged in and on dashboard
- Refresh the page (F5)
What should happen:
- Brief "Loading..." (while Firebase checks session)
- Dashboard stays rendered (no redirect)
- You're still logged in
Test logout from protected route:
- While on dashboard
- Click "Logout"
- Should redirect to
/(home) - If you manually go to
/dashboard→ redirects to/login
Checklist:
- [ ] Logged out + visit
/dashboard→ redirects to/login - [ ] Logged in + visit
/dashboard→ shows dashboard - [ ] Public routes work when logged out
- [ ] No redirect loop (dashboard doesn't keep redirecting)
- [ ] Loading state shows briefly on page refresh
- [ ] Auth persists on refresh
Common Issues
Infinite redirect loop
Problem: Login or Register page wrapped in ProtectedRoute
Fix: Only protect routes that require auth:
// Right:
<Route path="/dashboard" element={<ProtectedRoute><DashboardPage /></ProtectedRoute>} />
// Wrong - don't protect these!
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />Dashboard shows for a moment before redirect
Problem: Not checking loading state
Fix:
function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
if (loading) { // Must check loading FIRST
return <div>Loading...</div>;
}
if (!user) {
return <Navigate to="/login" />;
}
return children;
}"Navigate is not defined"
Problem: Wrong import
Fix:
import { Navigate } from 'react-router-dom';"children is not assignable to type"
Problem: Missing TypeScript type
Fix:
import { ReactNode } from 'react';
interface ProtectedRouteProps {
children: ReactNode;
}
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
// ...
}Redirect works but console shows error
Problem: Trying to render after redirect
Fix: Make sure Navigate is the ONLY thing returned:
if (!user) {
return <Navigate to="/login" />; // Return immediately!
}Loading shows forever
Problem: Loading state never becomes false
Fix: Check AuthContext's useEffect:
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user);
setLoading(false); // Must set this!
});
return unsubscribe;
}, []);Code Example
Your ProtectedRoute.tsx should look roughly like this:
import { ReactNode } from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
interface ProtectedRouteProps {
children: ReactNode;
}
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const { user, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
return <Navigate to="/login" />;
}
return <>{children}</>;
}Your App.tsx routes should look like:
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
</Routes>What You Learned
At this point you should understand:
- ✅ How to create a route guard component
- ✅ How the children prop works
- ✅ The difference between Navigate component and useNavigate hook
- ✅ Why loading state is critical for protected routes
- ✅ How to wrap routes for conditional access
- ✅ The difference between UX (hiding links) and security (protecting routes)
Next Step
Authentication is complete! Let's verify everything works together: