# How to Detect Auth State Changes in Firebase

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 10-15 min
- Compatibility: Firebase JS SDK v9+, Firebase Auth (all plans)
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
import { getAuth, onAuthStateChanged, User } from 'firebase/auth';

const auth = getAuth();

const unsubscribe = onAuthStateChanged(auth, (user: User | null) => {
  if (user) {
    console.log('User signed in:', user.uid, user.email);
  } else {
    console.log('User signed out');
  }
});

// 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.

```
import { getAuth, onAuthStateChanged, User } from 'firebase/auth';

let currentUser: User | null = null;
let isLoading = true;

const auth = getAuth();

onAuthStateChanged(auth, (user) => {
  currentUser = user;
  isLoading = false;
  // Update your UI here
  renderApp();
});

function renderApp() {
  if (isLoading) {
    // Show loading spinner
    return;
  }
  if (currentUser) {
    // Show authenticated UI
  } else {
    // Show login screen
  }
}
```

**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.

```
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { getAuth, onAuthStateChanged, User } from 'firebase/auth';

interface AuthContextType {
  user: User | null;
  loading: boolean;
}

const AuthContext = createContext<AuthContextType>({
  user: null,
  loading: true,
});

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const auth = getAuth();
    const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
      setUser(firebaseUser);
      setLoading(false);
    });
    return unsubscribe;
  }, []);

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

export 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.

```
import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthProvider';

export function ProtectedRoute({ children }: { children: ReactNode }) {
  const { user, loading } = useAuth();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  return <>{children}</>;
}

// Usage in your router:
// <Route path="/dashboard" element={
//   <ProtectedRoute><Dashboard /></ProtectedRoute>
// } />
```

**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.

```
import { getAuth, onIdTokenChanged } from 'firebase/auth';

const auth = getAuth();

const unsubscribe = onIdTokenChanged(auth, async (user) => {
  if (user) {
    // Get the fresh token to send to your backend
    const token = await user.getIdToken();
    console.log('Token refreshed for:', user.uid);
    // Send token to your API for session management
  } else {
    console.log('No user — clear session');
  }
});

// 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 code example

File: `AuthProvider.tsx`

```typescript
import {
  createContext,
  useContext,
  useEffect,
  useState,
  ReactNode
} from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged, User, signOut } from 'firebase/auth';

const app = initializeApp({
  // Your Firebase config
});

interface AuthContextType {
  user: User | null;
  loading: boolean;
  logout: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType>({
  user: null,
  loading: true,
  logout: async () => {},
});

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const auth = getAuth(app);
    const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
      setUser(firebaseUser);
      setLoading(false);
    });
    return unsubscribe;
  }, []);

  const logout = async () => {
    const auth = getAuth(app);
    await signOut(auth);
  };

  return (
    <AuthContext.Provider value={{ user, loading, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);
```

## Common mistakes

- **Checking auth.currentUser directly on page load instead of using onAuthStateChanged** — undefined Fix: 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.
- **Not returning the unsubscribe function in React useEffect, causing memory leaks** — undefined Fix: Return the unsubscribe function from useEffect: return unsubscribe. This ensures the listener is removed when the component unmounts.
- **Redirecting unauthenticated users before the loading state resolves** — undefined Fix: 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.
- **Setting up multiple onAuthStateChanged listeners across different components** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-detect-auth-state-changes-in-firebase
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-detect-auth-state-changes-in-firebase
