To detect auth state changes in Firebase, use onAuthStateChanged from the modular SDK. This listener fires whenever a user signs in, signs out, or the auth token refreshes. Pass the auth instance and a callback that receives the user object (or null when signed out). Always unsubscribe from the listener when the component unmounts to prevent memory leaks. In React, use useEffect with the unsubscribe function as the cleanup return value.
Detecting Auth State Changes in Firebase
Firebase Auth uses onAuthStateChanged as the primary way to track whether a user is signed in. This observer fires once when the page loads (restoring the persisted session) and again whenever the auth state changes. This tutorial covers setting up the listener, handling the loading state before the initial check completes, building a reusable React auth context, and cleaning up listeners to avoid memory leaks.
Prerequisites
- A Firebase project with Authentication enabled
- Firebase JS SDK v9+ installed in your project
- At least one sign-in method configured in the Firebase Console
- Basic knowledge of React hooks (for the React integration steps)
Step-by-step guide
Set up onAuthStateChanged listener
Set up onAuthStateChanged listener
Import getAuth and onAuthStateChanged from firebase/auth. Call onAuthStateChanged with the auth instance and a callback function. The callback receives the User object when someone is signed in, or null when no user is authenticated. The function returns an unsubscribe function that you should call to stop listening. The listener fires immediately with the current state, then again on every subsequent change.
1import { getAuth, onAuthStateChanged, User } from 'firebase/auth';23const auth = getAuth();45const unsubscribe = onAuthStateChanged(auth, (user: User | null) => {6 if (user) {7 console.log('User signed in:', user.uid, user.email);8 } else {9 console.log('User signed out');10 }11});1213// Call unsubscribe() when you no longer need the listenerExpected result: The callback fires immediately with the current user (or null), then fires again each time the user signs in or out.
Handle the initial loading state
Handle the initial loading state
When your app loads, Firebase must check local storage for a persisted auth session. Until onAuthStateChanged fires for the first time, you do not know whether the user is signed in. Use a loading flag to show a loading spinner or skeleton screen during this initial check. Set loading to true initially, then set it to false inside the onAuthStateChanged callback.
1import { getAuth, onAuthStateChanged, User } from 'firebase/auth';23let currentUser: User | null = null;4let isLoading = true;56const auth = getAuth();78onAuthStateChanged(auth, (user) => {9 currentUser = user;10 isLoading = false;11 // Update your UI here12 renderApp();13});1415function renderApp() {16 if (isLoading) {17 // Show loading spinner18 return;19 }20 if (currentUser) {21 // Show authenticated UI22 } else {23 // Show login screen24 }25}Expected result: The app shows a loading state until Firebase confirms whether a user session exists, then renders the appropriate UI.
Build a React auth context with useEffect
Build a React auth context with useEffect
In React, create an AuthContext that wraps your app and provides the current user and loading state to all components. Use useEffect to set up onAuthStateChanged when the provider mounts, and return the unsubscribe function as the cleanup to prevent memory leaks. Export a useAuth hook for easy access in child components.
1import { createContext, useContext, useEffect, useState, ReactNode } from 'react';2import { getAuth, onAuthStateChanged, User } from 'firebase/auth';34interface AuthContextType {5 user: User | null;6 loading: boolean;7}89const AuthContext = createContext<AuthContextType>({10 user: null,11 loading: true,12});1314export function AuthProvider({ children }: { children: ReactNode }) {15 const [user, setUser] = useState<User | null>(null);16 const [loading, setLoading] = useState(true);1718 useEffect(() => {19 const auth = getAuth();20 const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {21 setUser(firebaseUser);22 setLoading(false);23 });24 return unsubscribe;25 }, []);2627 return (28 <AuthContext.Provider value={{ user, loading }}>29 {children}30 </AuthContext.Provider>31 );32}3334export const useAuth = () => useContext(AuthContext);Expected result: Any component in your app can call useAuth() to get the current user and loading state, and the listener is automatically cleaned up when the provider unmounts.
Protect routes based on auth state
Protect routes based on auth state
Use the auth context to redirect unauthenticated users away from protected pages. Create a ProtectedRoute wrapper component that checks the user state. While loading, show a spinner. If no user is signed in after loading completes, redirect to the login page.
1import { Navigate } from 'react-router-dom';2import { useAuth } from './AuthProvider';34export function ProtectedRoute({ children }: { children: ReactNode }) {5 const { user, loading } = useAuth();67 if (loading) {8 return <div>Loading...</div>;9 }1011 if (!user) {12 return <Navigate to="/login" replace />;13 }1415 return <>{children}</>;16}1718// Usage in your router:19// <Route path="/dashboard" element={20// <ProtectedRoute><Dashboard /></ProtectedRoute>21// } />Expected result: Protected routes show a loading state during auth initialization, redirect to login when no user is found, and render the protected content for authenticated users.
Use onIdTokenChanged for token-level monitoring
Use onIdTokenChanged for token-level monitoring
For apps that need to detect token refreshes (not just sign-in/sign-out), use onIdTokenChanged instead of onAuthStateChanged. This listener fires whenever the ID token changes, including automatic hourly refreshes and custom claim updates. This is useful when you need to sync the latest token with your backend or detect custom claim changes.
1import { getAuth, onIdTokenChanged } from 'firebase/auth';23const auth = getAuth();45const unsubscribe = onIdTokenChanged(auth, async (user) => {6 if (user) {7 // Get the fresh token to send to your backend8 const token = await user.getIdToken();9 console.log('Token refreshed for:', user.uid);10 // Send token to your API for session management11 } else {12 console.log('No user — clear session');13 }14});1516// Call unsubscribe() to stop listeningExpected result: The callback fires on sign-in, sign-out, and every automatic token refresh, giving you access to the latest ID token.
Complete working example
1import {2 createContext,3 useContext,4 useEffect,5 useState,6 ReactNode7} from 'react';8import { initializeApp } from 'firebase/app';9import { getAuth, onAuthStateChanged, User, signOut } from 'firebase/auth';1011const app = initializeApp({12 // Your Firebase config13});1415interface AuthContextType {16 user: User | null;17 loading: boolean;18 logout: () => Promise<void>;19}2021const AuthContext = createContext<AuthContextType>({22 user: null,23 loading: true,24 logout: async () => {},25});2627export function AuthProvider({ children }: { children: ReactNode }) {28 const [user, setUser] = useState<User | null>(null);29 const [loading, setLoading] = useState(true);3031 useEffect(() => {32 const auth = getAuth(app);33 const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {34 setUser(firebaseUser);35 setLoading(false);36 });37 return unsubscribe;38 }, []);3940 const logout = async () => {41 const auth = getAuth(app);42 await signOut(auth);43 };4445 return (46 <AuthContext.Provider value={{ user, loading, logout }}>47 {children}48 </AuthContext.Provider>49 );50}5152export const useAuth = () => useContext(AuthContext);Common mistakes when detecting Auth State Changes in Firebase
Why it's a problem: Checking auth.currentUser directly on page load instead of using onAuthStateChanged
How to avoid: auth.currentUser is null until Firebase restores the session from local storage. Always use onAuthStateChanged to wait for the auth state to be determined before rendering auth-dependent UI.
Why it's a problem: Not returning the unsubscribe function in React useEffect, causing memory leaks
How to avoid: Return the unsubscribe function from useEffect: return unsubscribe. This ensures the listener is removed when the component unmounts.
Why it's a problem: Redirecting unauthenticated users before the loading state resolves
How to avoid: Always check the loading flag first. If loading is true, show a spinner instead of redirecting. The user may be authenticated but Firebase has not finished restoring the session yet.
Why it's a problem: Setting up multiple onAuthStateChanged listeners across different components
How to avoid: Set up one listener at the top level (in an AuthProvider) and share the state via React Context. Multiple listeners waste resources and can cause state synchronization bugs.
Best practices
- Use a single AuthProvider at the app root and share auth state via React Context
- Always handle the loading state before making auth-dependent decisions
- Return the unsubscribe function from useEffect to clean up the listener on unmount
- Use onAuthStateChanged for most apps and onIdTokenChanged only when you need token refresh events
- Never rely on auth.currentUser being non-null at page load — always wait for onAuthStateChanged
- Show a loading skeleton or spinner while the initial auth check is in progress
- Combine the auth state listener with Firestore security rules for defense in depth
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Show me how to detect Firebase Auth state changes using onAuthStateChanged in a React app. Include a full AuthProvider component with React Context, a useAuth hook, handling the initial loading state, and a ProtectedRoute component that redirects unauthenticated users.
Create a React AuthProvider using Firebase onAuthStateChanged. Include the context setup, useAuth hook, loading state handling, sign-out function, and a ProtectedRoute component. Use the modular Firebase SDK v9+ and TypeScript.
Frequently asked questions
When does onAuthStateChanged fire?
It fires once immediately when you set it up (with the current auth state), then again each time a user signs in, signs out, or the auth session is restored on page load.
What is the difference between onAuthStateChanged and onIdTokenChanged?
onAuthStateChanged fires only on sign-in and sign-out. onIdTokenChanged also fires when the ID token refreshes (approximately every hour) and when custom claims change. Use onAuthStateChanged unless you need token-level events.
Why is auth.currentUser null even though the user is signed in?
Firebase needs time to restore the auth session from local storage when the page loads. auth.currentUser is null until that process completes. Use onAuthStateChanged to wait for the auth state to be determined.
Do I need to call onAuthStateChanged on every page of my app?
No. Set it up once in a top-level provider component and share the user state via React Context (or your framework's equivalent state management). All child components can then access the auth state.
How do I get the user's ID token for backend API calls?
Inside the onAuthStateChanged callback, call user.getIdToken() to get a JWT. Send this token in the Authorization header of your API requests. The token refreshes automatically every hour.
Can I use onAuthStateChanged with Next.js server-side rendering?
onAuthStateChanged is a client-side API and does not work during server-side rendering. For SSR, use the Firebase Admin SDK to verify tokens sent via cookies or headers in your server components or API routes.
Can RapidDev help implement a complete auth system with Firebase?
Yes, RapidDev can build a full authentication flow including social providers, email/password, protected routes, role-based access control, and server-side session management with Firebase Auth.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation