Damon
Damon

Reputation: 4524

How can I read an observable without page refresh (declarative pattern)?

I am trying to set/read if a user is authenticated in my application. I have things working, but as I am learning more about RxJs, I am trying to refactor things to be more declarative.

My code samples below are 98% working. When I log in, I am seeing my logging statement in my service as expected with the correct value(s):

console.log('setting auth status...', status); // true

However, I'm not seeing the logging in my component:

console.log('reading isAuthenticated$ status... ', response) <-- not seeing this when logging in

If I refresh my page, I am seeing Hello World as expected. Since I am subscribing using the async pipe, I thought I would see the ui update without having to refresh.

If I log out, (click a button). I am seeing all logging statements as expected every time--even without refreshing.

console.log('setting auth status...', status); // false

console.log('reading isAuthenticated$ status... ', response) // false

This makes me think there is something wrong how I am emitting or reading isAuthenticated$ observable.

How can I read the isAuthenticated$ observable in my template without refreshing?

Here is my service:

// auth.service.ts

export class AuthService {
    private readonly baseURL = environment.baseURL;
    private isAuthenticatedSubject = new BehaviorSubject<boolean>(
        this.hasAccessToken()
    );

    isAuthenticated$ = this.isAuthenticatedSubject.asObservable();

    ...

    signInWithEmailPassword(
        emailPasswordCredentials: EmailPasswordCredentials
    ) {
        return this.httpClient
            .post<Response>(
                `${this.baseURL}/v1/auth/signin`,
                emailPasswordCredentials
            )
            .pipe(
                map((authResponse) => {
                    // set auth cookie
                    this.setAuthenticatedStatus(true);
                })
                catchError((err) => {
                    return throwError(err);
                })
            );
    }


    ...

    setAuthenticatedStatus(status: boolean): void {
        console.log('setting auth status...', status);
        this.isAuthenticatedSubject.next(status); // true|false
    }

    signOut(): void {
        this.router.navigate(['/signin']).then(() => {
            this.setAuthenticatedStatus(false);
        });
    }
}

Here is what my component looks like:

// app.component.ts

isAuthenticated$ = this.authService.isAuthenticated$.pipe(
    tap((response) =>
        console.log('reading isAuthenticated$ status... ', response)
    ),
    catchError((err) => {
        this.message = err;
        return EMPTY;
    })
);

Finally, here is my template:

<div *ngIf="isAuthenticated$ | async">Hello World!</div>

EDIT

I have a login.component that handles the email/password button click. It looks like this:

signInWithEmailPassword(form: FormGroup): void {
    if (form.invalid) {
        return;
    }

    ...
    this.authService.signInWithEmailPassword(this.form.value).subscribe({
            next: (response) => {
                this.router.navigate(['/dashboard']).then(() => {
                    // this.authService.setAuthenticatedStatus(true);  // This doesn't seem to impact anything
                });     
            },
            error: (err) => {
                this.handleHttpError(err);
            },
            complete: () => {
                //...
            },
        });
    }

I thought that by setting that in my login.component I could update that subject. It all works after I refresh the page. The nav shows...but if I just login... it's like I'm not "watching" on my app.component that observable or something.

EDIT/UPDATE

I have tried to set up a stackblitz to help illustrate what I'm running into. I am not sure how to mock the auth part to return a dummy response, but hopefully this will help.

Upvotes: 2

Views: 503

Answers (2)

Damon
Damon

Reputation: 4524

First and foremost I want to thank everyone for their suggestions & answers. They really helped me think through things to find what I was doing wrong.

In the end, I created a stripped-down Stackblitz of what I was trying to do. It was interesting that everything seemed to work as expected, and after several days of chasing, I decided that there was something somewhere in my application that was causing my trouble.

I started a new application, and everything now is working as expected.

Here is what my application looks like, hopefully this will help someone else...

Here is the login.component:

// login.component.ts

public signInWithEmailPassword(form: FormGroup): void {
    if (form.invalid) {
        return;
    }

        this.authService.signInWithEmailPassword(this.form.value).subscribe({
            next: (response: AuthResponse) => {
                this.router.navigate(['/dashboard']).then(() => {
                        //...
                    });
            },
            error: (err) => {
                console.log('err: ', err);
            },
            complete: () => {
                //...
            },
        });
    }

Here is my app.component

// app.component.ts

export class AppComponent {
    message: string | undefined;

    isAuthenticated$ = this.authService.isAuthenticated$.pipe(
        catchError((err) => (this.message = err))
    );

    constructor(
        private readonly authService: AuthService
    ) {
        //...
    }
}

Here is what my template looks like:

// app.template

<div>
    <app-desktop-sidebar *ngIf="isAuthenticated$ | async"></app-desktop-sidebar>

    <div>
        <router-outlet></router-outlet>
    </div>
</div>

Here is my auth.service:

// auth.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { environment } from '../../environments/environment';

import { catchError, Observable, throwError, tap, BehaviorSubject } from 'rxjs';

import { EmailPasswordCredentials } from './models/email-password-credentials.model';
import { AuthResponse } from './models/auth-response.interface';

@Injectable({
    providedIn: 'root',
})
export class AuthService {
    private readonly baseURL = environment.baseURL;
    private isLoadingSubject = new BehaviorSubject<boolean>(false);
    private isAuthenticatedSubject = new BehaviorSubject<boolean>(
        this.hasAccessToken()
    );

    public isLoading$ = this.isLoadingSubject.asObservable();
    public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();

    constructor(
        private readonly httpClient: HttpClient,
    ) {
        //...
    }

    public signInWithEmailPassword(
        emailPasswordCredentials: EmailPasswordCredentials
    ): Observable<AuthResponse> {
        this.setLoadingStatus(true);

        return this.httpClient
            .post<AuthResponse>(
                `${this.baseURL}/v1/auth/signin`,
                emailPasswordCredentials
            )
            .pipe(
                tap((response) => {
                    ...
                    this.setLoadingStatus(false);
                    this.isAuthenticatedSubject.next(true);
                }),
                catchError((err) => {
                    this.setLoadingStatus(false);
                    return throwError(err);
                })
            );
    }

    public signOut() {
        this.isAuthenticatedSubject.next(false);
    }
}

Upvotes: 0

Eliseo
Eliseo

Reputation: 58019

I imagine your problem is that you has a router like

  {path:'login',component:LoginComponent},
  {path:'',component: HomeComponent,children:[
    {path:'dashboard',component:DashboardComponent}
    {path:'other',component:OtherComponent}
  ]}

and your HomeComponent like

<app-header></app-header>
<router-outlet></router-outlet>

So, when you login, the authservice emit a value but the "header" it's not, so can not listen the new value

The solution is use a BehaviourSubject, not a Subject

export class LoginService {
  isAuthenticatedSubject: BehaviorSubject<boolean> = new BehaviorSubject(false);
  isAuthenticated$ = this.isAuthenticatedSubject as Observable<boolean>;

  constructor() {}
  login(status: boolean) {
    this.isAuthenticatedSubject.next(status);
  }
}

See a little stackblitz the "dashboard" component has "login" component

Upvotes: 0

Related Questions