Search code examples
angulartypescriptobservablesubscription

Angular subscription either doesn't update the value or doesn't recognize value change


I'm fairly new to angular and currently building a web application using it. My issue lies in updating a value in one component after it's been changed in a different component with both components being unrelated to each other. To achieve this I'm trying to use a subscription which doesn't work.

My root component's html (app.component.html) looks like this:

<app-navbar></app-navbar>
<router-outlet></router-outlet>

Within the router outlet the user is automatically redirected to the login page (every page is a component) unless a user id and token are set and verified by the server. Once a user id and token are set and stored as cookies I want a log out button to be displayed in my navbar.

navbar.component.html:

<nav class="bg-dark border-bottom border-body" data-bs-theme="dark">
    <a routerLink="">Home</a>
    <button *ngIf="isAuthenticated" routerLink="login" class="logout">Log out</button>
</nav>

navbar.component.ts:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { AuthService } from '../auth.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-navbar',
  templateUrl: './navbar.component.html',
  styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent implements OnInit, OnDestroy {

  isAuthenticated: boolean = false
  authSubscription: Subscription

  constructor(private authService: AuthService) {
  }

  ngOnInit(): void {
    this.authSubscription = this.authService.isAuthenticated$.subscribe((isAuthenticated) =>
      {
        this.isAuthenticated = isAuthenticated
      }
    )
  }

  ngOnDestroy(): void {
    this.authSubscription.unsubscribe()
  }
}

I'm using auth.service.ts to authenticate the user:

import { Injectable } from '@angular/core';
import { CookieService } from 'ngx-cookie-service';
import { AUTHENTICATE } from './graphql/graphql.queries';
import { Apollo } from 'apollo-angular';
import { BehaviorSubject, Observable, firstValueFrom } from 'rxjs';

@Injectable({
  providedIn: "root",
})
export class AuthService {

  isAuthenticated = false;
  error: any

  private isAuthenticatedBS = new BehaviorSubject<boolean>(this.isAuthenticated)
  public isAuthenticated$ : Observable<boolean> = this.isAuthenticatedBS.asObservable()

  constructor(private cookieService: CookieService, private apollo: Apollo) {
    this.checkAuthentication()
  }

  async checkAuthentication(): Promise<boolean> {
    this.isAuthenticated = await this.authenticate()
    return this.isAuthenticated;
  }

  async authenticate()
  {
    if(this.cookieService.get("userId") && this.cookieService.get("token"))
    {
      const response = await firstValueFrom(this.apollo.query({
        query: AUTHENTICATE,
        variables: {
          userId: +this.cookieService.get("userId"),
        },
        fetchPolicy: 'network-only',
      }));

      if(<number>(<any>response.data).authenticate != null)
      {
        if(<number>(<any>response.data).authenticate.id > 0)
        {
          return true
        }
      }
      return false
    }
    else
    {
      return false
    }
  }
}

The issue is that the "isAuthenticated" property in navbar.component.ts never seems to be updated.

What I've tried and expected

What should be happening is once I have logged in with a correct email and password, i receive a token and store the token and the user id as cookies (this part definitely works) and am then redirected to the home page. Every time the user tries to navigate to a page that requires authentication, the auth.guard calls the auth.service's checkAuthentication method to check if a user with the given id and token exists. Upon login the log out button should appear in the navbar and stay until the user decides to log out and delete the cookies that store the user id and token.

I tried manually triggering a change detection and checked if the authentication is successful using console log. Upon chatgpt's recommendation I've installed the angular devtools expansion for chrome if you'd like any more details on the component's state changes.

Here is a small overview of what angular devtools shows me: app-navbar's properties as shown in angular devtools


Solution

  • In your AuthService, the boolean isAuthenticated is not connected to the isAuthenticatedBS. You initialize the BehaviourSubject with the value that isAuthenticated has (i.e. false, but changes to isAuthenticated do not trigger the subject. The Subject has its own internal value, you don't "connect" it to an existing variable in this way.

    Instead, if you want the BehaviourSubject to emit a new value, you need to call next(value) on it. So I would recommend to remove the isAuthenticated boolean from your AuthService, and call isAuthenticatedBS.next(await this.authenticate()) in your checkAuthentication method.

    Should you need the current value instead of an observable, you can just query isAuthenticatedBS.value