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
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...
}