1

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.

Tony Guinta
  • 633
  • 1
  • 8
  • 14

2 Answers2

2

Problem is that setUserData() is an async function and it takes some time to this.httpService.getUserData() to fetch your data from backend. So when your component calls it ngOnInit() method userDataSubject hasn't yet received the new value and that's why it returns null. I would suggest using BehaviorSubject subscribe handler so your code will do what it is supposed to do at the right time. It would be something like:

export class UtilsService {
  public 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);     
  }

and in your component

ngOnInit() {
  this.utils.userDataSubject.subscribe((data) => {
    if(data === null)
      return;

    console.log(data);

     // component logic to use your data
     // this will be called every time userDataSubject receives a new value
  });
}
alexortizl
  • 2,125
  • 1
  • 12
  • 27
  • Thanks! I had tried a solution similar to this, but was missing the "if (data === null) return" line, and was getting errors when trying to access any keys from userData. – Tony Guinta Oct 30 '19 at 06:29
0

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>
akop
  • 5,981
  • 6
  • 24
  • 51