JavaScript and React course Β· Module 6: React Router
Protecting Routes in the Space Station
In this lesson9
In different sectors of our space station, we need different access levels. Not every crew member should have access to all areas - similarly in web applications, where some pages should be accessible only to logged-in users or people with specific permissions.
What are Protected Routes?
Protected routes are a mechanism that allows restricting access to certain parts of the application only to authorized users. They are usually implemented by checking whether the user is logged in or has the appropriate permissions.
Basic Implementation of a Protected Route
The simplest way to protect a route is by using a higher-order component (HOC) that checks the authentication state:
1import { Navigate, useLocation } from 'react-router-dom';
2import { useAuth } from './AuthContext';
3
4function ProtectedRoute({ children }) {
5 const { user, isLoading } = useAuth();
6 const location = useLocation();
7
8 // Show loading indicator while checking authentication state
9 if (isLoading) {
10 return <div className="loading">Checking section permissions...</div>;
11 }
12
13 // If the user is not logged in, redirect to the login page
14 if (!user) {
15 // Save the path the user came from so we can return after login
16 return <Navigate to="/login" state={{ from: location.pathname }} replace />;
17 }
18
19 // If the user is logged in, render the protected content
20 return children;
21}Then we can use this component in our routes:
1<Routes>
2 <Route path="/" element={<HomePage />} />
3 <Route path="/login" element={<LoginPage />} />
4
5 {/* Protected routes */}
6 <Route path="/command-center" element={
7 <ProtectedRoute>
8 <CommandCenter />
9 </ProtectedRoute>
10 } />
11
12 <Route path="/restricted-area" element={
13 <ProtectedRoute>
14 <RestrictedArea />
15 </ProtectedRoute>
16 } />
17</Routes>Authentication Context
For the above code to work, we need an authentication context that will store information about the logged-in user:
1import { createContext, useContext, useState, useEffect } from 'react';
2
3const AuthContext = createContext(null);
4
5export function AuthProvider({ children }) {
6 const [user, setUser] = useState(null);
7 const [isLoading, setIsLoading] = useState(true);
8
9 useEffect(() => {
10 // Check if the user is already logged in (e.g., via a token in localStorage)
11 const checkAuthStatus = async () => {
12 try {
13 setIsLoading(true);
14 const token = localStorage.getItem('authToken');
15
16 if (token) {
17 // Token validation on the server or JWT decoding
18 const userData = await validateToken(token); // function implemented separately
19 setUser(userData);
20 }
21 } catch (error) {
22 console.error('Authentication error:', error);
23 // In case of error, log out the user
24 localStorage.removeItem('authToken');
25 setUser(null);
26 } finally {
27 setIsLoading(false);
28 }
29 };
30
31 checkAuthStatus();
32 }, []);
33
34 // Login and logout functions
35 const login = async (credentials) => {
36 setIsLoading(true);
37 try {
38 // Call login API
39 const response = await apiLogin(credentials);
40 const { token, user: userData } = response;
41
42 // Save the token
43 localStorage.setItem('authToken', token);
44 setUser(userData);
45 return true;
46 } catch (error) {
47 console.error('Login error:', error);
48 return false;
49 } finally {
50 setIsLoading(false);
51 }
52 };
53
54 const logout = () => {
55 localStorage.removeItem('authToken');
56 setUser(null);
57 };
58
59 return (
60 <AuthContext.Provider value={{ user, isLoading, login, logout }}>
61 {children}
62 </AuthContext.Provider>
63 );
64}
65
66// Hook for easy access to the authentication context
67export function useAuth() {
68 return useContext(AuthContext);
69}Redirect After Login
On the login page, we can use information about the previous path to redirect the user back after logging in:
1import { useLocation, useNavigate } from 'react-router-dom';
2import { useAuth } from './AuthContext';
3import { useState } from 'react';
4
5function LoginPage() {
6 const { login } = useAuth();
7 const navigate = useNavigate();
8 const location = useLocation();
9
10 // Get the path the user was redirected from (if it exists)
11 const from = location.state?.from || '/command-center';
12
13 const [credentials, setCredentials] = useState({
14 username: '',
15 password: ''
16 });
17
18 const handleChange = (e) => {
19 const { name, value } = e.target;
20 setCredentials(prev => ({
21 ...prev,
22 [name]: value
23 }));
24 };
25
26 const handleSubmit = async (e) => {
27 e.preventDefault();
28
29 const success = await login(credentials);
30
31 if (success) {
32 // Redirect back to the protected path
33 navigate(from, { replace: true });
34 }
35 };
36
37 return (
38 <div className="login-container">
39 <h1>Space Station Access Center</h1>
40 <form onSubmit={handleSubmit}>
41 <div className="form-group">
42 <label htmlFor="username">Astronaut ID:</label>
43 <input
44 id="username"
45 name="username"
46 type="text"
47 value={credentials.username}
48 onChange={handleChange}
49 required
50 />
51 </div>
52 <div className="form-group">
53 <label htmlFor="password">Access Code:</label>
54 <input
55 id="password"
56 name="password"
57 type="password"
58 value={credentials.password}
59 onChange={handleChange}
60 required
61 />
62 </div>
63 <button type="submit" className="login-button">
64 Gain Access
65 </button>
66 </form>
67 </div>
68 );
69}Protecting Routes Based on Roles
We often need more complex logic than just checking if the user is logged in. For example, it may be necessary to check if the user has the appropriate role:
1function RoleProtectedRoute({ children, requiredRoles = [] }) {
2 const { user, isLoading } = useAuth();
3 const location = useLocation();
4
5 if (isLoading) {
6 return <div className="loading">Checking permissions...</div>;
7 }
8
9 // Check if the user is logged in
10 if (!user) {
11 return <Navigate to="/login" state={{ from: location.pathname }} replace />;
12 }
13
14 // Check if the user has the required role
15 const hasRequiredRole = requiredRoles.length === 0 ||
16 requiredRoles.some(role => user.roles.includes(role));
17
18 if (!hasRequiredRole) {
19 return <Navigate to="/unauthorized" replace />;
20 }
21
22 return children;
23}
24
25// Usage:
26<Route
27 path="/engine-room"
28 element={
29 <RoleProtectedRoute requiredRoles={['engineer', 'captain']}>
30 <EngineRoom />
31 </RoleProtectedRoute>
32 }
33/>Organizing the Protected Section of the Application
A good practice is to group all protected routes in one place, which makes them easier to manage:
1function AppRoutes() {
2 return (
3 <Routes>
4 {/* Public paths */}
5 <Route path="/" element={<HomePage />} />
6 <Route path="/login" element={<LoginPage />} />
7 <Route path="/register" element={<RegisterPage />} />
8 <Route path="/about" element={<AboutPage />} />
9
10 {/* Protected section */}
11 <Route path="/dashboard" element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
12 <Route index element={<Overview />} />
13 <Route path="profile" element={<Profile />} />
14 <Route path="settings" element={<Settings />} />
15
16 {/* Routes for engineers */}
17 <Route path="maintenance" element={
18 <RoleProtectedRoute requiredRoles={['engineer']}>
19 <MaintenancePanel />
20 </RoleProtectedRoute>
21 } />
22
23 {/* Routes for captains */}
24 <Route path="command" element={
25 <RoleProtectedRoute requiredRoles={['captain']}>
26 <CommandCenter />
27 </RoleProtectedRoute>
28 } />
29 </Route>
30
31 {/* Error handling */}
32 <Route path="/unauthorized" element={<UnauthorizedPage />} />
33 <Route path="*" element={<NotFoundPage />} />
34 </Routes>
35 );
36}In the example above, we use nested routes where the main DashboardLayout component is protected, and all routes nested within it are automatically protected. Additionally, some routes have extra role-based protections.
Advanced Techniques
Lazy Loading for Protected Routes
We can combine route protection with lazy loading to improve application performance:
1import { lazy, Suspense } from 'react';
2
3// Lazy loading components
4const CommandCenter = lazy(() => import('./CommandCenter'));
5const MaintenancePanel = lazy(() => import('./MaintenancePanel'));
6
7// In routing:
8<Route path="command" element={
9 <RoleProtectedRoute requiredRoles={['captain']}>
10 <Suspense fallback={<Loading />}>
11 <CommandCenter />
12 </Suspense>
13 </RoleProtectedRoute>
14} />Remembering Multiple Routes
Sometimes a user may try to access multiple protected routes before logging in. We can remember more than one route:
1function useRedirectHistory() {
2 const [history, setHistory] = useState([]);
3
4 const addToHistory = (path) => {
5 setHistory(prev => {
6 // Avoid duplicates
7 if (!prev.includes(path)) {
8 return [...prev, path].slice(-5); // Keep only the last 5 paths
9 }
10 return prev;
11 });
12 };
13
14 const getLastPath = () => history[history.length - 1] || '/dashboard';
15
16 return { addToHistory, getLastPath };
17}Handling Session Expiration
An important aspect of route protection is handling session expiration:
1function useSessionExpiration() {
2 const { logout } = useAuth();
3 const navigate = useNavigate();
4
5 useEffect(() => {
6 // Handle JWT token expiration
7 const checkTokenExpiration = () => {
8 const token = localStorage.getItem('authToken');
9 if (token) {
10 try {
11 const payload = JSON.parse(atob(token.split('.')[1]));
12 const expirationTime = payload.exp * 1000; // Convert to milliseconds
13
14 if (Date.now() >= expirationTime) {
15 logout();
16 navigate('/login', {
17 state: {
18 message: 'Your session has expired. Please log in again.'
19 }
20 });
21 }
22 } catch (e) {
23 console.error('Token validation error:', e);
24 logout();
25 }
26 }
27 };
28
29 const interval = setInterval(checkTokenExpiration, 60000); // Check every minute
30 return () => clearInterval(interval);
31 }, [logout, navigate]);
32}Summary
Protecting routes in React Router is a key aspect of creating secure applications. The main techniques include:
- Creating higher-order components (HOC) to protect routes
- Using React Context to manage authentication state
- Redirecting users to the login page while remembering the original path
- Implementing role-based access control
- Organizing the application structure with a clear division into public and protected sections
- Handling session expiration and automatic logout
Remember that client-side (browser) security should never be the only layer of protection. It should always be supplemented with proper server-side security, which should independently verify user permissions before providing access to protected resources.
Code for this lesson: App.jsx
1import React, { useState, createContext, useContext } from 'react';
2import { BrowserRouter, Routes, Route, Link, Navigate, useNavigate, useLocation } from 'react-router-dom';
3
4// Authorization context
5const AuthContext = createContext();
6
7function AuthProvider({ children }) {
8 const [user, setUser] = useState(null);
9
10 const login = (username) => {
11 setUser({ name: username, role: 'astronaut' });
12 };
13
14 const logout = () => {
15 setUser(null);
16 };
17
18 return (
19 <AuthContext.Provider value={{ user, login, logout }}>
20 {children}
21 </AuthContext.Provider>
22 );
23}
24
25function useAuth() {
26 return useContext(AuthContext);
27}
28
29// Protected Route component
30function ProtectedRoute({ children }) {
31 const { user } = useAuth();
32 const location = useLocation();
33
34 if (!user) {
35 // Redirect to login while saving the location
36 return <Navigate to="/login" state={{ from: location }} replace />;
37 }
38
39 return children;
40}
41
42// Login page
43function LoginPage() {
44 const [username, setUsername] = useState('');
45 const { login } = useAuth();
46 const navigate = useNavigate();
47 const location = useLocation();
48
49 const from = location.state?.from?.pathname || '/dashboard';
50
51 const handleSubmit = (e) => {
52 e.preventDefault();
53 if (username.trim()) {
54 login(username);
55 navigate(from, { replace: true });
56 }
57 };
58
59 return (
60 <div className="page login">
61 <h1>Login</h1>
62 <form onSubmit={handleSubmit}>
63 <input
64 value={username}
65 onChange={(e) => setUsername(e.target.value)}
66 placeholder="Username"
67 />
68 <button type="submit">Log in</button>
69 </form>
70 <p className="info">Log in to access protected sections.</p>
71 </div>
72 );
73}
74
75// Public page
76function HomePage() {
77 return (
78 <div className="page">
79 <h1>Public page</h1>
80 <p>This page is accessible to everyone.</p>
81 </div>
82 );
83}
84
85// Protected page
86function DashboardPage() {
87 const { user, logout } = useAuth();
88 const navigate = useNavigate();
89
90 const handleLogout = () => {
91 logout();
92 navigate('/');
93 };
94
95 return (
96 <div className="page dashboard">
97 <h1>Dashboard</h1>
98 <p>Welcome, {user?.name}! This is a protected page.</p>
99 <button onClick={handleLogout} className="logout">Log out</button>
100 </div>
101 );
102}
103
104// Secret page
105function SecretPage() {
106 return (
107 <div className="page secret">
108 <h1>Secret page</h1>
109 <p>Congratulations! You have access to classified data.</p>
110 </div>
111 );
112}
113
114// Nawigacja
115function Navigation() {
116 const { user } = useAuth();
117
118 return (
119 <nav>
120 <Link to="/">Home</Link>
121 {user ? (
122 <>
123 <Link to="/dashboard">Dashboard</Link>
124 <Link to="/secret">Secret</Link>
125 <span className="user-info">{user.name}</span>
126 </>
127 ) : (
128 <Link to="/login">Log in</Link>
129 )}
130 </nav>
131 );
132}
133
134function App() {
135 return (
136 <AuthProvider>
137 <BrowserRouter>
138 <div className="app">
139 <Navigation />
140 <Routes>
141 <Route path="/" element={<HomePage />} />
142 <Route path="/login" element={<LoginPage />} />
143 <Route
144 path="/dashboard"
145 element={
146 <ProtectedRoute>
147 <DashboardPage />
148 </ProtectedRoute>
149 }
150 />
151 <Route
152 path="/secret"
153 element={
154 <ProtectedRoute>
155 <SecretPage />
156 </ProtectedRoute>
157 }
158 />
159 </Routes>
160 </div>
161 </BrowserRouter>
162 </AuthProvider>
163 );
164}
165
166export default App;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. How does NavLink in React Router allow you to dynamically set a CSS class based on the active state?
Hands-on tasks in the game
- Horizontal ordering
Arrange the Link component syntax elements:
- Click in order
Arrange the NavLink with a dynamic class: