Javascript
Front End
context.api
React.js
Authentication is a fundamental aspect of modern web applications, allowing users to securely access personalized content and features. React's Context API provides an elegant solution for managing authentication state across your entire application without prop drilling.
Let's explore how to implement an authentication context that will serve as the foundation for protecting routes and managing user sessions.
The Authentication Context pattern centralizes user authentication logic and state in one place, making it accessible to any component in your application tree. This approach offers several benefits:
When building a React application, you'll often need to access user authentication state from multiple components. Without a proper state management solution, you might end up passing authentication-related props through many levels of components (prop drilling), which can make your code harder to maintain and understand.
The Context API solves this by providing a way to share values like authentication state across the component tree without explicitly passing props through every level. This is particularly useful for authentication because:
Here's a basic implementation of an Authentication Context in React:
1import React, { createContext, useContext, useState, useEffect } from "react"; 2 3// Create the authentication context 4const AuthContext = createContext(); 5 6// Custom hook for using the auth context 7export const useAuth = () => { 8 return useContext(AuthContext); 9}; 10 11// Provider component that wraps your app and makes auth object available 12export const AuthProvider = ({ children }) => { 13 const [currentUser, setCurrentUser] = useState(null); 14 const [loading, setLoading] = useState(true); 15 16 // Sign-in function 17 const login = async (email, password) => { 18 try { 19 // Mock API call - replace with your actual authentication service 20 const response = await mockAuthService.login(email, password); 21 setCurrentUser(response.user); 22 localStorage.setItem("authToken", response.token); 23 return response.user; 24 } catch (error) { 25 throw new Error(error.message); 26 } 27 }; 28 29 // Sign-out function 30 const logout = () => { 31 setCurrentUser(null); 32 localStorage.removeItem("authToken"); 33 }; 34 35 // Check if user is authenticated 36 const isAuthenticated = () => { 37 return !!currentUser; 38 }; 39 40 // Load user from token on initial render 41 useEffect(() => { 42 const checkAuthStatus = async () => { 43 try { 44 const token = localStorage.getItem("authToken"); 45 if (token) { 46 // Mock API call to verify token - replace with your auth service 47 const user = await mockAuthService.verifyToken(token); 48 setCurrentUser(user); 49 } 50 } catch (error) { 51 // Token is invalid 52 localStorage.removeItem("authToken"); 53 } finally { 54 setLoading(false); 55 } 56 }; 57 58 checkAuthStatus(); 59 }, []); 60 61 // Context value object that will be shared 62 const value = { 63 currentUser, 64 login, 65 logout, 66 isAuthenticated, 67 loading 68 }; 69 70 return ( 71 <AuthContext.Provider value={value}> 72 {!loading && children} 73 </AuthContext.Provider> 74 ); 75};
Let's break down the key parts of this implementation:
Context Creation: We create a new context using createContext()
. This context will hold our authentication state and methods.
Custom Hook: The useAuth
hook provides an easy way to access the authentication context from any component. It uses the useContext
hook internally.
Provider Component: The AuthProvider
component is where we manage the authentication state and provide it to the rest of the application. It includes:
Authentication Methods:
login
: Handles user authentication, stores the token, and updates the user statelogout
: Clears the user state and removes the stored tokenisAuthenticated
: Checks if a user is currently authenticatedToken Persistence: The useEffect
hook checks for an existing token on component mount and attempts to restore the user's session if a valid token exists.
The structure above introduces:
AuthContext
for storing authentication stateuseAuth
custom hook for easy access to the contextAuthProvider
component that manages auth state and provides functionsTo use the authentication context, wrap your application with the AuthProvider
component:
1import React from "react"; 2import ReactDOM from "react-dom"; 3import App from "./App"; 4import { AuthProvider } from "./context/AuthContext"; 5 6ReactDOM.render( 7 <React.StrictMode> 8 <AuthProvider> 9 <App /> 10 </AuthProvider> 11 </React.StrictMode>, 12 document.getElementById("root") 13);
This setup ensures that the authentication context is available throughout your entire application. The AuthProvider
component should be placed high in your component tree, typically at the root level, so that all components can access the authentication state.
Now, any component in your application can access the authentication state and functions using the useAuth
hook:
1import React, { useState } from "react"; 2import { useAuth } from "../context/AuthContext"; 3 4function LoginForm() { 5 const [email, setEmail] = useState(""); 6 const [password, setPassword] = useState(""); 7 const [error, setError] = useState(""); 8 const { login } = useAuth(); 9 10 const handleSubmit = async (e) => { 11 e.preventDefault(); 12 setError(""); 13 14 try { 15 await login(email, password); 16 // Redirect or show success message 17 } catch (error) { 18 setError("Failed to sign in. Please check your credentials."); 19 } 20 }; 21 22 return ( 23 <form onSubmit={handleSubmit}> 24 <div> 25 <label htmlFor="email">Email</label> 26 <input 27 id="email" 28 type="email" 29 value={email} 30 onChange={(e) => setEmail(e.target.value)} 31 required 32 /> 33 </div> 34 <div> 35 <label htmlFor="password">Password</label> 36 <input 37 id="password" 38 type="password" 39 value={password} 40 onChange={(e) => setPassword(e.target.value)} 41 required 42 /> 43 </div> 44 {error && <div className="error">{error}</div>} 45 <button type="submit">Sign In</button> 46 </form> 47 ); 48}
This login form component demonstrates how to use the authentication context in a practical scenario. The useAuth
hook gives us access to the login
function, which we can call when the form is submitted. The component also handles error states and provides feedback to the user.
With the authentication context in place, creating a user profile component becomes straightforward:
1import React from "react"; 2import { useAuth } from "../context/AuthContext"; 3 4function Profile() { 5 const { currentUser, logout } = useAuth(); 6 7 if (!currentUser) { 8 return <div>Please log in to view your profile.</div>; 9 } 10 11 return ( 12 <div className="profile-container"> 13 <h2>User Profile</h2> 14 <div className="profile-info"> 15 <p><strong>Email:</strong> {currentUser.email}</p> 16 <p><strong>Name:</strong> {currentUser.name}</p> 17 <p><strong>Account created:</strong> {new Date(currentUser.createdAt).toLocaleDateString()}</p> 18 </div> 19 <button onClick={logout}>Sign Out</button> 20 </div> 21 ); 22}
The profile component shows how to access and display user information from the authentication context. It uses the currentUser
state to display user details and the logout
function to handle signing out. The component also includes a conditional render to show a message when no user is logged in.
For more complex applications, you might need to handle user roles and permissions. Let's enhance our authentication context to support user roles:
1// Inside AuthProvider.js 2const AuthProvider = ({ children }) => { 3 // ... previous code 4 5 // Check if user has a specific role 6 const hasRole = (role) => { 7 if (!currentUser || !currentUser.roles) return false; 8 return currentUser.roles.includes(role); 9 }; 10 11 // Check if user has permission 12 const hasPermission = (permission) => { 13 if (!currentUser || !currentUser.permissions) return false; 14 return currentUser.permissions.includes(permission); 15 }; 16 17 const value = { 18 currentUser, 19 login, 20 logout, 21 isAuthenticated, 22 hasRole, 23 hasPermission, 24 loading 25 }; 26 27 // ... rest of the provider 28};
This enhancement adds role-based access control to our authentication context. The hasRole
and hasPermission
functions allow us to check if a user has specific roles or permissions, which is useful for implementing access control in your application.
This enhancement lets you conditionally render components based on user roles and permissions:
1function AdminDashboard() { 2 const { hasRole } = useAuth(); 3 4 if (!hasRole("admin")) { 5 return <div>You don't have permission to access this page.</div>; 6 } 7 8 return ( 9 <div className="admin-dashboard"> 10 <h1>Admin Dashboard</h1> 11 {/* Admin-only content */} 12 </div> 13 ); 14}
The AdminDashboard
component demonstrates how to use role-based access control to restrict access to certain parts of your application. It checks if the current user has the "admin" role before rendering the admin dashboard content.
For better user experience, you'll want to persist authentication state across page refreshes. We've already implemented this using localStorage
in our example, but you can enhance it further with these considerations:
localStorage
when available1// Enhanced token verification with expiration check 2const verifyToken = async (token) => { 3 // First check if token is expired by decoding it (if using JWT) 4 const decodedToken = decodeJWT(token); // You'll need a JWT decoding function 5 const currentTime = Date.now() / 1000; 6 7 if (decodedToken.exp < currentTime) { 8 throw new Error("Token expired"); 9 } 10 11 // Then verify with the server 12 return await authService.verifyToken(token); 13};
This enhanced token verification function adds an expiration check to ensure that expired tokens are properly handled. It first decodes the JWT token to check its expiration time, then verifies it with the server if it's still valid.
Implementing an authentication context in your React application provides a robust foundation for managing user state across components. This pattern centralizes authentication logic, simplifies access control, and creates a more maintainable codebase.
Key benefits of using the Authentication Context pattern:
In the next lesson, we'll explore how to use this authentication context to implement private routes with React Router, ensuring that certain parts of your application are only accessible to authenticated users.