Vue
Pinia
Firebase Authentication
Route Guard
currentUser

Vue Pinia Firebase Authentication Fetch currentUser before Route Guard

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In a Vue app that uses Firebase Authentication, currentUser is not reliably available the moment the page reloads. Firebase restores the session asynchronously, so a route guard that checks too early can redirect an authenticated user to the login page before the auth state has finished initializing.

Wait for Firebase Auth Exactly Once

The clean pattern is to let your Pinia auth store own the initialization promise. The store waits for the first onAuthStateChanged callback, saves the user, and marks itself ready so the rest of the app can await that single piece of state.

typescript
1import { defineStore } from "pinia"
2import { getAuth, onAuthStateChanged, type User } from "firebase/auth"
3
4type AuthState = {
5  user: User | null
6  ready: boolean
7  initPromise: Promise<void> | null
8}
9
10export const useAuthStore = defineStore("auth", {
11  state: (): AuthState => ({
12    user: null,
13    ready: false,
14    initPromise: null,
15  }),
16
17  actions: {
18    init() {
19      if (this.initPromise) {
20        return this.initPromise
21      }
22
23      this.initPromise = new Promise<void>((resolve) => {
24        const auth = getAuth()
25        const unsubscribe = onAuthStateChanged(auth, (user) => {
26          this.user = user
27          this.ready = true
28          unsubscribe()
29          resolve()
30        })
31      })
32
33      return this.initPromise
34    },
35  },
36})

This avoids the common mistake of subscribing in every guard call. You only wait for the initial auth restoration once, then read store state after that.

Await the Store Before the Route Check

With the store in place, the router guard becomes predictable. It waits for auth readiness and only then decides whether the route should continue.

typescript
1import { createRouter, createWebHistory } from "vue-router"
2import { useAuthStore } from "@/stores/auth"
3
4const router = createRouter({
5  history: createWebHistory(),
6  routes: [
7    { path: "/login", name: "login", component: () => import("@/views/LoginView.vue") },
8    { path: "/dashboard", name: "dashboard", component: () => import("@/views/DashboardView.vue"), meta: { requiresAuth: true } },
9  ],
10})
11
12router.beforeEach(async (to) => {
13  const authStore = useAuthStore()
14  await authStore.init()
15
16  if (to.meta.requiresAuth && !authStore.user) {
17    return { name: "login", query: { redirect: to.fullPath } }
18  }
19})
20
21export default router

This pattern removes flicker because the decision happens after Firebase has restored the previous session, not before.

Keep Long-Lived Auth State Separate From Startup Readiness

The first auth callback solves the startup problem, but your app may still need a persistent listener if the user can sign out, sign in on another tab, or refresh tokens while the app is open. A common pattern is to use one method for the one-time startup wait and another for a long-lived subscription that keeps the store synchronized.

That separation keeps the guard logic simple. The guard only cares about readiness and current user state, while the store remains responsible for ongoing auth events.

Avoid Reading currentUser Directly in the Guard

getAuth().currentUser can be null during initialization even when a valid session exists. Reading it directly in the guard often works on a warm app state and then fails on a hard refresh, which makes the bug seem random.

Treat the store as the source of truth after initialization. That gives you one consistent place to handle loading, user state, and any fallback redirect logic.

Common Pitfalls

  • Checking getAuth().currentUser before Firebase has finished restoring the session from storage.
  • Registering a new onAuthStateChanged listener on every route navigation and never cleaning it up.
  • Running the guard before Pinia is installed on the app instance, which makes the store unavailable in router code.
  • Redirecting every unauthenticated request to login without excluding the login route itself, which can create a redirect loop.
  • Mixing startup readiness and long-lived auth synchronization into one unclear method, which makes later maintenance harder.

Summary

  • Firebase auth restoration is asynchronous, so guards must wait for readiness.
  • Put the one-time initialization promise in the Pinia store.
  • Await that store method inside beforeEach before checking protected routes.
  • Use store state rather than reading currentUser directly during startup.
  • Separate initial auth loading from long-lived auth subscriptions for clearer code.

Course illustration
Course illustration

All Rights Reserved.