1.1.1 reactive state component - mertenhanisch/ng-architecture GitHub Wiki

Der Aufbau einer Component, die sich eines "reactive state" bedient, folgt immer nach dem gleichen Muster.

some.state.ts

export interface SomeState {
  initialized: boolean;
  ...
}

export initialSomeState: SomeState = {
  initialized: false,
  ...
};

// diverse Actions

// diverse Effects

export const initialize = (...): BsStoreAction<SomeState> => {
  return (lastState): SomeState => {
    if (lastState.initialized) {
      return lastState;
    }

    // initialize effects

    return {
      ...lastState,
      initialized: true,
    };
  };
};

some.component.ts

import * as Some from './some.state';

@Component({
  ...
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SomeComponent implements OnInit, OnDestroy {
  private readonly _onDestroy = new Subject<true>();
  private readonly _store = new BsRootStore<SomeState>(Some.initialSomeState, this._onDestroy);

  public readonly state$ = this._store.stateChanges;

  constructor(...) {
  }

  ngOnInit(): void {
    this._store.dispatch(Some.initialize(...));
  }

  ngOnDestroy(): void {
    this._onDestroy.next(true);
    this._onDestroy.complete();
  } 
}

some.component.html

<ng-container *ngIf="state$ | async as state">
  ...
</ng-container>

Für den Fall, dass die Component ihre Daten nicht nur aus der Route bezieht, sondern auch ein oder mehrere Input-Bindings besitzt, ist es sinnvoller, den Store schon direkt im Constructor zu initialisieren, damit beim Setzen der Input-Bindings schon alles bereit ist. Was auch immer über ein Input-Binding in die Component kommt, wird genauso in den State transferiert, als ob es durch ein anderes Ereignis, die Route usw. in der Component ankommt. Es bietet sich auch an, nur ein Input-Binding mit einem Objekt zu definieren, das alle möglichen Parameter enthält. Dadurch ist sichergestellt, dass immer alle Eingaben in einem Tick in der Component ankommen. Bei mehreren getrennten Input-Bindings ist es schwerer, die übergreifende Konsistenz sicherzustellen.

export interface SomeInput {
}

export const SomeComponent ... {
  ...

  constructor(...) {
    this._store.dispatch(Some.initialize(...));
  }

  @Input() set data(value: SomeInput) {
    this._store.dispatch(Some.setInput(value));
  }

  ...
}

Das Template bedient sich komplett nur aus dem State-Objekt, welches aus dem Store über die Async-Pipe geholt wird. Soll im Template eine Aktion ausgelöst werden (Html-Event wie Click, Mouse, Keyboard usw.), wird immer nur eine ganz einfache Funktion in der Component aufgerufen, die das Event in den Store dispatched.

<button (click)="submit(...)">...</button>

  submit(...): void {
    this._store.dispatch(Some.submit(...));
  }

Meistens sind noch nicht mal Parameter nötig, weil häufig schon alle Informationen im State vorhanden sind. Üblich ist aber auch, dass z.B. eine Id oder ein Index (Id ist sinnvoller) bei einem Löschen-Button in einer Liste von Items mitgegeben wird.

Für Änderungen in Formularen sind ReactiveForms zu benutzen. Deren valueChanges-Observable lassen sich meist als Quelle in Effects benutzen, um daraufhin Actions im State auszulösen.

Unter der Voraussetzung, dass alle Actions pure Funktionen sind (die sich dadurch leicht testen lassen), und sie den State immer in einem konsistenten Zustand verändern (quasi in einer Transaktion), kann die Oberfläche "eigentlich" (TM) nie in einem nicht bedienbaren Zustand sein. Das kann sichergestellt werden durch:

  • Jede Action kann zuerst prüfen, ob der aktuelle State alle Voraussetzungen erfüllt, um die durch diese Action beabsichtigten Änderungen durchführen zu können.
  • Alle Änderungen durch eine Action werden in einem neuen State in einem Rutsch an den Store weitergeben.
  • Der Store sorgt dafür, dass immer nur eine Action zur Zeit und in der Reihenfolge ausgeführt werden, wie sie dispatched wurden. Das bedeutet u.a. auch, dass man gefahrlos aus einer Action eine weitere Action dispatchen kann, ohne dass sie vor Ende der aktuellen Action ausgeführt werden wird.
⚠️ **GitHub.com Fallback** ⚠️