Imagine displaying the user's name in the header, showcasing their posts on the home screen, and revealing their profile picture in the sidebarβall while keeping certain pages off-limits to unauthenticated users.
In this guide, you'll learn how to share user data between components with the context API and the useContext hook. We'll also dive into integrating this powerful technique with react-router to protect routes and update the nav bar based on the user's logged-in status.
Setting Up the Project with Vite
Let's dive right into creating a new React 18 project using Vite. The goal here is to create an app that can handle user authentication using useContext. So, fasten your seat belts and let's get started!
npx create-vite my-auth-app --template react
cd my-auth-app
npm install
Now that our project is ready, let's add the necessary components and set up our authentication context.
Creating the Authentication Context
First things first, let's create a context that will manage the user's logged-in status and store their JWT token. We'll call it AuthContext.
Note
Using Context allows us to keep track of information with useState or useReducer, then any component can access that information without having to pass it down as props.
If you're completely unfamiliar with how userContext works, check out the official documentation.
import { createContext, useState } from "react";
const AuthContext = createContext();
function AuthProvider(props) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [token, setToken] = useState(null);
const login = (jwtToken) => {
setIsLoggedIn(true);
setToken(jwtToken);
};
const logout = () => {
setIsLoggedIn(false);
setToken(null);
};
const value = {
isLoggedIn,
token,
login,
logout,
};
return <AuthContext.Provider value={value} {...props} />;
}
export { AuthContext, AuthProvider };
Here's what's happening in the code above:
- Import
createContextanduseStatefrom React. - Create a new context called
AuthContext. - Define an
AuthProviderfunction component that will wrap our entire app. This component maintains theisLoggedInandtokenstate. - Define the
loginandlogoutfunctions to manage the user's logged-in status and token. - Pass the context value as an object containing the state variables and functions.
- Finally, export the
AuthContextandAuthProviderso we can use them in other parts of our app.
Now we can make it so that any component is able to access the isLoggedIn,token,login, and logout values.
Creating the Login Screen
We're going to create a very simple login screen that mocks a network request for a JWT token.
import { useContext, useState } from "react";
import { AuthContext } from "./AuthContext";
function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { login } = useContext(AuthContext);
const handleSubmit = async (event) => {
event.preventDefault();
// Fake network request for JWT token
const jwtToken = "fake-jwt-token";
login(jwtToken);
};
return (
<div>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<br />
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<br />
<button type="submit">Login</button>
</form>
</div>
);
}
export default Login;
In the Login component above, we:
- Import the necessary hooks and
AuthContext. - Use the
useContexthook to access theloginfunction from ourAuthContext. - Define local state for email and password input fields.
- Define a
handleSubmitfunction that simulates a network request for a JWT token and calls theloginfunction with the fake token. - Render a simple form with email and password input fields and a submit button.
Creating the Home Screen
Now let's create a basic home screen that will display the user's logged-in status and allow them to log out.
import { useContext } from "react";
import { AuthContext } from "./AuthContext";
function Home() {
const { isLoggedIn, logout } = useContext(AuthContext);
return (
<div>
<h1>Home</h1>
{isLoggedIn ? (
<>
<p>Welcome! You are logged in.</p>
<button onClick={logout}>Logout</button>
</>
) : (
<p>Please log in to access more features.</p>
)}
</div>
);
}
export default Home;
In the Home component above, we:
- Import the necessary hooks and
AuthContext. - Use the
useContexthook to access theisLoggedInandlogoutfunctions from ourAuthContext. - Render a welcome message and a logout button if the user is logged in, otherwise, display a message prompting the user to log in.
Setting Up Routes with react-router-dom
Before diving into wrapping our app with AuthProvider, let's first install react-router-dom and set up routes for our Home and Login components.
If you're not familiar with react-router-dom, check out:
npm install react-router-dom
import { BrowserRouter, Link, Route, Routes } from "react-router-dom";
import Home from "./Home";
import Login from "./Login";
function App() {
return (
<BrowserRouter>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
</ul>
</nav>
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
</Routes>
</main>
</BrowserRouter>
);
}
export default App;
So we have a login screen and a home screen, but if you try running the app right now, it won't work. That's because we haven't wrapped our app with AuthProvider yet. A component must be nested inside AuthProvider in order to access the context values.
Wrapping the App with AuthProvider
With our routes in place, let's wrap our entire app with the AuthProvider. This will make the user data available to any component nested inside it.
import { BrowserRouter, Link, Route, Routes } from "react-router-dom";
import { AuthProvider } from "./AuthContext";
import Home from "./Home";
import Login from "./Login";
function App() {
return (
<AuthProvider>
<BrowserRouter>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
</ul>
</nav>
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
</Routes>
</main>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
Testing the App
Now that everything is set up, it's time to test our app.
npm run dev
Open your browser and navigate to http://localhost:5173. You should see the home page displaying the "Please log in to access more features." message. Click on the "Login" link in the navigation menu and enter any email and password to log in. After logging in, you can navigate back to the home page, which will now display a welcome message and a logout button.
And that's it! We've successfully created an app that uses React 18's useContext hook to manage user authentication. Remember, this tutorial uses a fake JWT token for simplicity, but in a real-world scenario, you'd want to replace it with a proper network request to your authentication server.
Redirect to Home After Login
After the user logs in, they should be navigated to a different page. In this case, we'll redirect them to the home page.
import { useContext, useState } from "react";
import { AuthContext } from "./AuthContext";
import { Navigate } from "react-router-dom";
function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { login, isLoggedIn } = useContext(AuthContext);
const handleSubmit = async (event) => {
event.preventDefault();
// Fake network request for JWT token
const jwtToken = "fake-jwt-token";
login(jwtToken);
};
if (isLoggedIn) {
return <Navigate to="/" />;
}
//...
Here we are checking if the user is logged in, and if they are, we redirect them to the home page using the Navigate component from react-router-dom. We can use a similar technique to redirect the user to the login page if when they are not logged in.
Adding a Protected Route
Now that we have our AuthProvider and routes set up, let's add a protected route to our app. This route will only be accessible to authenticated users. We'll create a Profile component that will be our protected route.
Creating the Profile Component
import { useContext } from "react";
import { AuthContext } from "./AuthContext";
function Profile() {
const { token } = useContext(AuthContext);
return (
<div>
<h1>Profile</h1>
<p>Your secret token is: {token}</p>
</div>
);
}
export default Profile;
In the Profile component above, we:
- Import the necessary hooks and
AuthContext. - Use the
useContexthook to access thetokenfrom ourAuthContext. - Render the user's secret token as part of their profile information.
Creating a Route Guard
To protect the Profile route, we'll create a higher-order component called RouteGuard that will check if the user is logged in before rendering the protected component.
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "./AuthContext";
function RouteGuard({ children }) {
const { isLoggedIn } = useContext(AuthContext);
return isLoggedIn ? children : <Navigate to="/login" />;
}
export default RouteGuard;
In the RouteGuard component above, we:
- Import the necessary hooks,
Navigatefromreact-router-dom, andAuthContext. - Use the
useContexthook to access theisLoggedInvalue from ourAuthContext. - Check if the user is logged in, and if so, render the protected component. Otherwise, redirect the user to the
/loginpage.
Adding the Protected Route
Finally, let's add the Profile component as a protected route in our App component.
import { BrowserRouter, Link, Route, Routes } from "react-router-dom";
import { AuthProvider } from "./AuthContext";
import Home from "./Home";
import Login from "./Login";
import Profile from "./Profile";
import RouteGuard from "./RouteGuard";
function App() {
return (
<AuthProvider>
<BrowserRouter>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/profile">Profile</Link>
</li>
</ul>
</nav>
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route
path="/profile"
element={
<RouteGuard>
<Profile />
</RouteGuard>
}
/>
</Routes>
</main>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
Now, the Profile route is protected by the RouteGuard component. Users who aren't logged in will be redirected to the /login page when trying to access the /profile route.
With this setup, you've successfully implemented a protected route in your
React app using AuthProvider and react-router-dom. Not only can you share data between pages with ease, but you can also create a more secure user experience by protecting sensitive routes.
Enhancing the User Experience with a Dynamic Navigation Bar
To make our app even more engaging and user-friendly, let's create a dynamic navigation bar that updates its content based on the user's logged-in status.
import { useContext } from "react";
import { BrowserRouter, Link, Route, Routes } from "react-router-dom";
import { AuthProvider, AuthContext } from "./AuthContext";
import Home from "./Home";
import Login from "./Login";
import Profile from "./Profile";
import RouteGuard from "./RouteGuard";
function Navigation() {
const { isLoggedIn } = useContext(AuthContext);
return (
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
{isLoggedIn ? (
<>
<li>
<Link to="/profile">Profile</Link>
</li>
</>
) : (
<li>
<Link to="/login">Login</Link>
</li>
)}
</ul>
</nav>
);
}
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Navigation />
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route
path="/profile"
element={
<RouteGuard>
<Profile />
</RouteGuard>
}
/>
</Routes>
</main>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
In the Navigation component above, we:
- Import the necessary hooks and
AuthContext. - Use the
useContexthook to access theisLoggedInvalue from ourAuthContext. - Conditionally render the "Profile" or "Login" link based on the user's logged-in status.
Now, the navigation bar will display the "Profile" link only when the user is logged in. Otherwise, it will show the "Login" link.
Storing and Loading JWT Token from Local Storage
In this section, we'll enhance our app by storing the JWT token in the browser's local storage. This way, the user's logged-in status will persist across sessions, and they won't have to log in again every time they visit the site.
First, let's update our AuthContext to store the JWT token in local storage when the user logs in and load the token from local storage when the context is initialized.
import { createContext, useContext, useState, useEffect } from "react";
const AuthContext = createContext();
function AuthProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [jwt, setJwt] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const storedJwt = localStorage.getItem("jwt");
if (storedJwt) {
setIsLoggedIn(true);
setJwt(storedJwt);
}
setIsLoading(false);
}, []);
function login(token) {
setIsLoggedIn(true);
setJwt(token);
localStorage.setItem("jwt", token);
}
function logout() {
setIsLoggedIn(false);
setJwt(null);
localStorage.removeItem("jwt");
}
const authValue = {
isLoggedIn,
isLoading,
login,
logout,
};
return <AuthContext.Provider value={authValue}>{children}</AuthContext.Provider>;
}
export { AuthProvider, AuthProvider };
In the updated AuthProvider component, we've made the following changes:
- Added a
useEffecthook that runs when the component is mounted. This hook checks if there's a JWT token stored in local storage, and if so, it sets theisLoggedInstate totrueand updates thejwtstate with the stored token. - Modified the
loginfunction to store the JWT token in local storage when the user logs in. - Modified the
logoutfunction to remove the JWT token from local storage when the user logs out.
When the user logs in, the JWT token will be stored in local storage, and their logged-in status will persist across browser sessions. When they visit the site again, their logged-in status will be automatically restored using the stored token.
Since useEffect runs after the component is mounted, we won't know if the user is logged in until components have already been loaded. So we need to include an isLoading state to indicate that the user's logged-in status is still being determined.
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "./AuthContext";
function RouteGuard({ children }) {
const { isLoggedIn, isLoading } = useContext(AuthContext);
if (isLoading) {
return <></>;
}
if (!isLoggedIn) {
return <Navigate to="/login" />;
}
return children;
}
export default RouteGuard;
- If the localStorage hasn't been checked yet,
isLoadingwill be true and we'll return an empty fragment to "do nothing". - If the user is not logged in, we'll redirect them to the login page.
- Otherwise, the user is logged in and we'll render the child components.
Final Code
https://github.com/Sam-Meech-Ward/react-router-auth-context
Summary
In this tutorial, we've built a React 18 app using Vite that can handle user authentication using the useContext hook. We've created an AuthContext to manage the logged-in status and JWT token, a Login component to handle user logins, and a Home component to display user status. We also added a protected route and a secret page to demonstrate how to restrict access based on the user's logged-in status.
As you can see, the useContext hook is a powerful tool for managing and sharing state across your app. While this tutorial focused on user authentication, the concepts can be applied to many other use cases as well.

