Firebase Auth State Management

Auth state management answers one critical question every page of your app needs: is there a logged-in user right now? Firebase provides a real-time listener that monitors login status and updates your app automatically whenever a user logs in or out.

The Problem Without Auth State Management

Without proper auth state management, apps face a common bug: the page loads, auth.currentUser returns null (because Firebase hasn't finished restoring the session from storage yet), the app shows the login screen, then Firebase finishes restoring the session and the user is actually logged in — causing a flash of wrong content.

Think of it like checking your mailbox the moment you wake up. You walk downstairs half-asleep, open the box, and see nothing. You assume no mail came. But the delivery person hasn't arrived yet — they come 30 seconds later. You checked too early.

The onAuthStateChanged listener solves this. Instead of checking once at startup, it watches the mailbox continuously and tells you the moment mail arrives or leaves.

The onAuthStateChanged Listener

import { onAuthStateChanged } from "firebase/auth";
import { auth } from "./firebase";

onAuthStateChanged(auth, (user) => {
  if (user) {
    // A user is logged in
    console.log("User is signed in:", user.email);
    console.log("UID:", user.uid);
    showDashboard();
  } else {
    // No user is logged in
    console.log("No user signed in");
    showLoginPage();
  }
});

Firebase calls this callback function every time the auth state changes:

  • When the page first loads (to restore a previous session)
  • When a user successfully logs in
  • When a user logs out
  • When a session expires or is revoked

Auth State Flow Diagram

App starts
    |
    v
Firebase checks local storage for saved session
    |
    +---> Session found --> onAuthStateChanged fires with User object
    |
    +---> No session ---> onAuthStateChanged fires with null

User logs in
    |
    v
Firebase saves session to local storage
    |
    v
onAuthStateChanged fires with User object

User logs out
    |
    v
Firebase clears session from local storage
    |
    v
onAuthStateChanged fires with null

Unsubscribing from the Listener

onAuthStateChanged returns an unsubscribe function. Call it when your component or page no longer needs to track auth state — for example, when a page unloads. This prevents memory leaks.

// Store the unsubscribe function
const unsubscribe = onAuthStateChanged(auth, (user) => {
  if (user) {
    console.log("Signed in:", user.email);
  } else {
    console.log("Signed out");
  }
});

// Later, when done:
unsubscribe(); // stops the listener

In React apps, call unsubscribe inside the useEffect cleanup function so the listener stops when the component unmounts.

Auth State in React

Managing auth state in React typically involves a context provider that shares the current user across all components:

import React, { createContext, useContext, useEffect, useState } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "./firebase";

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(undefined); // undefined = still loading

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser); // null if signed out, User object if signed in
    });
    return unsubscribe; // cleanup on unmount
  }, []);

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

export function useAuth() {
  return useContext(AuthContext);
}

Using the Auth Hook in Components

import { useAuth } from "./AuthContext";

function ProfilePage() {
  const { user } = useAuth();

  if (user === undefined) return <p>Loading...</p>;
  if (!user) return <p>Please log in.</p>;

  return <p>Welcome, {user.displayName || user.email}!</p>;
}

The three states of user are important:

  • undefined — Firebase hasn't checked yet (show a loading spinner)
  • null — no user is signed in (show login page)
  • User object — user is signed in (show app content)

Protecting Routes

Auth state management powers route protection — preventing logged-out users from accessing private pages. Here is a simple protected route pattern:

function PrivateRoute({ children }) {
  const { user } = useAuth();

  if (user === undefined) return <p>Loading...</p>;
  if (!user) {
    // Redirect to login page
    window.location.href = "/login";
    return null;
  }

  return children;
}

// Usage:
<PrivateRoute>
  <DashboardPage />
</PrivateRoute>

Persistence: Controlling How Long Sessions Last

Firebase stores auth sessions in the browser's local storage by default, so users stay logged in across page refreshes and browser restarts. You can change this behavior:

import { browserLocalPersistence, browserSessionPersistence,
         inMemoryPersistence, setPersistence } from "firebase/auth";
import { auth } from "./firebase";

// Options:
// browserLocalPersistence — stays logged in until explicitly signed out
// browserSessionPersistence — logged out when browser tab closes
// inMemoryPersistence — logged out on page refresh

await setPersistence(auth, browserSessionPersistence);
// Now sign in...

Use browserSessionPersistence for apps handling sensitive data like banking, where longer sessions pose a security risk.

Key Takeaway

Always use onAuthStateChanged instead of checking auth.currentUser directly at startup. The listener fires with the correct user state once Firebase finishes restoring the session. Store the returned unsubscribe function and call it when you no longer need the listener. In React, wrap your app in an auth context provider so every component can access the current user without prop drilling.

Leave a Comment

Your email address will not be published. Required fields are marked *