Search code examples
angularrxjsobservablebehaviorsubject

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.


Solution

  • 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);
        }
    }