Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 66

Building a Global Authentication Context

Recommended resources

Hello! Welcome to your next lesson.

In our last session, we successfully built a login form and learned how to store the authentication token in localStorage. This was a great step, but it leaves us with a challenge: how do we make the rest of our application aware that the user is logged in? We could have every component check localStorage directly, but that's inefficient and doesn't create a reactive, single source of truth.

Today, we'll solve that problem. This lesson addresses the learning outcome: Create a global AuthContext to provide authentication status and user data throughout the app. We'll use React's Context API to create a centralized place to manage our user's authentication state, making it easily and efficiently accessible to any component in our application.

This is a fundamental pattern for building any non-trivial React application and is a cornerstone of effective state management.

The Problem: Prop Drilling and Stale State

Imagine our application has many nested components. If a deeply nested component needs to know if the user is logged in, we would have to pass that information down as a prop through every single parent component. This is called "prop drilling" and it's cumbersome and hard to maintain.

Furthermore, localStorage is just a storage mechanism; it's not a state management tool. Components don't automatically re-render when a value in localStorage changes. We need a way to both store the auth state and have our UI react to changes in it.


The Solution: The React Context API

The Context API is React's built-in solution for sharing state across the entire app without having to pass props down manually at every level. It's perfect for global data like user authentication status, theme settings, or language preferences.

It works with three main parts:

  1. createContext(): Creates a "context" object. Think of it as creating a new channel for our global data.
  2. Context.Provider: A component that makes the context value available to all child components in its tree.
  3. useContext(): A hook that lets a component subscribe to the context and read its value.

Let's see how to put these pieces together to build our AuthContext.


1. Creating the AuthContext and AuthProvider

The standard pattern is to create three things in a single file (e.g., src/context/AuthProvider.js):

  • The AuthContext itself.
  • An AuthProvider component that will wrap our application.
  • A custom useAuth hook to make consuming the context easier.

The following article provides a clear, step-by-step guide to setting this up.

How to manage user authentication With React JS

Please read the first section of this DEV Community article. It lays out the foundational code for our authentication context.

Focus on the section titled 'Create AuthContext and AuthProvider'. Pay close attention to the three parts in the first code block: const AuthContext = createContext(); The AuthProvider functional component that returns <AuthContext.Provider>. Notice how it accepts and renders {children}. The custom useAuth hook that simplifies using useContext.

The useAuth hook is a powerful convention. Instead of components needing to import both useContext and AuthContext, they can just import and call useAuth(). This keeps our component logic cleaner and centralizes the context logic.


2. Managing State and Actions within the Provider

Our AuthProvider is just a shell right now. To make it useful, we need to:

  1. Use the useState hook to hold the authentication state (the user and token).
  2. Connect it to our previous lesson by initializing the state from localStorage.
  3. Create login and logout functions that update this state and also update localStorage.
  4. Pass the state and these functions to the components via the Provider's value prop.

This next reading section shows the complete implementation.

How to manage user authentication With React JS

This section of the same article shows how to bring the AuthProvider to life with state and logic.

Read the section 'Create Authentication Logic'. Analyze the updated AuthProvider.js code: State Initialization: Notice how useState is used for user and token. Critically, see how the token is initialized: useState(localStorage.getItem("site") || ""). This is how we remember the user across browser sessions. Actions: Examine the loginAction and logOut functions. See how they use setUser, setToken, and interact with localStorage. The value Prop: Look at the return statement. The value prop of the provider is an object containing everything we want to make available globally: { token, user, loginAction, logOut }.

This AuthProvider is now our application's single source of truth for authentication. It encapsulates the logic for storing, retrieving, and clearing the user's session.

To see this built from scratch in a video format, you can watch the following clip.

React Context Api with user Auth Project | useContext hook | React Project

This video provides a great visual demonstration of creating the context, provider, state, and login/logout functions.

Watch the video from 04:00 to 05:32 to see the initial setup of the context, provider, and state. Then, watch from 08:57 to 14:16 to see how the login and logout functions are implemented and passed through the context.


3. Providing and Consuming the Context

Now that we have our AuthProvider, we need to wrap our application with it. This is typically done in your main App.js or main.jsx file.

How to manage user authentication With React JS

This brief section from the article shows the final step in setting up the provider.

Look at the App.js code snippet right after the 'Create AuthContext and AuthProvider' section. It shows how the <AuthProvider> component wraps the rest of the application content.

With the AuthProvider in place, any component within it can now access the authentication state. Let's see how a Login or Dashboard component would use our useAuth hook.

// In a Login component
import { useAuth } from './context/AuthProvider';

const Login = () => {
  const { loginAction } = useAuth(); // Get the login function from the context
  // ... state for form inputs

  const handleLogin = (e) => {
    e.preventDefault();
    // Call the global login function from the context
    loginAction({ username, password }); 
  };

  return <form onSubmit={handleLogin}>...</form>;
};


// In a Dashboard or Header component
import { useAuth } from './context/AuthProvider';

const Header = () => {
  const { user, logOut } = useAuth(); // Get user data and logout function

  return (
    <header>
      {user ? (
        <>
          <span>Welcome, {user.username}!</span>
          <button onClick={logOut}>Logout</button>
        </>
      ) : (
        <span>You are not logged in.</span>
      )}
    </header>
  );
};

Notice how clean this is. The components don't know or care about localStorage or how the authentication logic works. They just consume the state and functions provided by the useAuth hook.


A Quick Note on Performance

The way we have structured our AuthProvider—in its own file and wrapping children—is a standard best practice. There is a common mistake that can cause your entire application to re-render unnecessarily, which can be a major performance problem.

This short video does an excellent job of explaining the mistake and why the structure we've used prevents it.

This Context API Mistake Ruins Your Whole React App (All Components Re-Render)

This video highlights a subtle but critical performance issue with the Context API and shows why creating a dedicated provider component is the correct approach.

Watch the video to understand the problem of unnecessary re-renders when context state is defined directly inside a parent component (the wrong way) and how creating a separate provider that accepts children solves this (the right way, which we've just implemented).

Understanding this distinction is a sign of a proficient React developer. Your experience as a Laravel developer likely gives you an appreciation for building things in a scalable and performant way, and this pattern is key to that in React.


Conclusion

You've now learned how to create one of the most important architectural patterns in a React application. By centralizing state management with a global AuthContext, you've made your application more robust, maintainable, and performant.

Key Takeaways:

  • React Context API is the solution for sharing global state and avoiding "prop drilling."
  • The AuthProvider pattern involves creating a context, a provider component with its own state, and a custom useAuth hook.
  • The provider encapsulates all authentication logic, including state updates and interactions with localStorage.
  • You initialize state from localStorage to persist the login session across browser reloads.
  • Components consume the context via the clean useAuth() hook, decoupling them from the implementation details of authentication.

Preview of the Next Lesson:

Now that we have a reliable, global way to know if a user is authenticated (if (auth.token)), we can put it to use. In the next lesson, we will create a protected route component that redirects unauthenticated users. This component will use our AuthContext to guard specific pages (like a dashboard or profile page) and ensure only logged-in users can access them.