Welcome back! In the last few lessons, you learned how to set up an API client, build a login form, protect routes, and create a registration flow. Now, you are ready to make your authentication logic more organized and accessible throughout your React app.
In a real-world application, you often need to know whether a user is logged in or not in many different places — like the navigation bar, protected pages, or even when making API requests. If you try to pass this information down through props, your code can get messy and hard to manage. This is where React Context comes in. By using an authentication context, you can keep track of the user's login state and make it available anywhere in your app without having to pass it through every component.
To see why context helps, let’s look at a simple example. Imagine you have a navigation bar deep inside your app that needs to know whether the user is logged in and how to log them out.
❌ Bad Approach: Prop Drilling
Here, isAuthenticated and logout are passed through multiple components, even if only the deepest one needs them:
Notice how isAuthenticated and logout get passed through Layout even though Layout doesn’t use them. As your app grows, this “prop drilling” gets messy.
✅ Better Approach: Using Context
With AuthContext, you provide authentication state and actions once at the top level. Any component can access them directly without threading props everywhere.
With this setup, only the component that actually needs the authentication data consumes it. You don’t have to manually pass isAuthenticated and logout through every intermediate layer.
React Context is a way to share data across your app without having to pass props down manually at every level. Think of it as a shared key that any component can use to check if the user is logged in, log them out, or update their authentication status.
For example, if you have a navigation bar and a protected page, both can use the same context to know if the user is authenticated. This keeps your code clean and avoids duplication.
When building a React app, different parts of your UI often need to know whether a user is logged in. For example, your navigation bar might need to show “Log out” instead of “Log in,” while your profile page needs access to the current user’s details. Without context, you would have to pass this information down as props through every intermediate component — a process called prop drilling. This quickly becomes messy, repetitive, and error-prone.
React Context solves this problem by acting as a shared store that any component can tap into, no matter how deep in the tree it is. With an AuthContext, we create one central place that holds authentication data and makes it available everywhere in the app.
-
What the AuthContext provides: At its core, the authentication context stores important state like
isAuthenticated, the user’s token, or even user profile info. Alongside the state, it also provides actions such aslogin(token)andlogout(). This makes it a single source of truth for authentication, so when the token changes, the rest of the app automatically knows whether the user is logged in or not. Typically, the token is also saved inlocalStorageso that the state can be restored even after a page refresh. -
Why this is better than props: Instead of manually passing
isAuthenticatedandlogoutdown through every layer of your app, any component can directly “subscribe” to the context. This keeps your code cleaner and makes it easier to maintain as your app grows.
- A note on performance: One important thing to know is that whenever the context value changes, all components that use it will re-render. For most apps this is fine, but for larger projects you’ll want to keep your context lean. A good practice is to memoize the value you pass to the provider and avoid putting data in context that changes very frequently.
- Other options at scale: For more advanced scenarios, libraries like Redux, Zustand, or Jotai can give you finer control over which components update when data changes. These tools are powerful, but it’s best to start by understanding
useContextand React’s built-in patterns first.
In short, AuthContext is what allows your entire React app to consistently know who the user is and whether they’re logged in, without having to juggle props everywhere.
To use AuthContext, the provider must wrap the entire subtree that calls useAuth()—that includes your routed pages and guards. The simplest, reliable setup is to wrap the RouterProvider with AuthProvider at the root:
- This guarantees that all routes, layout components, and utilities like
ProtectedRoutecan accessuseAuth(). - If
AuthProvideris mounted inside a page instead, anything outside that page (e.g., other routes or top-level nav) won’t see auth state. - Order matters only in terms of ancestry: the provider must be an ancestor of consumers. Wrapping
RouterProvideris the most straightforward way to ensure that.
Below is a minimal example for your entry point.
Why this placement works: Every routed screen and navigation element is now a child of AuthProvider, so useAuth() is available everywhere—header, pages, protected routes, etc.
Let’s build the authentication context step by step. We’ll create a context, a provider, and a custom hook to use the context. We’ll also handle login and logout actions.
Here’s the main code for the authentication context:
Let’s break down what’s happening here:
AuthContext: This is the context object that will hold our authentication state and actions.AuthProvider: This component wraps your app and provides authentication state to all its children.- It uses
useStateto keep track of the authentication token. - The
useEffectsets up an interceptor so that every API request includes the token if it exists. - The
loginfunction saves the token, updates the state, and navigates to the user’s shelf. - The
logoutfunction removes the token, updates the state, and navigates to the home page. - The
valueobject contains everything you need to know about authentication: whether the user is authenticated, and how to log in or out.
- It uses
useAuth: This is a custom hook that makes it easy to use the authentication context in any component.
There is no direct output from this code, but it sets up the foundation for managing authentication across your app.
Now that you have the authentication context, let’s see how you can use it in your components. For example, you might want to show different navigation links depending on whether the user is logged in.
Here’s how you can use the context in your main app component:
Explanation:
- The
useAuthhook gives you access toisAuthenticatedandlogout. - The navigation bar shows different links depending on whether the user is logged in.
- If the user is authenticated, they see a "My Shelf" link and a "Logout" button.
- If not, they see "Login" and "Register" buttons.
What’s happening
- We extract
{ isAuthenticated, logout } = useAuth(). - Conditional rendering:
{isAuthenticated && <NavLink to="/shelf" ... />}only shows “My Shelf” when logged in.- The auth button group switches between Login/Register and Logout.
logout()clears storage/state, thennavigate('/')returns the user to a public page.
For route protection, prefer a component that reads isAuthenticated and decides whether to render the nested route:
Another example is protecting routes. Here’s how you can use the context to protect a route:
Explanation:
- This component checks if the user is authenticated.
- If not, it redirects them to the login page.
- If they are, it renders the child routes.
We previously built the login component. Everything should look familiar except the call to login(token)—that’s what wires the UI to the context you just created. Here is the full component you provided; we’ll annotate how useAuth().login is used.
How login(token) integrates
- After a successful POST to
/auth/login, we extract the token from the backend envelope. login(token):- Saves the token to
localStorage. - Updates
AuthContextstate soisAuthenticatedbecomestrue. - Causes all
useAuth()consumers (nav, guards) to re-render and reflect the logged-in state.
- Saves the token to
- Finally, we
navigate('/shelf')to land the user on a protected page.
In this lesson, you learned how to create an authentication context in React. You saw how to set up the context, provide it to your app, and use it in your components to manage login state and protect routes. This approach keeps your authentication logic organized and easy to use anywhere in your app.
- Provider placement: Wrap
RouterProviderwithAuthProviderat the root so every routed component can calluseAuth(). - Context design: Keep
AuthContextsmall and stable—isAuthenticated,login,logoutto minimize re-renders. - Consumption: Extract
{ isAuthenticated, logout }fromuseAuth()for conditional nav and use aProtectedRouteto guard screens. - Login integration: Call
login(token)after successful authentication to persist and broadcast auth state; the UI updates automatically.
You are now ready to practice using the authentication context in real code. In the next exercises, you will get hands-on experience with these concepts, such as updating navigation based on authentication and protecting routes. This will help you build more secure and user-friendly applications. Keep up the great work!
