Introduction: Why Authentication Context Matters

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.

Example: Prop Drilling vs. Context

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:

// App.tsx
export default function App() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const login = () => setIsAuthenticated(true);
  const logout = () => setIsAuthenticated(false);

  return (
    <Layout
      isAuthenticated={isAuthenticated}
      logout={logout}
    />
  );
}

// Layout.tsx
export function Layout({ isAuthenticated, logout }) {
  return (
    <div>
      <Header isAuthenticated={isAuthenticated} logout={logout} />
    </div>
  );
}

// Header.tsx
export function Header({ isAuthenticated, logout }) {
  return (
    <nav>
      {isAuthenticated ? (
        <button onClick={logout}>Log out</button>
      ) : (
        <span>Please log in</span>
      )}
    </nav>
  );
}

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.

// AuthContext.tsx
import React, { createContext, useContext, useState } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const login = () => setIsAuthenticated(true);
  const logout = () => setIsAuthenticated(false);

  const value = { isAuthenticated, login, logout };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Custom hook for easy access
export const useAuth = () => useContext(AuthContext);
// App.tsx
export default function App() {
  return (
    <AuthProvider>
      <Layout />
    </AuthProvider>
  );
}

// Layout.tsx
export function Layout() {
  return (
    <div>
      <Header />
    </div>
  );
}

// Header.tsx
import { useAuth } from './AuthContext';

export function Header() {
  const { isAuthenticated, logout } = useAuth();

  return (
    <nav>
      {isAuthenticated ? (
        <button onClick={logout}>Log out</button>
      ) : (
        <span>Please log in</span>
      )}
    </nav>
  );
}

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.

Understanding React Context For Authentication

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 as login(token) and logout(). 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 in localStorage so that the state can be restored even after a page refresh.

  • Why this is better than props: Instead of manually passing isAuthenticated and logout down 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 useContext and 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.

App Bootstrap & Provider Placement (Using AuthContext Across Routes)

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 ProtectedRoute can access useAuth().
  • If AuthProvider is 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 RouterProvider is the most straightforward way to ensure that.

Below is a minimal example for your entry point.

///src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import { AuthProvider } from './features/auth/AuthContext';
// Use the precompiled Tailwind CSS (full utilities)
import './tailwind.css';
import { router } from './routes/router';
import reportWebVitals from './reportWebVitals';

const container = document.getElementById('root');
if (!container) throw new Error('Root container missing');
const root = ReactDOM.createRoot(container);
root.render(
  <React.StrictMode>
    <AuthProvider>
      <RouterProvider router={router} />
    </AuthProvider>
  </React.StrictMode>
);

reportWebVitals();

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.

Step-by-Step: Building the AuthContext

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:

// src/features/auth/AuthContext.tsx
import { createContext, useState, useContext, ReactNode } from 'react';
import { getToken, removeToken, saveToken } from '../../utils/auth';

interface AuthContextType {
  isAuthenticated: boolean;
  login: (token: string) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [token, setToken] = useState<string | null>(getToken());

  // No interceptor needed; our fetch-based client reads token directly from localStorage

  const login = (newToken: string) => {
    saveToken(newToken);
    setToken(newToken);
  };

  const logout = () => {
    removeToken();
    setToken(null);
  };

  const value = { isAuthenticated: !!token, login, logout };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within an AuthProvider');
  return 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 useState to keep track of the authentication token.
    • The useEffect sets up an interceptor so that every API request includes the token if it exists.
    • The login function saves the token, updates the state, and navigates to the user’s shelf.
    • The logout function removes the token, updates the state, and navigates to the home page.
    • The value object contains everything you need to know about authentication: whether the user is authenticated, and how to log in or out.
  • 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.

Using AuthContext In 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:

// src/App.tsx
import { Outlet, NavLink, Link, useNavigate } from 'react-router-dom';
import { useAuth } from './features/auth/AuthContext';

function App() {
  const { isAuthenticated, logout } = useAuth();
  const navigate = useNavigate();
  const linkStyles = ({ isActive }: { isActive: boolean }) =>
    `px-3 py-2 rounded-md transition-colors ${
      isActive ? 'text-white bg-sky-600' : 'hover:text-sky-400'
    }`;

  return (
    <div className="bg-slate-900 text-white min-h-screen font-sans">
      <header className="bg-slate-800 shadow-lg">
        <nav className="container mx-auto px-6 py-4 flex justify-between items-center">
          <NavLink to="/" className="text-2xl font-bold text-sky-400 hover:text-sky-300">
            ShelfPilot
          </NavLink>
          <div className="flex gap-2 items-center">
            <NavLink to="/" className={linkStyles} end>
              Home
            </NavLink>
            <NavLink to="/catalog" className={linkStyles}>
              Catalog
            </NavLink>
            {isAuthenticated && (
              <NavLink to="/shelf" className={linkStyles}>
                My Shelf
              </NavLink>
            )}
            <NavLink to="/reading-list" className={linkStyles}>
              Reading List
            </NavLink>
            <div className="ml-4 flex gap-2">
              {isAuthenticated ? (
                <button
                  onClick={() => { logout(); navigate('/'); }}
                  className="px-3 py-2 rounded-md bg-slate-700 hover:bg-slate-600"
                >
                  Logout
                </button>
              ) : (
                <>
                  <Link to="/login" className="px-3 py-2 rounded-md bg-slate-700 hover:bg-slate-600">
                    Login
                  </Link>
                  <Link to="/register" className="px-3 py-2 rounded-md bg-sky-500 hover:bg-sky-600">
                    Register
                  </Link>
                </>
              )}
            </div>
          </div>
        </nav>
      </header>
      <main className="container mx-auto p-6">
        <Outlet />
      </main>
      <footer className="container mx-auto px-6 py-8 text-sm text-slate-400">
        © {new Date().getFullYear()} ShelfPilot
      </footer>
    </div>
  );
}

export default App;

Explanation:

  • The useAuth hook gives you access to isAuthenticated and logout.
  • 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, then navigate('/') 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:

// src/components/ProtectedRoute.tsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../features/auth/AuthContext";

export default function ProtectedRoute() {
  const { isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  return <Outlet />;
}

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.
LoginForm (Revisited): Using login() from useAuth

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.

//src/features/auth/LoginForm.tsx
import { useState, FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { apiClient, paths } from '../../api/client';
import { useAuth } from './AuthContext';

export default function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const { login } = useAuth();
  const navigate = useNavigate();

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setError('');
    try {
      const res: any = await apiClient.post(paths.auth('/login'), { username, password });
      const token = res?.data?.data?.access_token;
      if (!token) throw new Error('Token missing in response');

      // <— This is the crucial piece provided by AuthContext
      login(token);

      navigate('/shelf');
    } catch (err: any) {
      setError(err?.data?.message || 'Invalid username or password.');
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4 max-w-sm mx-auto">
      {error && <p className="text-red-400 bg-red-900/50 p-3 rounded-md">{error}</p>}
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="Username"
        required
        className="w-full bg-slate-800 p-2 rounded-md"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
        className="w-full bg-slate-800 p-2 rounded-md"
      />
      <button type="submit" className="w-full bg-sky-500 hover:bg-sky-600 p-2 rounded-md font-semibold">
        Login
      </button>
    </form>
  );
}

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 AuthContext state so isAuthenticated becomes true.
    • Causes all useAuth() consumers (nav, guards) to re-render and reflect the logged-in state.
  • Finally, we navigate('/shelf') to land the user on a protected page.
Summary And What’s Next

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 RouterProvider with AuthProvider at the root so every routed component can call useAuth().
  • Context design: Keep AuthContext small and stable—isAuthenticated, login, logout to minimize re-renders.
  • Consumption: Extract { isAuthenticated, logout } from useAuth() for conditional nav and use a ProtectedRoute to 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!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal