Step 6: Create Auth Context
Time: ~10 minutes | Type: Coding | Concepts: React Context, Custom Hooks, Firebase Auth
What We're Building
Creating an AuthContext that:
- Tracks current user state
- Provides
signIn,signUp, andsignOutfunctions - Includes a custom
useAuth()hook for easy access - Wraps our app to make auth available everywhere
Before You Code: Understanding Custom Hooks
💡 Ask AI First:
What is a custom hook in React? Why do we create a useAuth() hook instead of using Context directly? What does createContext() do? What is a Provider component and how do we use it?
What you should learn:
- Custom hooks start with
useand can use other hooks useAuth()is cleaner thanuseContext(AuthContext)in every componentcreateContext()creates the tunnel for sharing data- Provider wraps components that need access to the context
Let's Build It
Prompt: Create Auth Context and Provider
Create an authentication context for my React + TypeScript app.
Requirements:
1. File location: src/contexts/AuthContext.tsx
2. Create AuthContext with createContext
3. Create AuthProvider component that:
- Tracks user state (null when logged out, User object when logged in)
- Tracks loading state (true while checking auth status)
- Provides signIn(email, password) function using Firebase
- Provides signUp(email, password) function using Firebase
- Provides signOut() function using Firebase
- Listens to Firebase auth state changes with onAuthStateChanged
4. Create useAuth() custom hook that returns the context
5. Export both AuthProvider and useAuth
Use:
- import { auth } from '../lib/firebase'
- import Firebase auth functions: createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, onAuthStateChanged
- TypeScript types for proper type safety
After creating the code, explain:
- What onAuthStateChanged does
- Why we need a loading state
- How useAuth() simplifies using the context
- Where we'll wrap our app with AuthProviderWhat to expect:
- New file
src/contexts/AuthContext.tsx - AuthContext created with createContext
- AuthProvider component with useState for user and loading
- useEffect with onAuthStateChanged listener
- Three async functions: signIn, signUp, signOut
- useAuth custom hook
- TypeScript types for User and AuthContextType
Files you'll create:
src/contexts/AuthContext.tsx
Understanding What You Built
After AI creates the code, make sure you understand each part:
💡 Ask AI to Explain:
Walk me through the AuthContext.tsx file: 1. What does createContext() create and why do we give it undefined as initial value? 2. What does onAuthStateChanged() do and when does it fire? 3. Why do we return a cleanup function from useEffect? 4. What happens when signIn() is called? Walk me through the flow. 5. Why does useAuth() throw an error if used outside AuthProvider? 6. What TypeScript types were created and why?
Key concepts to understand:
createContext()— Creates the context object (the tunnel)onAuthStateChanged()— Firebase listener that fires whenever auth state changesloadingstate — Prevents flashing wrong content while checking authuseAuth()throws error — Prevents using context in wrong places- Type safety — TypeScript ensures you use auth correctly
Let's Use It
Now wrap your app with the AuthProvider:
Prompt: Wrap App with AuthProvider
Update App.tsx to wrap the entire app with AuthProvider.
Structure should be:
<AuthProvider>
<BrowserRouter>
{/* existing routes and layout */}
</BrowserRouter>
</AuthProvider>
Show me the updated App.tsx code.What to expect:
- Import statement:
import { AuthProvider } from './contexts/AuthContext'; - AuthProvider wraps everything
- Existing router and routes stay inside
Files you'll modify:
src/App.tsx
Verify It Works
Manual Testing:
Check file exists:
bashls src/contexts/AuthContext.tsxRun the app:
bashnpm run devCheck console:
- Should see no errors
- App should load normally
- You won't see auth working yet (we haven't built forms)
Test useAuth hook (temporary):
Add this to any page (e.g.,
HomePage.tsx):typescriptimport { useAuth } from '../contexts/AuthContext'; export default function HomePage() { const { user, loading } = useAuth(); console.log('Auth state:', { user, loading }); return <h1>Home Page</h1>; }What to expect in console:
- First:
{ user: null, loading: true } - Then:
{ user: null, loading: false }
Remove the console.log after verifying!
- First:
Checklist:
- [ ]
src/contexts/AuthContext.tsxfile exists - [ ] No TypeScript errors
- [ ] App wrapped with AuthProvider in App.tsx
- [ ] useAuth() hook can be imported
- [ ] Console shows loading changes from true to false
- [ ] No Firebase errors in console
Common Issues
"AuthContext is undefined"
Problem: Trying to use useAuth() outside of AuthProvider
Fix:
- Make sure AuthProvider wraps your entire app in App.tsx
- Provider must be ABOVE any component that calls useAuth()
"Cannot read properties of undefined (reading 'user')"
Problem: createContext() default value is undefined
Fix: This is fine! The error should only happen if you use useAuth() outside AuthProvider, which the custom hook prevents:
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}"onAuthStateChanged is not a function"
Problem: Wrong import from Firebase
Fix:
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from '../lib/firebase';
// Use it like this:
onAuthStateChanged(auth, (user) => {
// ...
});App shows "Loading..." forever
Problem: Loading state never set to false
Fix: Make sure onAuthStateChanged callback sets loading:
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user);
setLoading(false); // Must set this!
});
return unsubscribe;
}, []);TypeScript error: "Type 'User | null' is not assignable"
Problem: Missing TypeScript types
Fix:
import { User } from 'firebase/auth';
const [user, setUser] = useState<User | null>(null);Code Example
Your AuthContext.tsx should look roughly like this:
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import {
User,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut as firebaseSignOut,
onAuthStateChanged,
} from 'firebase/auth';
import { auth } from '../lib/firebase';
interface AuthContextType {
user: User | null;
loading: boolean;
signUp: (email: string, password: string) => Promise<void>;
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const signUp = async (email: string, password: string) => {
await createUserWithEmailAndPassword(auth, email, password);
};
const signIn = async (email: string, password: string) => {
await signInWithEmailAndPassword(auth, email, password);
};
const signOut = async () => {
await firebaseSignOut(auth);
};
return (
<AuthContext.Provider value={{ user, loading, signUp, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}What You Learned
At this point you should understand:
- ✅ How to create a React Context
- ✅ How to create a Provider component
- ✅ How to create a custom hook for context
- ✅ How onAuthStateChanged tracks Firebase auth state
- ✅ Why we need a loading state during auth checks
- ✅ How to wrap an app with a Provider
Next Step
Auth context is ready! But before we build forms, let's understand how React forms work: