NgRx bietet seit geraumer Zeit eine Bibliothek für das State-Management in Angular Anwendungen. Die Diskussionen um die Notwendigkeit einer solchen Bibliothek ist relativ groß. Zumal der klassische NgRx-Ansatz zu sehr viel Boilerplate-Code führt, der vielen pragmatischen Entwicklern ein Dorn im Auge ist. Seit einigen Jahren ist NgRx auf den von Angular angeschobenen „Signals-Zug“ aufgesprungen und bietet einen Signal-basierten, leichtgewichtigen Ansatz: @ngrx/signals
Ein SignalStore wird in Angular Anwendungen analog zu einem zentralen Service verwendet. Stellt also Methoden und Attribute zur Verfügung, die Komponenten-übergreifend verwendet werden können. Der SignalStore setzt hierbei jedoch vollständig auf Signals und verfolgt damit einen reaktiven Ansatz.
Hello SignalStore
Beginnen wir mit einem sehr einfachen SignalStore
export const HelloStore = signalStore(
// { providedIn: 'root' }, = globaler Store
withState({
comment: {
author: 'Alice',
message: 'Welcome to NgRx Signals!'
}
})
)
über „signalStore“ erzeugen wir ein entsprechendes Objekt mit einem initialen State. Dieser State (in unserem Fall ein Objekt „comment“) wird nun nach außen zur Verfügung gestellt. In einer unserer Komponenten können wir jetzt einen solchen Store beziehen:
@Component({
selector: 'ged-say-hello',
templateUrl: './say-hello.html'
providers: [
HelloStore
]
})
export class SayHello {
helloStore = inject(HelloStore);
}
<div>{{helloStore.comment().author}} : {{helloStore.comment.message()}}</div>
Zwei Stellen sind hier schon spannend: wir nutzen die „providers“ der Komponente, um dieser Komponente (und ihren Kind-Komponenten) eine Instanz des Stores zur Verfügung zu stellen. Eine solche Bereitstellung auf Komponente-Baum Ebene ist flexibler als eine globale Bereitstellung. Außerdem bindet sich der Store an den Lebenszyklus der Komponente und wird zerstört, wenn diese zerstört wird (analog zu provided Services).
Ein zweiter Punkt fällt nur dem sehr aufmerksamen Betrachter auf: im Template verwenden wir zwei unterschiedliche Zugriffspfade, die offenbaren, dass hinter den Kulissen mehr passiert, als Werte nur als Signals bereit zu stelle. Der Store führt für komplexe Objekte „deepSignals“ ein = das Objekt selber (helloStore.comment()), aber auch die einzelnen Attribute (helloStore.comment.message()) werden als Signal zur Verfügung gestellt. Damit können wir sehr fein granular auf Änderungen reagieren, z.B. nur wenn sich der Autor ändert, ohne zusätzliche Signals selber zu definieren.
Patching
States verändern sich. Hier setzt NgRx auf eine Helper Methode die den Zustand innerhalb des Stores aktualsiert. Eine solche Aktualisieurng kann technisch auch durch die Komponenten aufgerufen werden. Dies sollte in aller Regel vermieden werden. Der State bietet die Möglichkeit entsprechende Methoden zu deklarieren, die als öffentliche API zu diesem Store angesehen werden sollen:
export const HelloStore = signalStore(
withState(...),
withMethods((store) => ({
updateComment: (message: string, author: string) => {
const newComment = {message, author};
patchState(store, {comment: newComment})
},
}))
)
„withMethods“ erlaubt es uns nun entsprechende Logik zu implementieren. Sie nimmt ein Objekt entgegen, dessen Attribute die Methoden-Callbacks sind. Hier nutzen wir die „patchState“ Methode, die als ersten Parameter den Store und als zweiten Parameter ein Updater (meistens das aktualisierte State Objekt) annimmt, der den zu änderten Teil des States liefert (alle anderen Werte des Stores bleiben unverändert erhalten)
Patching, RxMethod
In aller Regel kommuniziert unsere Anwendung mit Rest-Ressourcen, die bei uns in den Projekten mittels OpenAPI Services angesprochen werden. Hier bietet NgRx eine entsprechende Wrapper Methode, welche die Brücke zwischen Obersables und unserem SignalStore schlägt. Der generische Typ legt dabei den Methoden-Parameter fest und die Funktion erwartet eine Observable Operator Chain.
export const HelloStore = signalStore(
withState(...),
withMethods((store, commentService= inject(CommentService)) => ({
updateComment: ... ,
loadMessage: rxMethod<number>(
pipe(
switchMap( id => service.getComment(id)),
tapResponse({
next: (comment: any) => {
const newComment = {message: comment.body, author: comment.user.username};
patchState(store, _store => ({comment: newComment}))
},
error: err => console.log(err),
})
))
)
Der Aufruf ist erst mal unspektakulär: this.helloStore.loadMessage(2); versteckt aber auch hier ein Stückchen Magie. Der in rxMethod deklarierte Typ (number) ist mit Nichten nur mit einem primitiven Wert zu übergeben, sondern erlaubt uns auch reaktive Typen (Signals,Observables). Damit interagieren diese Methoden hervorragend mit z.B. Input-Signals und updaten hier die Daten bei Änderungen der übergebenen Id:
export class SayHello {
helloStore = inject(HelloStore);
messageId = input<number>(1);
constructor() {
this.helloStore.loadMessage(this.messageId);
}
}
computed und effect
Zwei „alte“ Bekannte des Signals haben wir noch gar nicht gesehen, die aber natürlich auch ihren Platz finden. Computed (also abgeleitete Werte) werden über eine entsprechende Methode im Store analog zu den methods deklariert:
export const HelloStore = signalStore(
withState(...),
withMethods(...),
withComputed(store => ({
chars: () => store.comment.message().length
})),
Zudem haben wir auch die Möglichkeit Lifecycle Hooks zu implementieren, wo wir auch entsprechende effects verwenden können:
export const HelloStore = signalStore(
withState(...),
withMethods(...),
withComputed(...),
withHooks(({
onInit: (store) => {
store.loadMessage(1);
effect(() => {
console.log('author changed: ' + store.comment.author());
});
},
onDestroy: (store) => {},
}))
)
…brauchen wir das?
Am Ende bleibt die Frage: Brauchen wir das? Eine ähnliche Funktionalität ließe sich erst einmal auch mit einfachen Services, mit Signals lösen, in dem wir noch flexibler agieren können und das ganz ohne den Preis einer weiteren Abhängigkeit, die ggf. unsere Angular Update Vorhaben im Wege stehen. Auf der anderen Seite schenkt uns der SignalStore hier und da ein paar Nettigkeiten. Viel wichtiger in meinen Augen ist aber die „Standardisierung“, gerade in größeren Projekten, mit Entwicklern verschiedener Skill-Level im Umgang mit Angular. Der SignalStore definiert exakt einen Weg innerhalb unseres Projektes, die OpenAPI-Services zu kapseln und einen reaktiven State zur Verfügung zu stellen. Ob es das Wert ist, hängt damit sicherlich auch vom Projektteam ab und sollte bewusst entschieden werden.







