'Angular 7 - Use BehaviorSubject for Publishing API Results to All Components

I have an API call that returns data about the current user (i.e., username, full name, authorization group memberships, email address, etc.). I would like to only call this API once per user session and have its data shared among all components.

I don't want to rely upon localStorage, since I don't want this data to be modifiable by the user. I am trying to use a BehaviorSubject and have tried following a number of examples, but I keep receiving the initial value (null) when trying to access it via my components.

// HTTP Service:
getUserData() {
  const url = `${baseUrl}/api/users/`;
  return this.httpClient.get<UserData>(url).toPromise();
}
// Utils Service
export class UtilsService {
  private userDataSubject: BehaviorSubject<UserData>;

  constructor(
    private httpService: HttpService,
  ) {
    this.userDataSubject = new BehaviorSubject<UserData>(null);
    this.setUserData();
  }

  async setUserData() {
    let userData: UserData = await this.httpService.getUserData();
    this.userDataSubject.next(userData);
    console.log(this.userDataSubject.value); // Properly returns the data I want     
  }

  public get userDataValue(): UserData {
    return this.userDataSubject.value;
  }
// Component
  ngOnInit() {
    this.userData = this.utils.userDataValue; // This is the variable I need to set in my component.
    console.log(this.userData);               // Returns null
    console.log(this.utils.userDataValue);    // Also returns null
  }

I have also tried using an async function for userDataValue():

async userDataValue(): Promise<UserData> {
    return this.userDataSubject.value;
  }

And modified my component:

async ngOnInit() {
    this.userData = await this.utils.userDataValue();
    console.log(this.userData);                    // Still returns null
    console.log(await this.utils.userDataValue()); // Still returns null
  }

So far, I can only make this work if I avoid using a BehaviorSubject and call the API in each component, but that seems like it shouldn't be necessary. I'd greatly appreciate any guidance in how I can make this work. Thanks.



Solution 1:[1]

Here is a little improvement of alexortizl's answer:

Instead of using the BehaviorSubject<> you should use the ReplaySubject<> of size 1. Why?
A BehaviorSubject must be initialized with a value. In your code, it is a null. That means: All subscribers must handle the null (like in alexortizl's answer, the if(date === null)-line).

On the other hand, a ReplaySubject of the size one will act like the BehaviorSubject, but doesn't have initial value. All subscribers will not get any value until the first correct value was emitted. Read this for a deeper dive -> Subject vs BehaviorSubject vs ReplaySubject in Angular

So, you code will look like this:

Service

export class UtilsService {
  // we set the type to "Subject" to hide the actual ReplaySubject 
  public userDataSubject$: Subject<UserData> = new ReplaySubject(1);

  constructor(private httpService: HttpService) {
    this.setUserData();
  }

  async setUserData() {
    let userData: UserData = await this.httpService.getUserData();
    this.userDataSubject$.next(userData);     
  }

Component.ts

ngOnInit() {
  this.utils.userDataSubject$.subscribe((data) => {
    console.log(data);
     // component logic to use your data
     // this will be called every time userDataSubject receives a new value
  });
}

Component.html

... or even better with the async-pipe. Then you don't have to unsubscribe and it can improve the Change Detection of Angular. Deep dive -> “SEEING” ANGULAR CHANGE DETECTION IN ACTION

<ng-container *ngIf="utils.userDataSubject$ | async as userData">
    <span>Hello {{ userData.firstName }} {{ userData.lastName }}</span>
</ng-container>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1