To use Firebase Auth with React, create an AuthContext provider that wraps onAuthStateChanged in a useEffect hook and shares the current user state across your component tree. Use signInWithEmailAndPassword and createUserWithEmailAndPassword for email/password flows, signInWithPopup for OAuth providers like Google, and signOut to end sessions. Protect routes by checking the user state in a wrapper component that redirects unauthenticated users.
Setting Up Firebase Authentication in React
Firebase Auth integrates cleanly with React through the Context API and hooks pattern. This tutorial shows you how to create an AuthContext that listens to auth state changes, implement email/password and Google sign-in flows, handle errors with user-friendly messages, and protect routes so only authenticated users can access certain pages. The pattern works with any React router (React Router, TanStack Router) and scales from simple apps to complex role-based systems.
Prerequisites
- A React 18+ project (Create React App, Vite, or Next.js)
- Firebase JS SDK v9+ installed (npm install firebase)
- A Firebase project with Email/Password sign-in enabled
- React Router v6+ for route protection examples
Step-by-step guide
Initialize Firebase and export the auth instance
Initialize Firebase and export the auth instance
Create a firebase configuration file that initializes the Firebase app and exports the auth instance. This file is imported throughout your app whenever you need to call auth methods. Store config values in environment variables for security.
1// src/lib/firebase.ts2import { initializeApp } from 'firebase/app';3import { getAuth } from 'firebase/auth';45const firebaseConfig = {6 apiKey: import.meta.env.VITE_FIREBASE_API_KEY,7 authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,8 projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,9 storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,10 messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,11 appId: import.meta.env.VITE_FIREBASE_APP_ID,12};1314const app = initializeApp(firebaseConfig);15export const auth = getAuth(app);16export default app;Expected result: Firebase is initialized and the auth instance is available for import throughout the application.
Create an AuthContext provider with useAuth hook
Create an AuthContext provider with useAuth hook
The AuthContext wraps onAuthStateChanged in a useEffect and provides the current user, loading state, and auth methods to the entire component tree. The useAuth custom hook gives any component quick access to the auth state. Place the AuthProvider at the root of your app so all components can consume it.
1// src/contexts/AuthContext.tsx2import { createContext, useContext, useEffect, useState, ReactNode } from 'react';3import {4 onAuthStateChanged,5 signInWithEmailAndPassword,6 createUserWithEmailAndPassword,7 signInWithPopup,8 GoogleAuthProvider,9 signOut as firebaseSignOut,10 User,11} from 'firebase/auth';12import { auth } from '../lib/firebase';1314interface AuthContextType {15 user: User | null;16 loading: boolean;17 signIn: (email: string, password: string) => Promise<void>;18 signUp: (email: string, password: string) => Promise<void>;19 signInWithGoogle: () => Promise<void>;20 signOut: () => Promise<void>;21}2223const AuthContext = createContext<AuthContextType | null>(null);2425export function AuthProvider({ children }: { children: ReactNode }) {26 const [user, setUser] = useState<User | null>(null);27 const [loading, setLoading] = useState(true);2829 useEffect(() => {30 const unsubscribe = onAuthStateChanged(auth, (user) => {31 setUser(user);32 setLoading(false);33 });34 return unsubscribe;35 }, []);3637 const value: AuthContextType = {38 user,39 loading,40 signIn: async (email, password) => {41 await signInWithEmailAndPassword(auth, email, password);42 },43 signUp: async (email, password) => {44 await createUserWithEmailAndPassword(auth, email, password);45 },46 signInWithGoogle: async () => {47 await signInWithPopup(auth, new GoogleAuthProvider());48 },49 signOut: async () => {50 await firebaseSignOut(auth);51 },52 };5354 return (55 <AuthContext.Provider value={value}>56 {children}57 </AuthContext.Provider>58 );59}6061export function useAuth() {62 const context = useContext(AuthContext);63 if (!context) throw new Error('useAuth must be used within AuthProvider');64 return context;65}Expected result: Any component can call useAuth() to access the current user, loading state, and auth methods.
Build a login form with error handling
Build a login form with error handling
Create a login component that uses the useAuth hook to sign in. Handle Firebase Auth errors by catching the error code and displaying a user-friendly message. Common errors include user-not-found, wrong-password, and invalid-email.
1// src/components/LoginForm.tsx2import { useState, FormEvent } from 'react';3import { useAuth } from '../contexts/AuthContext';4import { FirebaseError } from 'firebase/app';56export function LoginForm() {7 const { signIn, signInWithGoogle } = useAuth();8 const [email, setEmail] = useState('');9 const [password, setPassword] = useState('');10 const [error, setError] = useState('');11 const [loading, setLoading] = useState(false);1213 async function handleSubmit(e: FormEvent) {14 e.preventDefault();15 setError('');16 setLoading(true);1718 try {19 await signIn(email, password);20 } catch (err) {21 if (err instanceof FirebaseError) {22 switch (err.code) {23 case 'auth/user-not-found':24 setError('No account found with this email.');25 break;26 case 'auth/wrong-password':27 setError('Incorrect password.');28 break;29 case 'auth/invalid-email':30 setError('Please enter a valid email.');31 break;32 default:33 setError('Sign-in failed. Please try again.');34 }35 }36 } finally {37 setLoading(false);38 }39 }4041 return (42 <form onSubmit={handleSubmit}>43 {error && <p style={{ color: 'red' }}>{error}</p>}44 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" required />45 <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" required />46 <button type="submit" disabled={loading}>Sign In</button>47 <button type="button" onClick={signInWithGoogle}>Sign in with Google</button>48 </form>49 );50}Expected result: Users can sign in with email/password or Google, with clear error messages for failed attempts.
Create a ProtectedRoute component
Create a ProtectedRoute component
A ProtectedRoute component checks if the user is authenticated before rendering protected content. If the user is not signed in, redirect them to the login page. If auth state is still loading, show a loading indicator to prevent a flash of the login page before the auth check completes.
1// src/components/ProtectedRoute.tsx2import { Navigate } from 'react-router-dom';3import { useAuth } from '../contexts/AuthContext';45export function ProtectedRoute({ children }: { children: React.ReactNode }) {6 const { user, loading } = useAuth();78 if (loading) {9 return <div>Loading...</div>;10 }1112 if (!user) {13 return <Navigate to="/login" replace />;14 }1516 return <>{children}</>;17}1819// Usage in your router:20// <Route path="/dashboard" element={21// <ProtectedRoute><Dashboard /></ProtectedRoute>22// } />Expected result: Protected routes redirect unauthenticated users to login and show content only after authentication is confirmed.
Wire everything together in the app root
Wire everything together in the app root
Wrap your app with the AuthProvider at the root level so all components and routes can access auth state. Set up your router with public routes (login, sign-up) and protected routes (dashboard, settings) using the ProtectedRoute component.
1// src/App.tsx2import { BrowserRouter, Routes, Route } from 'react-router-dom';3import { AuthProvider } from './contexts/AuthContext';4import { ProtectedRoute } from './components/ProtectedRoute';5import { LoginForm } from './components/LoginForm';6import { Dashboard } from './pages/Dashboard';78export default function App() {9 return (10 <AuthProvider>11 <BrowserRouter>12 <Routes>13 <Route path="/login" element={<LoginForm />} />14 <Route15 path="/dashboard"16 element={17 <ProtectedRoute>18 <Dashboard />19 </ProtectedRoute>20 }21 />22 </Routes>23 </BrowserRouter>24 </AuthProvider>25 );26}Expected result: The app has complete auth routing: unauthenticated users see the login page, authenticated users access the dashboard.
Complete working example
1import { createContext, useContext, useEffect, useState, ReactNode } from 'react';2import {3 onAuthStateChanged,4 signInWithEmailAndPassword,5 createUserWithEmailAndPassword,6 signInWithPopup,7 GoogleAuthProvider,8 signOut as firebaseSignOut,9 User,10 AuthError,11} from 'firebase/auth';12import { auth } from '../lib/firebase';1314interface AuthContextType {15 user: User | null;16 loading: boolean;17 signIn: (email: string, password: string) => Promise<void>;18 signUp: (email: string, password: string) => Promise<void>;19 signInWithGoogle: () => Promise<void>;20 signOut: () => Promise<void>;21}2223const AuthContext = createContext<AuthContextType | null>(null);2425export function AuthProvider({ children }: { children: ReactNode }) {26 const [user, setUser] = useState<User | null>(null);27 const [loading, setLoading] = useState(true);2829 useEffect(() => {30 const unsubscribe = onAuthStateChanged(auth, (currentUser) => {31 setUser(currentUser);32 setLoading(false);33 });34 return unsubscribe;35 }, []);3637 async function signIn(email: string, password: string) {38 await signInWithEmailAndPassword(auth, email, password);39 }4041 async function signUp(email: string, password: string) {42 await createUserWithEmailAndPassword(auth, email, password);43 }4445 async function signInWithGoogle() {46 const provider = new GoogleAuthProvider();47 await signInWithPopup(auth, provider);48 }4950 async function signOut() {51 await firebaseSignOut(auth);52 }5354 return (55 <AuthContext.Provider value={{56 user, loading, signIn, signUp, signInWithGoogle, signOut,57 }}>58 {!loading && children}59 </AuthContext.Provider>60 );61}6263export function useAuth(): AuthContextType {64 const context = useContext(AuthContext);65 if (!context) {66 throw new Error('useAuth must be used within an AuthProvider');67 }68 return context;69}7071export function mapAuthError(code: string): string {72 const errorMap: Record<string, string> = {73 'auth/user-not-found': 'No account found with this email.',74 'auth/wrong-password': 'Incorrect password.',75 'auth/email-already-in-use': 'An account with this email already exists.',76 'auth/weak-password': 'Password must be at least 6 characters.',77 'auth/invalid-email': 'Please enter a valid email address.',78 'auth/too-many-requests': 'Too many attempts. Please try again later.',79 'auth/popup-closed-by-user': 'Sign-in popup was closed.',80 };81 return errorMap[code] ?? 'An error occurred. Please try again.';82}Common mistakes when using Firebase Auth with React
Why it's a problem: Not returning the unsubscribe function from useEffect, causing memory leaks with multiple listeners
How to avoid: Always return the onAuthStateChanged unsubscribe function from useEffect: return unsubscribe; This stops the listener when the component unmounts.
Why it's a problem: Rendering protected content before the auth check completes, causing a flash of the login page
How to avoid: Track a loading state that starts as true and flips to false after onAuthStateChanged fires. Show a loading indicator while loading is true.
Why it's a problem: Placing AuthProvider inside BrowserRouter, causing auth state to be unavailable in route guards
How to avoid: Wrap AuthProvider outside BrowserRouter so auth context is accessible to all routing components.
Why it's a problem: Displaying raw Firebase error messages to users instead of mapping error codes to friendly messages
How to avoid: Check err.code (like 'auth/wrong-password') and map it to a human-readable message. Firebase error messages are technical and confusing for end users.
Best practices
- Use the Context + useEffect + onAuthStateChanged pattern for global auth state management in React
- Always provide a loading state to prevent UI flashes before auth initialization completes
- Map Firebase Auth error codes to user-friendly messages instead of displaying raw error strings
- Store Firebase config in environment variables, not hardcoded in source code
- Create a ProtectedRoute wrapper component for consistent route guarding across the app
- Place AuthProvider at the root level, outside the router, so all components have access to auth state
- Clean up the onAuthStateChanged listener by returning the unsubscribe function from useEffect
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Build Firebase Auth for a React app with TypeScript. Show an AuthContext provider with onAuthStateChanged in useEffect, signInWithEmailAndPassword, signInWithPopup for Google, a ProtectedRoute component, error handling with error code mapping, and React Router v6 integration.
Create a React AuthContext with Firebase Auth v9 modular SDK. Include onAuthStateChanged in useEffect, email/password signIn and signUp, Google sign-in with signInWithPopup, signOut, a useAuth hook, a ProtectedRoute component with React Router, and an error code mapper.
Frequently asked questions
Does Firebase Auth persist sessions in React by default?
Yes. Firebase Auth defaults to browserLocalPersistence, which persists the session in IndexedDB and survives browser restarts. Users stay signed in until they explicitly sign out.
Can I use Firebase Auth without React Context?
Yes. You can call Firebase Auth methods directly from any component. However, the Context pattern centralizes auth state and prevents redundant onAuthStateChanged listeners.
How do I add Google Sign-In to my React app?
Enable Google as a provider in Firebase Console, create a GoogleAuthProvider instance, and call signInWithPopup(auth, provider). No additional packages are needed for web.
What happens when the Firebase ID token expires?
The Firebase client SDK automatically refreshes the ID token before it expires (every ~55 minutes). You do not need to handle token refresh manually on the client side.
Can I use Firebase Auth with state management libraries like Redux or Zustand?
Yes. Instead of React Context, you can store the user in Redux or Zustand. Set up the onAuthStateChanged listener in a top-level component and dispatch actions or update the store on auth state changes.
How do I add role-based access control with Firebase Auth?
Use custom claims set with the Admin SDK: admin.auth().setCustomUserClaims(uid, { role: 'admin' }). Access claims on the client via user.getIdTokenResult().claims. Enforce roles in Firestore security rules with request.auth.token.role.
Can RapidDev help implement authentication in my React application?
Yes. RapidDev can build complete auth systems with email/password, social logins, role-based access control, and protected routing patterns tailored to your React application.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation