Code To Learn logo

Code To Learn

M7: Forms & Authentication

L1: Create AuthProvider Component

Build the foundation for authentication with Context API

Let's build the foundation for authentication by creating an AuthProvider component using React's Context API! 🏗️

Why We Need AuthProvider

Right now, our app has no way to:

  • Know if a user is signed in
  • Share authentication state across components
  • Protect routes that require authentication
  • Add auth tokens to API requests

The solution: Create a global authentication context that wraps our entire app and provides auth state to any component that needs it.

What is Context API?

Create AuthProvider Component

Create a new file for the authentication provider:

src/components/AuthProvider.jsx
import { createContext, useContext, useState } from 'react';

// 1. Create Auth Context
const AuthContext = createContext(undefined);

// 2. Create useAuth Hook
export const useAuth = () => {
  const context = useContext(AuthContext);
  
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  
  return context;
};

// 3. Create AuthProvider Component
export const AuthProvider = ({ children }) => {
  // Authentication state
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  // Sign in function
  const signIn = async (email, password) => {
    // TODO: Implement in later lesson
    console.log('signIn called:', email);
  };

  // Sign out function
  const signOut = () => {
    setUser(null);
    setToken(null);
  };

  // Context value provided to children
  const value = {
    user,
    token,
    isLoading,
    signIn,
    signOut,
  };

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

Understanding the Code

Creating the context:

const AuthContext = createContext(undefined);

Why undefined as default?

This forces us to use the hook correctly. If someone tries to use useAuth() outside of <AuthProvider>, they'll get:

const context = useContext(AuthContext);  // undefined
if (context === undefined) {
  throw new Error('useAuth must be used within an AuthProvider');
}

Better than null because:

  • null could be a valid state (no user)
  • undefined clearly means "not wrapped in provider"
  • Catches setup errors early

Three pieces of state:

const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [isLoading, setIsLoading] = useState(true);

Why each one:

1. user (null | object)

// null = not signed in
user = null

// object = signed in
user = {
  id: '123',
  name: 'John Doe',
  email: 'john@example.com'
}

2. token (null | string)

// null = no auth token
token = null

// string = has auth token
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

3. isLoading (boolean)

// true = checking for existing session
isLoading = true  // On app start

// false = done checking
isLoading = false  // After checking token

Why start with isLoading: true?

When the app loads, we need to check if the user has an existing session (stored token). During this check, we show a loading state. Once the check completes, we set isLoading to false.

Custom hook for accessing context:

export const useAuth = () => {
  const context = useContext(AuthContext);
  
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  
  return context;
};

Why this pattern?

1. Error handling:

// ❌ Without hook:
function Component() {
  const auth = useContext(AuthContext);
  // If outside provider, auth is undefined
  // Might cause runtime errors later
}

// ✅ With hook:
function Component() {
  const auth = useAuth();
  // Throws error immediately if outside provider
  // Catches mistakes during development
}

2. Cleaner imports:

// ❌ Without hook:
import { useContext } from 'react';
import { AuthContext } from './AuthProvider';
const auth = useContext(AuthContext);

// ✅ With hook:
import { useAuth } from './AuthProvider';
const auth = useAuth();

3. Abstraction: If we change the internal implementation, components using useAuth() don't need updates.

Provider component wraps children:

export const AuthProvider = ({ children }) => {
  // State and functions...
  
  const value = {
    user,
    token,
    isLoading,
    signIn,
    signOut,
  };

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

Key points:

1. children prop:

<AuthProvider>
  <App />  {/* This is children */}
</AuthProvider>

2. value object: Everything in value is accessible via useAuth():

const { user, token, signIn, signOut } = useAuth();

3. Provider renders children:

{children}  // Renders wrapped components

Flow:

AuthProvider
  → Manages auth state
  → Provides value to children
  → Children can access via useAuth()

Placeholder Functions

Notice the placeholder functions:

const signIn = async (email, password) => {
  // TODO: Implement in later lesson
  console.log('signIn called:', email);
};

const signOut = () => {
  setUser(null);
  setToken(null);
};

Why not implement now?

We're building incrementally:

  1. Lesson 1 - Create provider structure
  2. Lesson 2 - Add to app
  3. Lesson 3 - Fetch token on mount
  4. Later - Full sign-in implementation

This approach helps you understand each piece before combining them.

Testing the Component

Even though it's not connected yet, let's verify it compiles:

Check file structure:

src/
├── components/
│   └── AuthProvider.jsx  ← New file!

Verify imports:

Make sure all imports are correct:

  • createContext from 'react'
  • useContext from 'react'
  • useState from 'react'

Check exports:

File should export:

  • useAuth hook
  • AuthProvider component

What's Next?

In the next lesson, we'll:

  1. Add AuthProvider to wrap our entire app
  2. Make auth state available to all components
  3. Test that useAuth() hook works

✅ Lesson Complete! You've created the AuthProvider component with Context API!

Key Takeaways

  • Context API provides global state without prop drilling
  • Three-part pattern: Context, Provider, Custom Hook
  • AuthProvider manages user, token, and loading state
  • useAuth hook provides safe access to auth context
  • Error handling catches misuse during development
  • Placeholder functions prepare for future implementation