Hedi
Hedi

Reputation: 3

Getting and storing a global variable

I am struggling with a simple concept I can easily do in C#, but not in Angular.

I am trying to store the user profile in a service I add in every component, so I do not get the profile every time a component is loaded/displayed.

So, I created a service, where the constructor gets the user profile, and saves it in a variable, and use a getter for all my components.

The issue I have, is that when the components call the getter, the variable is net set yet, as the service has not responded yet. This leads to an undefined error and breaks the component as the profile is vital in the code of the component.

In C#, I would simply put 1 await when getting the profile, so that everything else can run without waiting, as I know the data is there.

But in Angular, I tried that, but it does not seem to work.

export class SharedService {
  private userProfile;
  constructor(private http: HttpService) {
    this.initProfile();
  }
  async initProfile(): Promise < void > {   
    this.userProfile =await this.http.getProfile().subscribe((data: any) => {
       //...
    }
  });

  getUserProfile() {
    return this.userProfile;
  }
} 

The getprofile:

  getProfile() {
    return this.shttp.get(environment.apiEndpoint + 'Getprofile/', {
      headers: new HttpHeaders({
        'Content-Type': 'application/json',
        Accept: '*/*'
      })
    });
  }

In the components, when I call my getUserProfile function, the service answers immediately with the empty userProfile.

EDIT: I manage to have the desired behavior, with very nasty code snippet, but this should make clear what I am trying to accomplish. In my profile.component.ts:

async ngOnInit(): Promise<void>  {    
 while (this.profile == null) {
  await this.sharedService.sleep(500);
  this.profile = this.sharedService.getUserProfile();    
 }
 if (this.profile.isadmin){
  this.CalculateStuff();
 }
 //
 //imagine here even more code using the this.profile variable in if statements, 
 //cases, etc.
 //
}

The sleep function

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

The fact I have to code a sleep function to achieve this, is a clear signal to me that i am doing something wrong. That is why I am here.

Upvotes: 0

Views: 2302

Answers (2)

James
James

Reputation: 2895

You're almost there. You're awaiting a subscription which doesn't work with async / await. Just do the following. Just convert it to a promise so you can await it.

async ngOnInit(): Promise<void>  {  
  this.profile = await this.sharedService.getUserProfile().toPromise();    
 if (this.profile.isadmin){
  this.CalculateStuff();
 }
}

This question is similar How exactly works this async-await method call? is it correct?

To give you a bit more detail. What you're doing is assigning this.userProfile to the RXJS subscription object. This will not give you the value that you need. If you want to stay with Rxjs as suggested in one of the comments. You can just do all the logic for the user profile inside the subscribe block.

  this.http.getProfile().subscribe((userProfile: any) => {
       // do userProfile actions here
  });
  //Since subscribe is an async callback, anything outside that block will have userProfile undefined.

Also as mentioned, a rudimentary knowledge of RxJs will be very useful and solve a lot of your early issues. https://www.learnrxjs.io/learn-rxjs/concepts/rxjs-primer

Upvotes: 0

Barremian
Barremian

Reputation: 31125

One other way would be to make the userProfile in the service a multicast observable (like RxJS ReplaySubject with buffer 1). That way there isn't a need to mix observables with promises.

Service

import { ReplaySubject } from 'rxjs';

export class SharedService {
  private userProfile = new ReplaySubject<any>(1);

  constructor(private http: HttpService) {
    this.initProfile();
  }

  initProfile() {   
    this.http.getProfile().subscribe({
      next: (data: any) => {
        this.userProfile.next(data);           // <-- push the new profile
      },
      error: (error: any) => { }               // <-- handle error
  });

  getUserProfile(): Observable<any> {          // <-- return observable here
    return this.userProfile.asObservable();
  }
}

Now in the component you could subscribe to the subject from the service. Additionally you could use takeUntil operator with Subject to close the open subscription when the component is closed/destroyed.

Component

import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

export class SomeComponent implements OnInit, OnDestroy {
  public userProfile: any;
  private closed$ = new Subject<any>();

  constructor(private shared: SharedService) { }

  ngOnInit() {
    this.shared.getUserProfile().pipe(
      takeUntil(this.closed$)
    ).subscribe({
      next: (data: any) => {
        this.userProfile = data;
        // other statements that depend on `this.userProfile`
      }
    });
  }

  ngOnDestroy() {
    this.closed$.next();   // <-- close open subscription(s)
  }
}

Upvotes: 2

Related Questions