Skip to main content
RapidDev - Software Development Agency
firebase-tutorial

How to Detect Auth State Changes in Firebase

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.

What you'll learn

  • How to set up onAuthStateChanged to listen for sign-in and sign-out events
  • How to properly unsubscribe from the listener to prevent memory leaks
  • How to build a React auth context that provides user state to your entire app
  • How to handle the initial loading state while Firebase restores the auth session
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read10-15 minFirebase JS SDK v9+, Firebase Auth (all plans)March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1import { getAuth, onAuthStateChanged, User } from 'firebase/auth';
2
3const auth = getAuth();
4
5const 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});
12
13// Call unsubscribe() when you no longer need the listener

Expected result: The callback fires immediately with the current user (or null), then fires again each time the user signs in or out.

2

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.

typescript
1import { getAuth, onAuthStateChanged, User } from 'firebase/auth';
2
3let currentUser: User | null = null;
4let isLoading = true;
5
6const auth = getAuth();
7
8onAuthStateChanged(auth, (user) => {
9 currentUser = user;
10 isLoading = false;
11 // Update your UI here
12 renderApp();
13});
14
15function renderApp() {
16 if (isLoading) {
17 // Show loading spinner
18 return;
19 }
20 if (currentUser) {
21 // Show authenticated UI
22 } else {
23 // Show login screen
24 }
25}

Expected result: The app shows a loading state until Firebase confirms whether a user session exists, then renders the appropriate UI.

3

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.

typescript
1import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
2import { getAuth, onAuthStateChanged, User } from 'firebase/auth';
3
4interface AuthContextType {
5 user: User | null;
6 loading: boolean;
7}
8
9const AuthContext = createContext<AuthContextType>({
10 user: null,
11 loading: true,
12});
13
14export function AuthProvider({ children }: { children: ReactNode }) {
15 const [user, setUser] = useState<User | null>(null);
16 const [loading, setLoading] = useState(true);
17
18 useEffect(() => {
19 const auth = getAuth();
20 const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
21 setUser(firebaseUser);
22 setLoading(false);
23 });
24 return unsubscribe;
25 }, []);
26
27 return (
28 <AuthContext.Provider value={{ user, loading }}>
29 {children}
30 </AuthContext.Provider>
31 );
32}
33
34export 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.

4

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.

typescript
1import { Navigate } from 'react-router-dom';
2import { useAuth } from './AuthProvider';
3
4export function ProtectedRoute({ children }: { children: ReactNode }) {
5 const { user, loading } = useAuth();
6
7 if (loading) {
8 return <div>Loading...</div>;
9 }
10
11 if (!user) {
12 return <Navigate to="/login" replace />;
13 }
14
15 return <>{children}</>;
16}
17
18// 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.

5

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.

typescript
1import { getAuth, onIdTokenChanged } from 'firebase/auth';
2
3const auth = getAuth();
4
5const unsubscribe = onIdTokenChanged(auth, async (user) => {
6 if (user) {
7 // Get the fresh token to send to your backend
8 const token = await user.getIdToken();
9 console.log('Token refreshed for:', user.uid);
10 // Send token to your API for session management
11 } else {
12 console.log('No user — clear session');
13 }
14});
15
16// Call unsubscribe() to stop listening

Expected 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

AuthProvider.tsx
1import {
2 createContext,
3 useContext,
4 useEffect,
5 useState,
6 ReactNode
7} from 'react';
8import { initializeApp } from 'firebase/app';
9import { getAuth, onAuthStateChanged, User, signOut } from 'firebase/auth';
10
11const app = initializeApp({
12 // Your Firebase config
13});
14
15interface AuthContextType {
16 user: User | null;
17 loading: boolean;
18 logout: () => Promise<void>;
19}
20
21const AuthContext = createContext<AuthContextType>({
22 user: null,
23 loading: true,
24 logout: async () => {},
25});
26
27export function AuthProvider({ children }: { children: ReactNode }) {
28 const [user, setUser] = useState<User | null>(null);
29 const [loading, setLoading] = useState(true);
30
31 useEffect(() => {
32 const auth = getAuth(app);
33 const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
34 setUser(firebaseUser);
35 setLoading(false);
36 });
37 return unsubscribe;
38 }, []);
39
40 const logout = async () => {
41 const auth = getAuth(app);
42 await signOut(auth);
43 };
44
45 return (
46 <AuthContext.Provider value={{ user, loading, logout }}>
47 {children}
48 </AuthContext.Provider>
49 );
50}
51
52export 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.

ChatGPT Prompt

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.

Firebase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.