angularfirebaseauthenticationfirebase-authenticationauth-guard

How can i prevent from Login component render when Firebase Authentication hasn't yet determined


The login component flashes even if the user is authenticated. I believe it is due to the initial state of the user$ observable from Firebase Auth.

When my application first loads, Firebase Authentication hasn't yet determined whether the user is logged in or not. This means that during the initial load, the user$ observable is null (indicating no user is authenticated), and until Firebase processes the user's authentication state, the guard might redirect to the login page.

authGuard.ts

import { CanActivateFn } from '@angular/router';
import { AuthService } from './auth';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { map } from 'rxjs/operators';


export const AuthGuard: CanActivateFn = (route, state) => {
  const firebaseAuth = inject(AuthService);
  const router = inject(Router);
  
  return firebaseAuth.user$.pipe(
    map(user => {
      if (user) {
        return true; // Allow access if user is authenticated
      } else {

        router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); // Redirect to login if not authenticated
        return false;
      }
    })
  );
};

auth.ts

import { Injectable, inject, signal } from '@angular/core';
import {
  Auth,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  user,
  signOut,
  UserCredential 
} from '@angular/fire/auth';
import { Observable, from, BehaviorSubject,switchMap } from 'rxjs';
import { IUser } from '../interfaces/user';
import { Router } from '@angular/router';
import { UserService } from './user.service';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  router = inject(Router);
  user$: Observable<IUser | null>;
  currentUserSig = signal<IUser | null | undefined>(undefined);
  isLoggedInGuard: boolean = false;
  private loadingSubject = new BehaviorSubject<boolean>(true);  // Loading state
  loading$ = this.loadingSubject.asObservable();

  userService = inject(UserService);


 constructor(private firebaseAuth: Auth) {
  this.user$ = user(this.firebaseAuth) as Observable<IUser | null>;

  // Listen for user state changes and update loading state
  this.user$.subscribe((user) => {
    console.log(user);
    if (user) {
      this.currentUserSig.set({ email: user.email, uid: user.uid });
    } else {
      this.currentUserSig.set(null);
    }
    this.loadingSubject.next(false);  // Authentication state resolved, stop loading
  });
}

  register(email: string, password: string, nome: string, apelido: string): Observable<void> {
    // Create the user with email and password
    return from(createUserWithEmailAndPassword(this.firebaseAuth, email, password)).pipe(
      // Use switchMap to chain the Firebase user creation to adding user data to Firestore
      switchMap((ref: UserCredential) => {
        // Prepare user data object
        const userData = {
          email: email,
          nome: nome,
          apelido: apelido,
          uid: ref.user.uid,  // Access uid here correctly
        };

        // Call addUser in userService and return its observable
        return this.userService.addUser(userData);
      })
    );
  }

  login(email: string, password: string): Observable<void> {
    const promise = signInWithEmailAndPassword(
      this.firebaseAuth,
      email,
      password
    ).then(() => {});
    return from(promise);
  }

  logout(): Observable<void> {
    const promise = signOut(this.firebaseAuth).then(() => {
      this.router.navigate(['/login']);
      this.isLoggedInGuard = false;
    });
    return from(promise);
  }

}

app.component.ts

import { Component,inject,signal } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { OnInit } from '@angular/core';
import { AuthService } from './services/auth';
import { IUser } from './interfaces/user';
import { LoadingComponent } from './layouts/loading/loading.component';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet,LoadingComponent,CommonModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent implements OnInit {
  authService = inject(AuthService);
  router = inject(Router);
  loading:boolean = true;

  ngOnInit(): void{
    this.authService.loading$.subscribe((isLoading) => {
      this.loading = isLoading;
    });
  }
}

app.component.html

@if(loading){
  <app-loading></app-loading>
}
@else{
  <router-outlet></router-outlet>
} 

app.routes.ts

import { Routes } from '@angular/router';
import { LoginComponent } from './auth/login/login.component';
import { RegisterComponent } from './auth/register/register.component';
import { ForgotPasswordComponent } from './auth/forgot-password/forgot-password.component';
import { HomeComponent } from './layouts/home/home.component';
import { AuthGuard } from './services/authGuard';
import { NotFoundComponent } from './layouts/not-found/not-found.component';

export const routes: Routes = [
    { path: '', redirectTo: 'home', pathMatch: 'full' }, // Redirect root to 'home' or another default
    { path: 'login', component: LoginComponent },
    { path: 'register', component: RegisterComponent },
    { path: 'forgot-password', component: ForgotPasswordComponent },
    { path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
    { path: '**', component: NotFoundComponent } // Wildcard route for 404 page
];

I tried to create a Loading component so when user is undefined it renders the loading component if not renders the app normally. The problem it is flashing the login and loading component at the same time due to this router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); in authGuard.ts


Solution

  • This happens due to SSR rendering the login screen on the server. The server returns the login page, but when the guard runs it redirects to the other page, this gives you the flashing bug, to solve this try the below approach.


    You can solve this by using @defer to render the login HTML which displays the HTML only on the browser. Additionally add the @placeholder to display a loader while on the server to get rid of this flashing.

    @defer {
      <!-- Place your login HTML here -->
    } @placeholder {
      Loading...
    }