Angular: observe two states but only act once

Viewed 104

My component observes two states (in observable stores): selectedCar and viewConfig. When either of these states updates, I need to run showCar() to display the car with the new config. However, when both of these states update, e.g. on init, showCar() should only run once.

Code below:

    this.selectedCarStore.state$
      .pipe(takeUntil(this.ngUnsubscribe))
      .subscribe(state => {
        this.selectedCar = state.selectedCar;
        this.showCar();
      });

    this.configStore.state$
      .pipe(takeUntil(this.ngUnsubscribe))
      .subscribe(_state => {
        this.viewConfig = state.viewConfig;
        this.showCar();
      });
    
    showCar() {
      if (this.viewConfig != null || this.selectedCar != null) return;

      // update UI with selected car and view config
    }
2 Answers

The displaying of your car has two sources: selectedCar and viewConfig. We can design an observable that will emit only when both of these data are available AND whenever either of them change. The combineLatest operator is perfect for this:

So now, instead of having two separate subscriptions, we just have one!

ngOnInit() {
  combineLatest([
    this.selectedCarStore.state$, 
    this.configStore.state$]
  ).pipe(
    takeUntil(this.ngUnsubscribe)
  )
  .subscribe(([carState, config]) => {
    this.allCars     = carState.allCars;
    this.selectedCar = carState.selectedCar;
    this.viewConfig  = config.viewConfig;
    this.showCar();
  });
}

The above code in the subscribe block will only emit when both selectedCarStore.state$ and configStore.state$ have emitted at least once.

Check out this working StackBlitz; See how the view is updated when you select different View Config or select a different car.

This accomplishes your goal... but we can make the code even better (simpler)!


If we take a look at your comment for the showCar() method:

// update UI with selected car and view config

It sort of hints at the fact that we weren't previously structuring the code in a declarative / reactive way. Now that we have designed a stream that emits when we want, with all the necessary data, we don't need to "manually" check in a function to update the view. Our view will update automatically, because our stream emits at the appropriate time. Any time either of the sources emit a new value, the view will automatically be updated! :-)

Notice how we are copying state into our component:

this.allCars     = carState.allCars;
this.selectedCar = carState.selectedCar;
this.viewConfig  = config.viewConfig;

This actually isn't necessary. Instead of subscribing in our component, we can leave all the data the view needs as observables. So instead of:

public viewConfig: ViewConfig;
public selectedCar: Car;
public allCars: Car[];

We can do:

  private viewConfig$ = this.configStore.state$.pipe(
    map(state => state.viewConfig)
  );
  private selectedCar$ = this.selectedCarStore.state$.pipe(
    map(state => state.selectedCar)
  );
  public selectedCarVm$ = combineLatest([this.selectedCar$, this.viewConfig$]).pipe(
    map(([car, config]) => ({car, config}))
  );
  public allCars$ = this.selectedCarStore.state$.pipe(
    map(state => state.allCars)
  );

The public ones are what the view needs. I used the Vm suffix to distinguish between the data for selectedCar and the view model we designed to only emit when both the data and config are available.

Notice, we define these observable streams right on our class, we no longer need to do this in ngOnInit. We also no longer need to subscribe, as we can use the async pipe in our template.

  <div *ngIf="selectedCarVm$ | async as selected"
    [ngStyle]="{'background-color': selected.config.bgColor }"
    [class.large]="selected.config.size === 'large'"
  >
    {{ selected.car.year }} {{ selected.car.make }} {{ selected.car.model }}
  </div>

The means we no longer need to manage unsubscribing, so we can get rid of ngOnDestroy, the ngUnsubscribe Subject as well as all our takeUntils!

Please compare code between your Original Code > the First Solution > and this Reactive Solution.

Each step reduces the code and simplifies the logic.

I hope this helps to show you how you can design streams to emit at the desired time, and keep the data in observable form in order to reduce the amount of code (and complexity).

Cheers! :-)

when both of these states update

Given the single thread nature of JavaScript, the states would never update simultaneously. Once you understand that, your problem becomes one of resolution. i.e, "when both of these states update within x time units of the other"

You may use operators like throttleTime and auditTime after merging both with merge to solve.

Related