Check if a user is logged in with Firebase using onAuthStateChanged(), which fires a callback whenever the auth state changes. The callback receives the user object when signed in or null when signed out. Avoid using auth.currentUser directly on page load because it is null until Firebase finishes initializing. Wrap your auth check in a Promise or use a React hook with useEffect to properly handle the async initialization.
Detecting Authentication State in Firebase with onAuthStateChanged
Firebase Auth is asynchronous — when your app loads, there is a brief period where Firebase checks if a user session exists. During this time, auth.currentUser is null even if the user is logged in. The correct pattern is onAuthStateChanged(), which fires once when auth initializes and again on every sign-in or sign-out. This tutorial shows the correct approach for vanilla JavaScript, React, and Next.js projects.
Prerequisites
- A Firebase project with Authentication enabled (any provider)
- Firebase SDK installed and initialized in your project
- The getAuth instance exported from your firebase.ts module
- At least one auth provider enabled (email/password, Google, etc.)
Step-by-step guide
Understand why auth.currentUser can be null on page load
Understand why auth.currentUser can be null on page load
When your app first loads, Firebase checks for an existing user session in the browser's IndexedDB or localStorage. This check is asynchronous and takes a moment to complete. During this time, auth.currentUser returns null even if the user has an active session. Using currentUser directly causes a common bug where authenticated users briefly see a logged-out state.
1import { auth } from "@/lib/firebase";23// BAD — currentUser is null during initialization4const user = auth.currentUser;5console.log(user); // null on page load, even if user is signed in!67// This causes the logged-out UI to flash before the user appearsExpected result: You understand that auth.currentUser is unreliable on initial load and should not be used for auth checks.
Use onAuthStateChanged to detect login state
Use onAuthStateChanged to detect login state
onAuthStateChanged registers a listener that fires immediately with the current auth state (once Firebase finishes initializing) and then fires again every time the user signs in or out. The callback receives a User object when signed in or null when signed out. This is the correct and recommended way to check authentication.
1import { auth } from "@/lib/firebase";2import { onAuthStateChanged } from "firebase/auth";34onAuthStateChanged(auth, (user) => {5 if (user) {6 // User is signed in7 console.log("Logged in as:", user.uid);8 console.log("Email:", user.email);9 console.log("Display name:", user.displayName);10 } else {11 // User is signed out12 console.log("Not logged in");13 }14});Expected result: The callback fires with the user object if signed in, or null if signed out.
Create a Promise-based auth check for one-time use
Create a Promise-based auth check for one-time use
Sometimes you need a one-time auth check (e.g., before making an API call) rather than a continuous listener. Wrap onAuthStateChanged in a Promise that resolves with the first auth state. This is useful in utility functions where you cannot use a listener pattern.
1import { auth } from "@/lib/firebase";2import { onAuthStateChanged, User } from "firebase/auth";34function getCurrentUser(): Promise<User | null> {5 return new Promise((resolve) => {6 const unsubscribe = onAuthStateChanged(auth, (user) => {7 unsubscribe(); // Stop listening after first result8 resolve(user);9 });10 });11}1213// Usage:14async function fetchProtectedData() {15 const user = await getCurrentUser();16 if (!user) {17 throw new Error("Not authenticated");18 }19 // Proceed with the authenticated user20 const token = await user.getIdToken();21 // Use token for API calls...22}Expected result: The getCurrentUser function resolves with the user object or null after Firebase initialization completes.
Build a React useAuth hook
Build a React useAuth hook
In React, create a custom hook that wraps onAuthStateChanged and returns the current user and loading state. Use useEffect to subscribe on mount and unsubscribe on unmount. The loading state is critical — it lets you show a loading spinner instead of flashing the wrong UI while Firebase initializes.
1// src/hooks/useAuth.ts2import { useState, useEffect } from "react";3import { auth } from "@/lib/firebase";4import { onAuthStateChanged, User } from "firebase/auth";56export function useAuth() {7 const [user, setUser] = useState<User | null>(null);8 const [loading, setLoading] = useState(true);910 useEffect(() => {11 const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {12 setUser(firebaseUser);13 setLoading(false);14 });1516 return () => unsubscribe(); // Cleanup on unmount17 }, []);1819 return { user, loading, isAuthenticated: !!user };20}2122// Usage in a component:23// const { user, loading, isAuthenticated } = useAuth();24// if (loading) return <Spinner />;25// if (!isAuthenticated) return <LoginPage />;Expected result: The useAuth hook provides user, loading, and isAuthenticated values that update in real-time.
Protect routes based on auth state
Protect routes based on auth state
Create a wrapper component that checks authentication before rendering protected content. Show a loading indicator while Firebase initializes, redirect to login if not authenticated, and render the protected content only for authenticated users.
1// src/components/ProtectedRoute.tsx2import { useAuth } from "@/hooks/useAuth";3import { Navigate } from "react-router-dom";45export function ProtectedRoute({6 children,7}: {8 children: React.ReactNode;9}) {10 const { user, loading } = useAuth();1112 if (loading) {13 return <div className="flex justify-center p-8">Loading...</div>;14 }1516 if (!user) {17 return <Navigate to="/login" replace />;18 }1920 return <>{children}</>;21}2223// Usage in your router:24// <Route path="/dashboard" element={25// <ProtectedRoute><Dashboard /></ProtectedRoute>26// } />Expected result: Protected routes show a loading state during auth initialization, redirect unauthenticated users to login, and render content for authenticated users.
Complete working example
1import { useState, useEffect, useCallback } from "react";2import { auth } from "@/lib/firebase";3import {4 onAuthStateChanged,5 signOut as firebaseSignOut,6 User,7} from "firebase/auth";89export function useAuth() {10 const [user, setUser] = useState<User | null>(null);11 const [loading, setLoading] = useState(true);1213 useEffect(() => {14 const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {15 setUser(firebaseUser);16 setLoading(false);17 });1819 return () => unsubscribe();20 }, []);2122 const signOut = useCallback(async () => {23 await firebaseSignOut(auth);24 }, []);2526 return {27 user,28 loading,29 isAuthenticated: !!user,30 signOut,31 };32}3334// One-time auth check for utility functions35export function getCurrentUser(): Promise<User | null> {36 return new Promise((resolve) => {37 const unsubscribe = onAuthStateChanged(auth, (user) => {38 unsubscribe();39 resolve(user);40 });41 });42}Common mistakes when checkking If a User Is Logged In with Firebase
Why it's a problem: Using auth.currentUser directly on page load to check if the user is logged in
How to avoid: auth.currentUser is null until Firebase finishes initializing, which can take a moment. Always use onAuthStateChanged() to wait for the auth state to resolve before making decisions.
Why it's a problem: Not showing a loading state while Firebase auth initializes, causing a flash of the login page for authenticated users
How to avoid: Track a loading boolean that starts as true and flips to false inside the onAuthStateChanged callback. Show a spinner or skeleton while loading is true.
Why it's a problem: Forgetting to unsubscribe from onAuthStateChanged when a React component unmounts
How to avoid: Store the unsubscribe function returned by onAuthStateChanged and call it in the useEffect cleanup: return () => unsubscribe().
Best practices
- Always use onAuthStateChanged instead of auth.currentUser for reliable auth state detection
- Track a loading state to prevent UI flashes while Firebase initializes the auth session
- Create a reusable useAuth hook to centralize auth state management in React apps
- Unsubscribe from auth listeners when components unmount to prevent memory leaks
- Use a ProtectedRoute component to declaratively guard authenticated-only pages
- For one-time auth checks in utility functions, wrap onAuthStateChanged in a Promise that resolves on first callback
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Show me how to check if a user is logged in with Firebase Auth in a React TypeScript app. I need a useAuth hook that uses onAuthStateChanged with proper loading state handling, and a ProtectedRoute component that redirects to login if not authenticated.
Create a Firebase Auth hook for React that uses onAuthStateChanged from firebase/auth. Return the current user, loading state, and isAuthenticated boolean. Include useEffect cleanup to unsubscribe. Also create a ProtectedRoute wrapper component that shows a loading spinner during auth initialization.
Frequently asked questions
Why is auth.currentUser null even though the user is signed in?
Firebase Auth is asynchronous. When your app loads, it takes a moment to check for an existing session in the browser. During this initialization period, currentUser is null. Use onAuthStateChanged to wait for the auth state to resolve.
How long does Firebase Auth take to initialize?
Typically 50-200 milliseconds on a fast connection. It reads the persisted session from IndexedDB or localStorage and validates the token. Always show a loading state during this period rather than assuming the user is not logged in.
Does onAuthStateChanged fire when the page first loads?
Yes. It fires once immediately after Firebase resolves the auth state (with a User object if signed in, or null if not), and then fires again on every subsequent sign-in or sign-out event.
How do I get the user's ID token for API calls?
Call user.getIdToken() on the User object from onAuthStateChanged. This returns a Promise that resolves with a JWT you can send to your backend. The token auto-refreshes when expired.
Can I check if a user's email is verified?
Yes. The User object has an emailVerified boolean property. Check user.emailVerified in your onAuthStateChanged callback or useAuth hook to conditionally allow or restrict access.
What happens to the auth listener when the browser tab is closed?
The listener is destroyed, but the user session persists in the browser's storage (IndexedDB by default). When the user opens the app again, onAuthStateChanged will fire with the previously signed-in user.
Should I use onAuthStateChanged or onIdTokenChanged?
Use onAuthStateChanged for most cases — it fires on sign-in and sign-out. Use onIdTokenChanged if you also need to react to ID token refreshes (which happen every hour), such as when syncing tokens to a backend.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation