Effizientes State-Management in Angular: Vergleich von ComponentStore und normalen Services

Effizientes State-Management in Angular: Vergleich von ComponentStore und normalen Services

31. Okt. 2024 | Gregor Koletzki | 6 Min. Lesezeit

Angulars ComponentStore ist eine elegante und performante State-Management-Lösung, die speziell für die Verwaltung von Status (State) innerhalb einzelner Komponenten oder kleinerer Komponentenhierarchien entwickelt wurde. Sie ist Teil der @ngrx/component-store-Bibliothek und bietet einen modularen und reaktiven Ansatz zur Handhabung und Optimierung von Zustandsverwaltung in Angular-Anwendungen.

Was ist der ComponentStore?

Im Gegensatz zu zentralisierten State-Management-Bibliotheken wie @ngrx/store, die den gesamten Status einer Anwendung speichern und verwalten, ist ComponentStore für spezifische Anwendungsfälle gedacht. Statt den State im globalen Store abzulegen, ermöglicht der ComponentStore es Entwicklern, Status direkt in einer Komponente zu verwalten. Das führt zu einer klaren Trennung der Zuständigkeiten und verbessert die Lesbarkeit und Wartbarkeit des Codes.

Vorteile des ComponentStore

  • Modularität: Da der ComponentStore Status nur für eine bestimmte Komponente oder Komponentengruppe verwaltet, ist der Status modular und unabhängig von anderen Komponenten. Dies vereinfacht das Debuggen und die Wartung.
  • Effizienz: Der ComponentStore nutzt RxJS und ist vollständig reaktiv. Das bedeutet, dass nur dann Änderungen ausgelöst werden, wenn dies notwendig ist, was die Leistung bei größeren Anwendungen verbessern kann.
  • Isolation und Testbarkeit: Da jeder ComponentStore spezifisch für eine Komponente ist, ist der Code isoliert und kann einfacher getestet werden.

Grundprinzipien des ComponentStore

Der ComponentStore arbeitet mit drei Hauptkonzepten:

  • State: Der Status der Komponente wird lokal gespeichert und ist nur in der jeweiligen Komponente zugänglich.
  • Updater: Mit Updater-Funktionen können Sie den Status im ComponentStore aktualisieren. Sie arbeiten in der Regel mit reaktiven Streams und sind dafür verantwortlich, den State in einer unveränderlichen Weise zu aktualisieren.
  • Selector: Selector-Funktionen geben einen Observable zurück, das eine Teilmenge des State bereitstellt. Sie sind der einzige Zugangspunkt für andere Teile der Komponente, um auf den State zuzugreifen.
  • Effect: Effekte ermöglichen die Ausführung von nebenläufigen oder asynchronen Aufgaben, z. B. das Abrufen von Daten von einer API. Sie werden oft für komplexere Logik verwendet, die nicht direkt mit dem State zu tun hat.

Beispiel: Verwendung des ComponentStore

Angenommen, wir wollen einen UserComponentStore erstellen, der Nutzerdaten verwaltet und asynchrone API-Aufrufe abhandelt.

1. ComponentStore Setup

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Observable } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { UserService } from './user.service';

interface UserState {
  user: User | null;
  loading: boolean;
}

@Injectable()
export class UserComponentStore extends ComponentStore<UserState> {
  constructor(private userService: UserService) {
    super({ user: null, loading: false });
  }

  // Selector: Gibt den User-State zurück
  readonly user$ = this.select(state => state.user);
  readonly loading$ = this.select(state => state.loading);

  // Updater: Setzt den Loading-Status
  readonly setLoading = this.updater((state, loading: boolean) => ({
    ...state,
    loading,
  }));

  // Effekt: Lädt die User-Daten asynchron und aktualisiert den Status
  readonly loadUser = this.effect((userId$: Observable<number>) => {
    return userId$.pipe(
      tap(() => this.setLoading(true)),
      switchMap(userId => 
        this.userService.getUser(userId).pipe(
          tap({
            next: (user) => this.patchState({ user, loading: false }),
            error: () => this.setLoading(false)
          })
        )
      )
    );
  });
}

