Combining Auth Observables in Angular: A Complete Guide

Currently, I'm working on an Angular service that leverages AngularFire's auth observable to monitor user state changes. Upon a user signing in, the application should retrieve a user document from MongoDB. To enable components to consume this data, I need to establish another observable. However, I'm encountering difficulties figuring out how to achieve this.

Displayed below 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 {

      }
    })
  }

}

My main dilemma revolves around the initialization of _mongoUser. The current method using 'of' and the httpClient approach is not yielding the desired outcome.

My objective is to utilize _mongoUser or mongoUser in other components, however, the existing code does not suffice. Here's an example:

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

    })
 }

Answer №1

When you assign a new value to this._mongoUser, you are essentially discarding all previous subscriptions.

To prevent this, it's recommended to utilize a Subject or BehaviorSubject. A BehaviorSubject seems more suitable in this scenario as it retains the latest emitted item and

  1. re-emits that item to new subscribers
  2. allows synchronous access to the item through the BehaviorSubject#getValue method

Below is an adjusted version of your code snippet implementing 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 = '';

  // Declared as readonly to prevent reassignment
  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)
    });
  }

}

Answer №2

Object Reassignment in JavaScript

When a reference to an object is re-assigned in JavaScript, the original reference remains unchanged.

let exampleObject = {Hello: "World"};
const ref = exampleObject;
exampleObject = {New: "Value"};

console.log("ref: ", ref); // ref: {"Hello":"World"}
console.log("eo : ", exampleObject); // eo : {"New":"Value"}

Therefore, avoid re-assigning public _mongoUser as it may result in other services holding outdated references.

Instead, consider using a BehaviorSubject:

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

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

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

    this.auth.user.pipe(
      map(user => user.getIdToken().pipe(
        map(idToken => ({user, idToken}))
      ))
    ).subscribe(({user, idToken}) => {
      console.log('auth changed', user)
      if (user) {

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

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

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

      } else {

      }
    })
  }

}

Streamlining the Code

The above changes are essential to get started, but you can further simplify your code using RxJS in a few areas:

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

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

  constructor(public auth: AngularFireAuth, private router: Router, private http: HttpClient,) {
    
    this._mongoUser.subscribe(val => {
      console.log('val', val)
      this.mongoUser = val
    });

    this.auth.user.pipe(
      filter(user => user != null),
      map(user => user.getIdToken().pipe(
        map(idToken => ({
          user, 
          headers: { 
            headers: new HttpHeaders().set('idToken', idToken) 
          }
        }))
      )),
      concatMap(({user, headers}) => this.http.post<User>(
        `${environment.apiUrl}/users/email/${user.email}`,
        { personal: { email: user.email } },
        headers
      ))
    ).subscribe(
      this._mongoUser.next.bind(this._mongoUser)
    );
  }
  
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the method to merge min and max validation errors when using React Hook Form?

<input {...register("subject", { maxLength: 50, minLength: 2, required: true, })} disabled={isLoading} id="subject" autoComplete=&q ...

Using a dictionary of class types as input and returning a dictionary of their corresponding instances

