Search code examples
angulartypescriptrxjsangularfire2

Angular - How to unify auth observable?


I have an Angular service that uses AngularFire's auth observable to listen to state changes of the user. When a user signs in, the app must fetch a user document from MongoDB. This data needs to be consumable by components, so I need to have another observable. The problem is, I'm not quite sure how to make it work.

Here is a snippet of my auth service.

import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { Router } from '@angular/router';
import firebase from 'firebase/app';
import { environment } from '../../environments/environment'
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { User } from '../interfaces/User.model'

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

  public redirectRoute = ''
  public loginError = '';
  public _mongoUser: Observable<User | null> = of(null);
  public mongoUser: User | null = null;

  constructor(public auth: AngularFireAuth, private router: Router, private http: HttpClient,) {

    this.auth.user.subscribe(async (user) => {
      console.log('auth changed', user)
      if (user) {

        let headers = {
          headers: new HttpHeaders()
            .set('idToken', await user.getIdToken())
        }

        this._mongoUser = this.http.post<User>(
          `${environment.apiUrl}/users/email/${user.email}`,
          { personal: { email: user.email } },
          headers
        )

        this._mongoUser.subscribe(val => {
          console.log('val', val)
          this.mongoUser = val
        })

      } else {

      }
    })
  }

}

The main question is, how should I initialize _mongoUser? I think using of... and then the httpClient method is not working the way I want it to.

I want to consume _mongoUser or mongoUser like this in other components. However, my code above does not work.

constructor() {
    this.authService._mongoUser.subscribe(val => {    
      if (val) {
        this.editForm.patchValue({ 'username': val.username })
      }

    })
 }

Solution

  • By reassigning to this._mongoUser, you are throwing away all of the subscriptions that were made before the reassignment.

    To avoid reassignment, you can use a Subject or BehaviorSubject. In this case, I think BehaviorSubject would be a better fit. It keeps track of the most recently emitted item and

    1. re-emits that item to new subscribers
    2. lets you synchronously access that item with the BehaviorSubject#getValue method

    Here is a version of your example that is modified to use BehaviorSubject.

    import { Injectable } from '@angular/core';
    import { AngularFireAuth } from '@angular/fire/auth';
    import { Router } from '@angular/router';
    import firebase from 'firebase/app';
    import { environment } from '../../environments/environment'
    import { HttpClient, HttpHeaders } from '@angular/common/http';
    import { Observable, of, BehaviorSubject } from 'rxjs';
    import { tap, filter, mergeMap } from 'rxjs/operators';
    import { User } from '../interfaces/User.model'
    
    @Injectable({
      providedIn: 'root'
    })
    export class AuthService {
    
      public redirectRoute = ''
      public loginError = '';
    
      // readonly so we never reassign this field
      public readonly mongoUser: BehaviorSubject<User | null> = new BehaviorSubject(null);
    
      constructor(public auth: AngularFireAuth, private router: Router, private http: HttpClient,) {
        this.auth.user.pipe(
          tap((user) => console.log('auth changed', user)),
          filter((user) => !!user),
          mergeMap(async (user) => ({ user, idToken: await user.getIdToken()})),
          mergeMap(({user, idToken}) => {
            let headers = {
              headers: new HttpHeaders()
                .set('idToken', idToken)
            }
    
            return this.http.post<User>(
              `${environment.apiUrl}/users/email/${user.email}`,
              { personal: { email: user.email } },
              headers
            )
          }),
        ).subscribe({
            next: (userFromApi) => this.mongoUser.next(userFromApi)
        });
      }
    
    }