In diesem Beispiel wird der Initialzustand (user: null, loading: false) im Konstruktor definiert. Der ComponentStore bietet eine loadUser-Effektfunktion, um asynchrone API-Aufrufe zu handhaben, ohne dass die Komponente selbst diese Logik implementieren muss. Stattdessen wird die loadUser-Methode aufgerufen und über ein Observable der User-ID gesteuert.

2. Verwendung im Component

Im UserComponent lässt sich der UserComponentStore einfach nutzen:

import { Component, OnInit } from '@angular/core';
import { UserComponentStore } from './user.component-store';

@Component({
  selector: 'app-user',
  template: `
    <ng-container *ngIf="userStore.loading$ | async as loading">
      <p *ngIf="loading">Loading...</p>
    </ng-container>
    <ng-container *ngIf="userStore.user$ | async as user">
      <h1>{{ user.name }}</h1>
      <p>{{ user.email }}</p>
    </ng-container>
  `,
  providers: [UserComponentStore]
})
export class UserComponent implements OnInit {
  constructor(public userStore: UserComponentStore) {}

  ngOnInit() {
    this.userStore.loadUser(1); // Beispielhafte User-ID
  }
}

Hier bindet die Komponente direkt an die Observables loading$ und user$, die im ComponentStore definiert sind. Das hat den Vorteil, dass der ComponentStore die gesamte State- und Effektlogik behandelt und die Komponente nur die Präsentation übernimmt.

Vergleich zwischen ComponentStore und normalen Services

In Angular ist der Standardansatz zur Zustandsverwaltung die Verwendung von Services, die für eine einfache Zustandsverwaltung in der Regel ausreichen. Jedoch bringt ComponentStore einige Vorteile mit sich, die ihn in bestimmten Anwendungsfällen zu einer besseren Wahl machen können. Hier ist ein Vergleich der Vor- und Nachteile von ComponentStore im Vergleich zu normalen Services.

Vorteile von ComponentStore gegenüber normalen Services

  1. Reaktive Programmierung:

    • ComponentStore nutzt RxJS und ist vollständig reaktiv. Das bedeutet, dass State-Änderungen automatisch an alle Abonnenten weitergegeben werden, ohne dass manuell ein Event oder Subject ausgelöst werden muss. Dies kann den Code reaktiver, eleganter und leichter verständlich machen.
    • Normale Services benötigen oft zusätzliche Mechanismen wie BehaviorSubject oder Subject, um Änderungen im State an Komponenten weiterzugeben. Das führt zu mehr Boilerplate-Code und kann die Lesbarkeit verringern.
  2. Eingeschränkter Scope und Modularität:

    • ComponentStore eignet sich hervorragend für Komponenten, die spezifischen lokalen State benötigen, der sich leicht trennen lässt. Da jeder ComponentStore nur für eine Komponente oder eine kleine Komponentenhierarchie zuständig ist, bleibt der State modular und isoliert.
    • Normale Services sind oft global in der Anwendung eingebunden und teilen den State unter mehreren Komponenten. Dies kann in größeren Anwendungen zu einem "spaghettimäßigen" Zustand führen, wenn der State nicht sauber isoliert wird.
  3. Bessere Testbarkeit und Wartbarkeit:

    • ComponentStore-Logik ist in einer eigenen Klasse gekapselt, was das Testen und die Wartung vereinfacht. Selektoren, Updater und Effekte können isoliert getestet werden, da sie spezifische, kleine Einheiten sind.
    • Normale Services bieten zwar auch Möglichkeiten zur Kapselung, jedoch ist der Testaufwand oft höher, da Abhängigkeiten von anderen Komponenten und globalem State bestehen können.
  4. State-Management durch Updater und Selector:

    • ComponentStore verwendet ein klares, standardisiertes API für State-Updates (Updater) und State-Zugriffe (Selector). So ist der Code strukturiert und folgt einer konsistenten Architektur.
    • Normale Services müssen eigene Getter- und Setter-Methoden oder Streams implementieren, was den Codeumfang vergrößern und die Einheitlichkeit beeinträchtigen kann.
  5. Asynchrone State-Änderungen mit Effekten:

    • ComponentStore bietet Effekte, die speziell für nebenläufige und asynchrone Vorgänge (z. B. API-Aufrufe) entwickelt wurden. Effekte kapseln diese Logik und machen den Code strukturiert und einfach erweiterbar.
    • Normale Services müssen für asynchrone Operationen oft eine Kombination aus RxJS-Operatoren und zusätzlicher Logik verwenden, was den Code unübersichtlicher machen kann.

