ngrx rant — State für Wahnsinnige
NgRx ist eines von vielen Frameworks zur Verwaltung von "States" innerhalb von Webapplikationen. Hier meine Gründe warum ich es nicht mag.
Rote und Blaue Dateien
Bob Nystrom schreibt in seinem Artikel What Color is Your Function? über die Probleme bei der gleichzeitigen Verwendung von synchronen und asynchronen Funktionen in einem Programm. Der Kern des Artikels ist die Aussage, dass synchrone Funktionen keine asynchronen Funktionen aufrufen können wenn sie auf deren Ergebnis angewiesen sind.
Mein Gefühl ist, dass NgRx diese Spaltung noch weiter verschärft durch noch mehr Artefakte. Der State in NgRx kann nur synchron durch Actions verändert werden. Asynchrone Verarbeitung ist nur in Effects erlaubt. Ein Effect kann nur durch Actions den State manipulieren.
Das folgende stark vereinfachte ReactiveX-Beispiel zeigt eine einfachen asynchronen Ablauf den ich später in NgRx zeigen will.
trigger = new Subject();
output = trigger
.pipe(
switchMap(() => somethingAsynchronous()),
);
<div>{{ output | async }}</div>
<button click="trigger.next()">trigger</button>
In NgRx würde man für diesen Ablauf folgendes implementieren:
- Eine Action die beim click dispatched wird.
- Einen Effect der auf die click Action horcht, somethingAsynchronous() aufruft und dann eine "schreib in den Store" Action dispatched.
- Einen Reducer, der die "schreib in den Store" Action, die den State des Stores manipuliert.
- Wenn man es "ordentlich" macht wird man auch noch einen Selector Schreiben um das Ergebnis von somethingAsynchronous() wieder aus dem State zu extrahieren.
In Code sieht das Ganze dann ungefähr so aus:
let clicked = createAction('[Ye View] Clicked');
let resultDelivered = createAction(
'[somethingAsynchronous] Result Delivered',
props<{ result: MyResultType }>()
);
class MyEffects {
createEffect(() => this.action$
.pipe(
ofType(clicked),
switchMap(() => somethingAsynchronous()),
map(result => resultDelivered({result})),
)
);
}
createReducer(
on(resultDelivered, (state, { result }) => ({
...state,
result,
})),
);
export const selectResult = createSelector(
selectMyState,
state => state.result
);
Man darf nicht vergessen, dass dazu für jede Action die Transportparameter der Action deklariert werden müssen. So vervielfacht sich schnell die Tipparbeit und auch der Aufwand wenn man Strukturen bei neuen Features wieder anfassen muss.
Das Ganze erinnert mich an Bill Gate's Aussage "Measuring programming progress by lines of code is like measuring aircraft building progress by weight.".
Side Effects
Laut Mike Ryan's Talk Good Action Hygiene with NgRx ist alles in NgRx ein Side Effect. Und zwar nicht nur weil es "so sein muss", sondern "weil es so sein soll". Und ich denke Mike weiß wovon er sprich, denn er ist "NgRx Core Team & Software Engineer at Synapse".
Für jemanden dem Funktionale Programmierung wegen ihren Einfachheit ganz gut gefällt ist dies sehr, sehr schwer zu verstehen.
Ja, sehr schwer.
Wenn man eine NgRx Applikation verstehen will ist man ständig am Referenzen suchen.
- Was macht diese Action? → Suche nach Referenzen ergibt, dass sie in 2 Effects und 3 Reducern verwendet wird. → 😕
- Woher kommen die Daten aus dem Selector? → Der Selektor liefert Property X aus meinem State. X wird in 3 Reducern gesetzt. → 😫
Wenn man ReactiveX .pipe(…)-Ketten gewöhnt ist folgt man nur den Events bis zur Quelle. Und wenn man schön sauber mit so wenig BehaviorSubjects wie nötig programmiert hat muss man dazu nie m zu n Beziehungen auflösen.