Angular, NgRx And Adaptors: Separating State From Observers


Image result for adaptor"

In this post I'm going to discuss a model we've developed on my open source project as well as in my day job. With it we hide the details for state management in an Angular application that is using NgRx stores and effects.

Overview

When working with NgRx, one of the issues to solve is to provide state management without too tightly coupling the details of that state with the components that benefit from it. You want to avoid having your components reacting to every change in that state when the changes are of no interest to that component's area of concern. For example, if your application has a state for managing the currently authenticated user, you may not want your navigation bar have to process a state update at any time other than when the user's name or email address is changed or if their authentication state changes.

The goal of this pattern is to hide the details of that state management inside of an injectable class that maintains knowledge of that state, provides methods to initiate actions and observables to allow components to subscribe to only specific changes in state, those in which it is interested.

In our example project, we've created a simple feature to perform multiplication and division. It provides the simple ability to multiply two numbers and return the product of those them. To accomplish this we provide the actions, the reducer and the effects to perform this operation. And to hide the details, we provide an adaptor that can be injected into any component to allow it to either initiate this behavior, to receive updates when such behavior concerns, or to do both.

The Adaptor Details

Our adaptor code is very simple:

@Injectable()
export class MultiplicationAdaptor {
  private _first$ = new BehaviorSubject<number>(0);
  private _second$ = new BehaviorSubject<number>(0);
  private _product$ = new BehaviorSubject<number>(0);
  private _showProduct$ = new BehaviorSubject<boolean>(false);

  constructor(private store: Store<AppState>) {
    this.store
      .select(multiplierFeatureKey)
      .pipe(filter(state => !!state))
      .subscribe((state: MultiplicationState) => {
        console.log('*** state:', state);
        if (this._first$.getValue() !== state.first) {
          this._first$.next(state.first);
        }
        if (this._second$.getValue() !== state.second) {
          this._second$.next(state.second);
        }
        if (this._product$.getValue() !== state.product) {
          this._product$.next(state.product);
        }
        if (this._showProduct$.getValue() !== state.showProduct) {
          this._showProduct$.next(state.showProduct);
        }
      });
  }

  get first$(): Observable<number> {
    return this._first$.asObservable();
  }

  get second$(): Observable<number> {
    return this._second$.asObservable();
  }

  get product$(): Observable<number> {
    return this._product$.asObservable();
  }

  get showProduct$(): Observable<boolean> {
    return this._showProduct$.asObservable();
  }

  evaluate(first: number, second: number, online: boolean): void {
    this.store.dispatch(
      new MultiplierEvaluate({ first: first, second: second, online: online })
    );
  }
}


As you can see, the adaptor is very simple. When created it is give a reference to the NgRx store and subscribes itself to state changes, and then updates a set of BehaviorSubject instance variables based on changes to the state.

A key piece of efficiency here is in how the adaptor updates those subjects each time the state changes: it only updates those that have changed since the last update. So, for example, if the first value wasn't changed by the user, then no update is published when the next state change occurs.

This adaptor is then injected into any component that is interested in receiving updates.

In our example application, AppComponent is the type that subscribes only to notifications to show the product and the values involved, while the MultiplicationComponent is the type that initiates the process based on user interactions.

Initiating Actions

To start the process, the user enters the first and second values into the form and clicks the Evaluate button. This results in the following call to the adaptor:

this.multiplicationAdaptor.evaluate(
  this.multiplicationForm.controls.first.value,
  this.multiplicationForm.controls.second.value
);


This passed the two values to the adaptor, which then translates that into NgRx action that is dispatched for processing. Below is the code from the adaptor:

evaluate(first: number, second: number): void {
  this.store.dispatch(
    new MultiplierEvaluate({ first: first, second: second })
  );
}


With this we have decoupled our component from how the action is performed.

Observing State Changes

When the state changes, there are four things potentially published to observers: changes to the first and second values, changes to the product, and changes to the flag to show or hide the product.

Our application component is provide an instance of our adaptor (code not shown for brevity). In its Typescript code the component subscribes to updates to the show product flag and then sets its own local instance variable based on changes to the state:

ngOnInit(): void {
  this.multiplicationAdaptor.showProduct$.subscribe(
    show => (this.showProduct = show)
  );
}


This flag toggles showing or hiding the multiplication output as we see below:

<div *ngIf='showProduct'>
  <h2>Multiplication Output</h2>
  The product of {{multiplicationAdaptor.first$|async}}
             and {{multiplicationAdaptor.second$|async}}
              is {{multiplicationAdaptor.product$|async}}.
</div>


A key piece to take note of in the HTML is how the values and the product are updated in the HTML. Rather than storing these values as instance variables on the component, the adaptor is accessed directly and, since it's an Observable, is filtered through Angular's AsynPipe. The further reduces the amount of code needed to translate state changes to updates for the user.

So, What Are The Benefits?

As you can see from this simple example, the benefits are a separation of concerns between the various components in the application and the processing of state changes, and of publishing those changes atomically to components that are interested in specific changes to the state.

Source Code

You can view the source code for this project here.

As with any other code, if you find any errors of have an update to share, please send them over and I'll include them.

Comments