Is there a way to create a function that takes a dictionary with class values as an argument and returns a dictionary of their instances? For example: classes C1, C2 function f: ({ name1: C1, name2: C2 }): ({ name1: new C1() name2: new C2 ...

Steps for activating Brotli compression in Nginx and Docker for an Angular Application

For my Angular application using Docker, I have implemented Brotli compression on Nginx by adding commands to the Dockerfile and enabling brotli in the nginx/default.conf file. nginx/default.conf: server { brotli on; brotli_static on; listen ...

How to Determine the Requirement Status of Input Variables in an Angular 2 Directive?

Is it possible to specify an input variable in a directive as required or optionally as non-required? In the example below, we have set a default value of false. However, if I fail to declare it in the parent component template, ng2 AoT throws an error: ...

Having trouble grasping the concept of Interfaces and dealing with FormGroup problems in Angular?

Apologies if my question is a duplicate, I have found several solutions for the same issue on Stack Overflow, but unfortunately, I struggle to understand them in technical terms. Problem 1 src/app/models/dataModel.ts:2:5 2 id: number; ~~ The exp ...

What causes @typescript-eslint to retain old types/files in its cache and prevent successful compilation?

When I kick off my Typescript application using tsc -b -w, I always encounter an issue with @typescript-eslint not reacting to file changes accurately. It flags invalid types/syntax errors where there are none. Restarting the process sometimes doesn't ...

Attempting to run the command "npx typescript --init" resulted in an error message stating "npm ERR! could not determine executable to run."

What could be the reason behind the error message npm ERR! could not determine executable to run? Currently, I am attempting to set up a basic Node.js application using TypeScript and Yarn. Yarn is a tool that I am not very familiar with. These are the c ...

Scanning for devices on Ionic 2/3 made simple: How to easily exclude unwanted application and Android directories

I'm currently working on a gallery application that enables users to choose images from their phone and transfer them to a kiosk. Upon loading the application, it searches the entire device for folders containing images and organizes them into an albu ...

Issue with Angular 8: click event is not triggering when using ngFor directive to iterate through arrays of objects

Update: The original post has been modified to omit implementation details and complexity. I am facing an issue with an ngFor loop that invokes a method on a service. The method returns an array which is then iterated over by the for loop. The click even ...

A guide on integrating the URI.js library into an Angular2+ application

I'm currently exploring ways to integrate a third-party library called urijs into my Angular 2+ application. Below, you can see the relevant files involved in this process. // package.json { ... "dependencies": { ... "urijs": "^1.18.10", ...

What are the steps for encountering a duplicate property error in TypeScript?

I'm currently working with typescript version 4.9.5 and I am interested in using an enum as keys for an object. Here is an example: enum TestEnum { value1 = 'value1', value2 = 'value2', } const variable: {[key in TestEnum]: nu ...

Updating *ngIf in Angular 6 after the Component has finished Loading

Component: import { Component, OnInit } from '@angular/core'; // Services import { AuthService } from './services/auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleU ...

Ways to manipulate the placement of angular material 2 sidenav content to the bottom

I have been experimenting with various methods in CSS to override the existing side nav component in Angular Material 2. mat-sidenav-container { background: white; bottom: 0px !important; /* <---- no success */ } mat-sidenav { width: 300px; ...

Learn the process of transferring product details to a new component in Angular when a product is clicked from the product list component

Currently, my focus is on creating a shopping application using Angular and Django. I managed to get the products from the Django API into the angular products list component. However, I'm looking to enhance the user experience by allowing them to cl ...

Retrieve the attributes associated with a feature layer to display in a Pop-up Template using ArcGIS Javascript

Is there a way to retrieve all attributes (fields) from a feature layer for a PopupTemplate without explicitly listing them in the fieldInfos object when coding in Angular? .ts const template = { title: "{NAME} in {COUNTY}", cont ...

Best Practices for Variable Initialization in Stencil.js

Having just started working with Stencil, I find myself curious about the best practice for initializing variables. In my assessment, there seem to be three potential approaches: 1) @State() private page: Boolean = true; 2) constructor() { this.p ...

What is the proper method for transferring a JWT token to an external URL?

I currently have two REST endpoints set up: accounts.mydomain.com/login - This is an identity provider that sends a JWT token as a response once a user is authenticated with their username and password. api.mydomain.com/users - This endpoint accepts the ...

Warning: Node encountering unexpected Unhandled Promise Rejection ERROR

I've encountered a problem in my code that is triggering an UnhandledPromiseRejectionWarning in Node, but I'm struggling to understand the root cause. Here's a simplified version of the code: export class Hello { async good(): Promise<s ...

Guide to creating a personalized pipe that switches out periods for commas

I currently have a number with decimal points like --> 1.33 My goal is to convert this value so that instead of a dot, a comma is displayed. Initially, I attempted this using a custom pipe but unfortunately, it did not yield the desired result. {{get ...

How to dynamically incorporate methods into a TypeScript class

I'm currently attempting to dynamically inject a method into an external class in TypeScript, but I'm encountering the following error. Error TS2339: Property 'modifyLogger' does not exist on type 'extclass'. Here's the ...