DEV Community

Cover image for Managing Firebase Auth State with React Context in Next.js
Terry W
Terry W

Posted on • Edited on

Managing Firebase Auth State with React Context in Next.js

Authentication state often needs to be available throughout an application. Navigation, profile settings, and authenticated UI may all need access to the currently signed-in user.

In this article, I’ll show how I use Firebase Authentication with React Context in a Next.js application to:

  • Make the current user available throughout the app
  • Wait for Firebase to restore the existing session
  • Handle authentication-based navigation
  • Read and update profile data in Firestore

Firebase Authentication handles session persistence. React Context simply makes the resolved authentication state available throughout the component tree.

This example uses Next.js App Router. The same Context approach can be used with Vite, but routing and provider placement may differ.

Creating the Auth Context

Create an auth context such as /context/AuthContext.tsx:

"use client";

import {
  createContext,
  useContext,
  useEffect,
  useState,
  type ReactNode,
} from "react";
import { onAuthStateChanged, type User } from "firebase/auth";
import { Loader2 } from "lucide-react";

import { auth } from "@/firebase/config";

type AuthContextValue = {
  user: User | null;
  loading: boolean;
};

const AuthContext = createContext<AuthContextValue | undefined>(undefined);

export function useAuth() {
  const context = useContext(AuthContext);

  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }

  return context;
}

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

  useEffect(() => {
    return onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });
  }, []);

  if (loading) {
    return (
      <div className="flex min-h-screen items-center justify-center">
        <Loader2
          className="animate-spin"
          size={24}
          aria-label="Checking authentication status"
        />
      </div>
    );
  }

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

onAuthStateChanged waits for Firebase to determine whether a session already exists. This prevents the application from treating the initial null value as a confirmed signed-out state.

The returned unsubscribe function also removes the listener when the provider unmounts.

Adding the Provider

Wrap your application with AuthProvider in the root layout:

import "./globals.css";

import type { Metadata } from "next";

import { AuthProvider } from "@/context/AuthContext";
import { ThemeProvider } from "@/components/ThemeProvider";
import Navbar from "@/components/Navbar";

export const metadata: Metadata = {
  title: "Firebase Auth with React Context",
  description: "Managing Firebase authentication state in Next.js",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <AuthProvider>
          <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
            <Navbar />
            {children}
          </ThemeProvider>
        </AuthProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

The suppressHydrationWarning property is commonly used with theme providers when server and client output may differ.

It suppresses the warning for an expected mismatch. It does not prevent hydration mismatches and is unrelated to Firebase Authentication.

Handling Auth-Based Navigation

Once Firebase has resolved the user state, a client component can redirect users based on their authentication status:

"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";

import { useAuth } from "@/context/AuthContext";
import PromptLibrary from "@/components/PromptLibrary";

export default function HomePage() {
  const { user } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!user) {
      router.replace("/auth");
      return;
    }

    if (!user.emailVerified) {
      router.replace("/verify-email");
    }
  }, [router, user]);

  if (!user || !user.emailVerified) {
    return null;
  }

  return (
    <main className="min-h-screen">
      <PromptLibrary />
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

There is no need for an arbitrary timeout. The provider has already waited for Firebase to resolve the initial session.

Using router.replace also prevents the restricted route from remaining in the browser history.

Client Redirects Are Not Security

This redirect controls what the user sees, but it does not secure the underlying data.

Firestore documents, API routes, Server Actions, and other protected operations must perform their own authentication and authorisation checks.

For Firestore, this means using Security Rules.

Managing Firestore Profiles

Firebase Authentication stores basic information such as a display name, photo URL, and email address.

Additional profile data can be stored in Firestore:

import {
  doc,
  getDoc,
  serverTimestamp,
  setDoc,
  type DocumentData,
} from "firebase/firestore";

import { db } from "@/firebase/config";

type UserProfileUpdate = {
  displayName?: string;
  photoURL?: string;
  email?: string;
};

export async function getUserProfile(
  userId: string
): Promise<DocumentData | null> {
  const snapshot = await getDoc(doc(db, "users", userId));

  return snapshot.exists() ? snapshot.data() : null;
}

export async function updateUserProfile(
  userId: string,
  data: UserProfileUpdate
) {
  await setDoc(
    doc(db, "users", userId),
    {
      ...data,
      updatedAt: serverTimestamp(),
    },
    { merge: true }
  );
}
Enter fullscreen mode Exit fullscreen mode

Using setDoc with { merge: true } creates the document if it does not exist or updates it without replacing unrelated fields.

It also avoids reading the document before every update.

serverTimestamp() is preferable to new Date() because it does not rely on the user’s device clock.

Updating a User Profile

You can update Firebase Authentication and Firestore from a profile form:

import { updateProfile, type User } from "firebase/auth";

async function saveProfile(
  user: User,
  displayName: string,
  photoURL: string
) {
  await updateProfile(user, {
    displayName,
    photoURL,
  });

  await updateUserProfile(user.uid, {
    displayName,
    photoURL,
    email: user.email ?? undefined,
  });
}
Enter fullscreen mode Exit fullscreen mode

These are two separate requests, so one could succeed while the other fails.

The code attempts to keep both profiles aligned, but it cannot guarantee complete consistency. Production applications should decide which system is the source of truth and handle partial failures appropriately.

Email Verification

Use user.emailVerified for navigation and user messaging.

If a user verifies their email in another tab, reload the Firebase user before checking again:

await user.reload();

if (user.emailVerified) {
  router.replace("/");
}
Enter fullscreen mode Exit fullscreen mode

Do not rely on a client-controlled Firestore field such as:

emailVerified: true
Enter fullscreen mode Exit fullscreen mode

for security decisions.

Firestore Security Rules can use Firebase’s trusted authentication claim instead:

request.auth.token.email_verified
Enter fullscreen mode Exit fullscreen mode

Basic Firestore Security Rules

A user profile should at least be restricted to its owner:

rules_version = "2";

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, create, update: if request.auth != null
        && request.auth.uid == userId;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Production rules may also need to restrict which fields users can update.

Conclusion

Firebase Authentication and React Context provide a straightforward way to share the current user across a Next.js application.

Firebase restores and maintains the session, while the context exposes that state to client components. From there, the application can handle navigation, authenticated UI, and profile forms without passing user data through multiple layers of props.

The important distinction is that client-side auth state controls the interface. Actual security must still be enforced through Firestore Security Rules and trusted server-side checks.

Top comments (2)

Collapse
 
ishmaelsunday profile image
Ishmael Sunday

Thanks for sharing @ttibbs I am saving this article

Collapse
 
ttibbs profile image
Terry W

No problem, I'm glad you found it useful. Let me know if you have any thoughts or questions when you revisit it.