3

Background

In an Angular component, I need to load some settings before to load the data, so I have the following code:

  constructor(
    private dataService: DataService,
    settingsService: SettingsService
  ) {
    settingsService.get().subscribe({
      next: (settings) => this.settings = settings
    });
  }

  onInit() {
    this.loadData();
  }

  loadData() {
    this.dataService.get(this.settings).subscribe({
      next: (data) => this.data = data
    });
  }

As you can see, this.settings has to be loaded before to call loadData(), which is called in the initialization and subsequently when the user press a button. While settingsService.get() is called just once in the beginning, this.dataService.get() can be called many times.

Problem

The first loadData() calling from onInit() has been called before settingsService.get() finishes, so the parameter is undefined and I get an error.

Question

How do I synchronize them for this particular situation?

NOTE

There is a similar question at Angular - Make multiple HTTP calls sequentially, but I believe it is a different scenario, because in that case both observers are called every time together, sequentially. In this particular case, I need to synchronize them only on the first time, so I cannot see how switchMap can be used here. I am not a RXJS expert, so if I'm not right, some tip will be welcome. Thanks.

Gabriel
  • 312
  • 1
  • 13

4 Answers4

1

You can just use switchMap to load data from settings first and then call the dataService.get() with the result:

readonly data$ = this.settingsService.get().pipe(
  switchMap(settings => this.dataService.get(settings))
);

constructor(private dataService: DataService, private settingsService: SettingsService) {}

Notice I'm not bothering to use the ngOnInit() lifecycle hook, and instead just creating a stream that will be consumed using the async pipe on the template. This makes the code a lot smaller, and you don't have to go through the effort of unsubscribing from subscriptions in the ngOnDestroy() hook.

  • If you only want one result, and either of the methods stay open, then you can easily add take(1) after the switchMap.
  • If there are multiple bindings to data$ on then template then consider adding the shareReplay operator at the end of the stream so multiple underlying calls aren't made.
Daniel Gimenez
  • 18,530
  • 3
  • 50
  • 70
  • Thank you @Daniel Gimenez, but I can't see in this example how to make subsequent calls to ```this.dataService.get()``` without to call ```this.settingsService.get()``` together thru the ```data$``` variable – Gabriel Jul 01 '21 at 21:29
  • Under what circumstances do you need to call `this.dataService.get()` again? Do the settings change? – DeborahK Jul 01 '21 at 22:32
  • Hi, @DeborahK, that code is a minimal reproducible example. Actually ```this.dataService.get()``` carries more parameters that depends on user interaction. So, yes, the settings change. – Gabriel Jul 02 '21 at 11:27
  • You could define "action" streams. Here is an article: https://tomastrajan.medium.com/practical-angular-the-most-impactful-rxjs-best-practice-tip-of-all-time-c5d717ec8c4b I have a github example (using pagination instead of settings, but the idea would be the same): https://github.com/DeborahK/Angular-ActionStreams Here is a talk on the topic: https://www.youtube.com/watch?v=OKZBHuYa-wc – DeborahK Jul 02 '21 at 18:05
0

I think what you need here is Subject. See code and explanations below.

  //will be use to trigger if settings api completes
  private settingsReady: Subject<any> = new Subject<any>();
  
  //make settingsReady as observable so we can subscribe to it
  private settingsReady$: any = this.settingsReady.asObservable();
  
  //mock the settings api that will be executed after 5 sec
  private settingsApi$ = of('settingsApi').pipe(
    delay(5000),
  );
  
  //mock the data api that will be executed after 2 sec
  private dataApi$ = of('dataApi').pipe(
    delay(2000),
  );
  
  constructor() { 
    //trigger the settings api
    this.settingsApi$.subscribe((result: any) => {
      console.log('settingsApi execute after 5 seconds =>', result);
  
      //inform settingsReady subject that setting api already complete
      //##KEY_PART you can also pass the result of the api here
      this.settingsReady.next(result);
    });
  }
  
  ngOnInit() {
    //will only trigger once settings api complete
    //that is when settingsReady call next on ##KEY_PART
    this.settingsReady$.subscribe((settingsResult) => {
      console.log('settings result from Subject =>', settingsResult);
      this.loadData();
    });
  }
  
  loadData() {
    //trigger the data api
    this.dataApi$.subscribe((result) => {
      console.log('dataApi execute after settingsApi completes =>', result);
    });
  }

Result of above code:

> settingsApi execute after 5 seconds => settingsApi
> settings result from Subject => settingsApi
> dataApi execute after settingsApi completes => dataApi

Note: On some case, you may want to trigger loadData immediately without waiting for the settings api to return value initially. For such scenario, you may look into BehaviorSubject.

carlokid
  • 207
  • 1
  • 4
0

Use the power of observables. switchMap will ensure that first observable completes first

constructor(
    private dataService: DataService,
    settingsService: SettingsService
) {}

onInit() {
    this.loadData();
}

loadData() {
    this.settingsService.get().pipe(
        switchMap(settings => this.dataService.get(settings))
    ).subscribe(data => {
        // do something
    })
}
Timothy
  • 3,213
  • 2
  • 21
  • 34
-1

you can use repeatWhen:

    private reload$ = new Subject<void>();
    
    constructor(
        private dataService: DataService,
        settingsService: SettingsService
    ) {}
 
    onInit() {
        settingsService.get()
            .pipe(
                switchMap(settings => this.dataService.get(settings).pipe(repeatWhen(() => reload$))),
                
            ).subscribe({next: (data) => this.data = data});
    }

    reloadData() {
        this.reload$.next();
    }