1.2 FeatureStates FeatureStores - mertenhanisch/ng-architecture GitHub Wiki

Da freut man sich über die "reactive-state components" und implementiert fröhlich eine Funktionalität nach der anderen, weil es so gut von der Hand geht, und dann stellt man nach kurzer Zeit fest: "Huch, meine State-Management-Datei ist ja schon über 1.000 Zeilen lang!". Du hast dich eigentlich noch gar nicht wirklich ausgebremst gefühlt, aber so langsam sagt dir dein Bauch, dass das vielleicht in der Zukunft zu einem Problem werden kann, insbesondere, wenn andere Entwickler (z.B. Future-Du) auf diesen Code schauen sollen, um irgendwas zu fixen oder zu verändern.

Auftritt: Feature-State

Du schaust dir deinen State an und stellst fest, dass du die ganzen Actions in kleinere Gruppen unterteilen kannst, weil sie sich jeweils immer nur mit einer Handvoll Properties in deinem gesamten State kümmern. Da liegt es nahe, diese Gruppe von Codeteilen irgendwie zu separieren. Ein Feature-State sieht nach außen hin erst mal genauso wie ein normaler State aus. Der einzige Unterschied: er hat nur wenige Properties und nur eine Funktion. Der Clou liegt dann darin, aus vielen Feature-States einen großen Root-State zu komponieren, der dann von der Component verwendet wird. So ähnlich, wie man also eine große Component in viele kleinere Components zerlegt, kann man dies auch mit dem State machen.

Zuerst kopiert man sich die nötigen Codebestandteile wie State-Interface, initialState und die passenden Actions und Effects in eine neue Datei, die dann nach dem Feature benannt ist. Danach entfernt man diese Dinge aus dem Root-State und fügt stattdessen eine Property vom Type des Feature-State ein.

export interface FeatureState {
  readonly initialized: boolean;
  readonly someProperty: string;
}

export const initialFeatureState: FeatureState = {
  initialized: false,
  someProperty: '',
};

...

export const initialize = (store: BsStore<FeatureState>, ...): BsStoreAction<FeatureState> => {
  return (lastState): FeatureState => {
    if (lastState.initialized) {
      return lastState;
    }

    ...

    return {
      ...lastState,
      initialized: true,
    };
  };
};
import * as Feature from './feature.state';

export interface RootState {
  readonly initialized: boolean;
  readonly feature: FeatureState;
};

export const initialRootState: RootState = {
  initialized: false,
  feature: Feature.initialFeatureState,
};

...

export const initialize = (store: BsStore<RootState>, featureStore: BsStore<FeatureState>, ...): BsStoreAction<RootState> => {
  return (lastState): RootState => {
    if (lastState.initialized) {
      return lastState;
    }

    featureStore.dispatch(Feature.initialize(featureStore, ...));
    ...

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

Wie jetzt, soll ich da zwei Stores parallel betreiben, die sich quasi um den gleichen State kümmern? Macht das nicht Probleme?

Auftritt: Feature-Store

Bisher haben wir in der Component einen BsRootStore benutzt (und haben uns sicherlich auch schon gefragt, warum der so heißt). Der Root-Store ist genau die BsStore-Implementation, die tatsächlich die ihr übergebenen Actions aufruft und damit einen neuen State erschafft, um ihn an die Außenwelt weiterzugeben. Parallel dazu gibt es einen BsFeatureStore. Dieser arbeitet quasi als ein Proxy zwischen der Außenwelt, die den Store nutzen will, und einem übergeordneten BsStore (Parent-Store, meist ein BsRootStore, es kann aber auch ein anderer BsFeatureStore sein). Jedes dispatch oder stateChanges wird so umgewandelt, als ob es direkt vom Parent-Store aufgerufen wurde und mit genau der Property, die zu dem Feature-State im Parent-State gehört. Parent-States wissen also alles über ihre Feature-States, ein Feature-State weiß aber nichts über seinen Parent-State. Das ist auch der Grund, warum es das BsStore-Interface gibt, damit es der State-Implementation egal sein kann, ob sie in einem Root-Store, einen Feature-Store oder einem ganz anderen Store läuft. Warte, es gibt noch andere Stores? Ja, aber das ist Inhalt eines anderen Beitrags...

const onDestroy: Observable<unknown> = ...;
const rootStore = new BsRootStore<RootState>(Root.initialRootState, onDestroy);
const featureStore = new BsFeatureStore(rootStore, 'feature');

rootStore.dispatch(Root.initialize(rootStore, featureStore, ...));

featureStore.dispatch(Feature.someFeatureAction(...));
rootStore.dispatch(Root.someRootAction(...));

Das (Root-)Template kann weiterhin einfach die State-Changes des Root-Store per async-Pipe abonnieren und gefahrlos auf alle Properties des gesamten States inklusive der Features zugreifen. Es ist aber auch eine Feature-Component denkbar, die als Input-Binding den Feature-Store mitbekommt und dann in ihrem Template einfach die State-Changes des Feature-Store abonniert. Sie kann dann auch direkt Actions auf diesen Feature-Store absetzen, welche automatisch im Root-State der Parent-Component ankommen.

Dein State ist dir über den Kopf gewachsen? Dann hast du jetzt keine Ausrede mehr, um ihn zu zerlegen...

⚠️ **GitHub.com Fallback** ⚠️