Nachteile von ComponentStore im Vergleich zu normalen Services

  1. Komplexität für einfache Anwendungsfälle:

    • ComponentStore kann für einfache Anwendungen oder Komponenten mit minimalem Status-Management unnötig komplex erscheinen, da für jeden State ein eigener Store und eine eigene Klasse notwendig sind.
    • Normale Services bieten oft eine einfachere Lösung für kleine Anwendungen und einfache Datenoperationen, da sie ohne zusätzliche Struktur auskommen.
  2. Zusätzlicher Lernaufwand:

    • ComponentStore basiert stark auf RxJS und reaktiven Konzepten, was für Entwickler, die neu in der reaktiven Programmierung oder in RxJS sind, eine Herausforderung darstellen kann.
    • Normale Services hingegen sind leichter zu verstehen und erfordern weniger Wissen über RxJS, was den Entwicklungsprozess für Entwickler ohne viel RxJS-Erfahrung vereinfacht.
  3. Überkopfanforderungen durch zusätzliche Abstraktionen:

    • ComponentStore erfordert zusätzliche Abstraktionen und eine neue Klassenstruktur, was die Komplexität in manchen Fällen unnötig steigern kann, wenn lediglich ein einfacher State-Management-Service benötigt wird.
    • Normale Services bieten eine einfachere und direktere Struktur ohne zusätzliche Abstraktionen und können oft den gleichen Zweck erfüllen, insbesondere in kleineren Projekten.
  4. Eingeschränkter Scope:

    • ComponentStore ist für spezifische Komponenten konzipiert, und es ist in vielen Fällen nicht praktikabel, ihn für übergreifende, globale Zustände zu verwenden. Für das Management von globalem Status ist der ComponentStore weniger geeignet als zentralisierte Ansätze wie @ngrx/store.
    • Normale Services sind ideal für übergreifende Anwendungszustände, da sie einfach als Singleton Services konfiguriert werden können und in jeder Komponente verfügbar sind.
  5. Abhängigkeit von der NgRx-Bibliothek:

    • ComponentStore ist Teil von NgRx, was bedeutet, dass die Bibliothek in das Projekt integriert werden muss. Das kann die Projektgröße erhöhen und zusätzliche Abhängigkeiten schaffen.
    • Normale Services sind nativ in Angular und benötigen keine zusätzlichen Bibliotheken, was sie leichtgewichtiger macht und die Projekteinrichtung vereinfacht.

Wann ist ComponentStore sinnvoll?

Die Verwendung von ComponentStore eignet sich besonders für:

  • Komplexe Komponenten, die isoliert verwaltet werden sollen.
  • Anwendungen mit intensivem State-Management auf Komponentenebene, bei denen reaktive Programmierung Vorteile bringt.
  • Komponenten mit lokaler API-Interaktion, bei der asynchrone Daten geladen und im State verarbeitet werden müssen.

Wann sind normale Services sinnvoller?

Normale Services eignen sich besser für:

  • Einfaches State-Management ohne aufwendige Logik, wie das Zwischenspeichern von Werten oder das Teilen von Daten zwischen Komponenten.
  • Globale Anwendungszustände, die in mehreren Bereichen der Anwendung verfügbar sein müssen.
  • Kleine bis mittelgroße Anwendungen, in denen komplexes State-Management nicht notwendig ist.

Fazit

Angulars ComponentStore bietet eine elegante und modularisierte Alternative zu normalen Services, insbesondere für größere und komplexere UI-Komponenten. Bei der Entscheidung zwischen ComponentStore und Services hängt die Wahl stark von der Komplexität der Anwendung, den reaktiven Anforderungen und der Architektur ab.

Kategorien

Kontaktieren Sie uns:

Harksheider Weg 60H,
25451 Quickborn
+49 1520 40 73 253
info@gk-itsolutions.de

Schnellzugriff:

Folgt uns auf: