23 State Management in Angular

23.1 Einführung

In modernen Angular-Anwendungen ist das effektive Verwalten des Zustands (State) einer der kritischsten Aspekte für die Entwicklung skalierbarer und wartbarer Applikationen. Der Zustand einer Anwendung umfasst alle Daten, die zu einem bestimmten Zeitpunkt in der Applikation präsent sind, wie:

Mit wachsender Komplexität einer Anwendung steigt auch die Herausforderung, diesen Zustand effizient zu verwalten. Angular bietet verschiedene Ansätze zum State Management, die von einfachen Lösungen bis hin zu komplexen Architekturen reichen.

23.2 State Management Konzepte

Bevor wir in die spezifischen Implementierungen eintauchen, sollten wir einige grundlegende Konzepte des State Managements verstehen:

23.2.1 Single Source of Truth

Der gesamte Anwendungszustand sollte an einem einzigen Ort gespeichert werden, was Inkonsistenzen vermeidet und das Debugging erleichtert.

23.2.2 Unveränderlichkeit (Immutability)

Zustände sollten niemals direkt mutiert werden. Stattdessen sollten neue Zustandsobjekte erstellt werden, was die Vorhersehbarkeit erhöht und Side-Effects reduziert.

23.2.3 Unidirektionaler Datenfluss

Daten sollten in einer Richtung fließen, was die Nachvollziehbarkeit von Änderungen verbessert und die Anwendung leichter testbar macht.

23.3 State Management Lösungen in Angular

23.3.1 Komponentenbasiertes State Management

Die einfachste Form des State Managements in Angular ist die Verwendung von Komponenten-Eigenschaften und @Input/@Output-Dekoratoren.

23.3.1.1 Beispiel: Parent-Child Kommunikation

// parent.component.ts
@Component({
   selector: 'app-parent',
   template: `
    <h1>Elternkomponente</h1>
    <app-child [items]="items" (itemAdded)="onItemAdded($event)"></app-child>
    <div>Gesamtanzahl: {{ items.length }}</div>
  `
})
export class ParentComponent {
   items: string[] = ['Item 1', 'Item 2'];

   onItemAdded(newItem: string): void {
      this.items = [...this.items, newItem]; // Immutabilität beachten
   }
}

// child.component.ts
@Component({
   selector: 'app-child',
   template: `
    <h2>Kindkomponente</h2>
    <ul>
      <li *ngFor="let item of items">{{ item }}</li>
    </ul>
    <input #newItem />
    <button (click)="addItem(newItem.value); newItem.value = ''">Hinzufügen</button>
  `
})
export class ChildComponent {
   @Input() items: string[] = [];
   @Output() itemAdded = new EventEmitter<string>();

   addItem(item: string): void {
      if (item) {
         this.itemAdded.emit(item);
      }
   }
}

Diese Methode funktioniert gut für einfache Anwendungen, stößt aber bei komplexeren Szenarien schnell an ihre Grenzen.

23.3.2 Services als State Container

Für mittlere Komplexität können Angular-Services als State Container verwendet werden.

23.3.2.1 Beispiel: Service mit RxJS BehaviorSubject

// data.service.ts
@Injectable({
   providedIn: 'root'
})
export class DataService {
   private itemsSubject = new BehaviorSubject<string[]>(['Item 1', 'Item 2']);
   items$ = this.itemsSubject.asObservable();

   addItem(item: string): void {
      const currentItems = this.itemsSubject.getValue();
      this.itemsSubject.next([...currentItems, item]);
   }

   removeItem(index: number): void {
      const currentItems = this.itemsSubject.getValue();
      const updatedItems = [...currentItems];
      updatedItems.splice(index, 1);
      this.itemsSubject.next(updatedItems);
   }
}

// list.component.ts
@Component({
   selector: 'app-list',
   template: `
    <h2>Items</h2>
    <ul>
      <li *ngFor="let item of items$ | async; let i = index">
        {{ item }}
        <button (click)="removeItem(i)">Entfernen</button>
      </li>
    </ul>
  `
})
export class ListComponent implements OnInit {
   items$: Observable<string[]>;

   constructor(private dataService: DataService) {}

   ngOnInit(): void {
      this.items$ = this.dataService.items$;
   }

   removeItem(index: number): void {
      this.dataService.removeItem(index);
   }
}

// add-item.component.ts
@Component({
   selector: 'app-add-item',
   template: `
    <input #newItem />
    <button (click)="addItem(newItem.value); newItem.value = ''">Hinzufügen</button>
  `
})
export class AddItemComponent {
   constructor(private dataService: DataService) {}

   addItem(item: string): void {
      if (item) {
         this.dataService.addItem(item);
      }
   }
}

Diese Methode skaliert besser als die rein komponentenbasierte Lösung, kann aber bei sehr komplexen Anwendungen unübersichtlich werden.

23.3.3 NgRx - Redux-Pattern für Angular

Für große und komplexe Anwendungen ist NgRx die empfohlene Lösung. NgRx implementiert das Redux-Pattern und nutzt RxJS, um einen vorhersehbaren State Container zu bieten.

23.3.3.1 Hauptbestandteile von NgRx:

23.3.3.2 Erweitertes NgRx Beispiel:

// todo.model.ts
export interface Todo {
   id: string;
   title: string;
   completed: boolean;
}

export interface TodoState {
   todos: Todo[];
   loading: boolean;
   error: string | null;
}

export const initialTodoState: TodoState = {
   todos: [],
   loading: false,
   error: null
};

// todo.actions.ts
import { createAction, props } from '@ngrx/store';
import { Todo } from './todo.model';

export const loadTodos = createAction('[Todo] Load Todos');
export const loadTodosSuccess = createAction(
        '[Todo] Load Todos Success',
        props<{ todos: Todo[] }>()
);
export const loadTodosFailure = createAction(
        '[Todo] Load Todos Failure',
        props<{ error: string }>()
);

export const addTodo = createAction(
        '[Todo] Add Todo',
        props<{ title: string }>()
);
export const addTodoSuccess = createAction(
        '[Todo] Add Todo Success',
        props<{ todo: Todo }>()
);
export const addTodoFailure = createAction(
        '[Todo] Add Todo Failure',
        props<{ error: string }>()
);

export const toggleTodo = createAction(
        '[Todo] Toggle Todo',
        props<{ id: string }>()
);

export const removeTodo = createAction(
        '[Todo] Remove Todo',
        props<{ id: string }>()
);

// todo.reducer.ts
import { createReducer, on } from '@ngrx/store';
import * as TodoActions from './todo.actions';
import { initialTodoState, TodoState } from './todo.model';

export const todoReducer = createReducer(
        initialTodoState,
        on(TodoActions.loadTodos, state => ({
           ...state,
           loading: true,
           error: null
        })),
        on(TodoActions.loadTodosSuccess, (state, { todos }) => ({
           ...state,
           todos,
           loading: false
        })),
        on(TodoActions.loadTodosFailure, (state, { error }) => ({
           ...state,
           loading: false,
           error
        })),
        on(TodoActions.addTodoSuccess, (state, { todo }) => ({
           ...state,
           todos: [...state.todos, todo]
        })),
        on(TodoActions.toggleTodo, (state, { id }) => ({
           ...state,
           todos: state.todos.map(todo =>
                   todo.id === id ? { ...todo, completed: !todo.completed } : todo
           )
        })),
        on(TodoActions.removeTodo, (state, { id }) => ({
           ...state,
           todos: state.todos.filter(todo => todo.id !== id)
        }))
);

// todo.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { TodoState } from './todo.model';

export const selectTodoState = createFeatureSelector<TodoState>('todos');

export const selectAllTodos = createSelector(
        selectTodoState,
        (state: TodoState) => state.todos
);

export const selectCompletedTodos = createSelector(
        selectAllTodos,
        todos => todos.filter(todo => todo.completed)
);

export const selectIncompleteTodos = createSelector(
        selectAllTodos,
        todos => todos.filter(todo => !todo.completed)
);

export const selectTodosLoading = createSelector(
        selectTodoState,
        (state: TodoState) => state.loading
);

export const selectTodosError = createSelector(
        selectTodoState,
        (state: TodoState) => state.error
);

// todo.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
import * as TodoActions from './todo.actions';
import { TodoService } from './todo.service';

@Injectable()
export class TodoEffects {
   loadTodos$ = createEffect(() =>
           this.actions$.pipe(
                   ofType(TodoActions.loadTodos),
                   switchMap(() =>
                           this.todoService.getTodos().pipe(
                                   map(todos => TodoActions.loadTodosSuccess({ todos })),
                                   catchError(error =>
                                           of(TodoActions.loadTodosFailure({ error: error.message }))
                                   )
                           )
                   )
           )
   );

   addTodo$ = createEffect(() =>
           this.actions$.pipe(
                   ofType(TodoActions.addTodo),
                   mergeMap(({ title }) =>
                           this.todoService.addTodo(title).pipe(
                                   map(todo => TodoActions.addTodoSuccess({ todo })),
                                   catchError(error =>
                                           of(TodoActions.addTodoFailure({ error: error.message }))
                                   )
                           )
                   )
           )
   );

   constructor(
           private actions$: Actions,
           private todoService: TodoService
   ) {}
}

// todo.service.ts
@Injectable({
   providedIn: 'root'
})
export class TodoService {
   private apiUrl = 'api/todos';

   constructor(private http: HttpClient) {}

   getTodos(): Observable<Todo[]> {
      return this.http.get<Todo[]>(this.apiUrl);
   }

   addTodo(title: string): Observable<Todo> {
      const todo: Partial<Todo> = {
         title,
         completed: false
      };
      return this.http.post<Todo>(this.apiUrl, todo);
   }

   // Weitere Methoden für update, delete, etc.
}

// todo-list.component.ts
@Component({
   selector: 'app-todo-list',
   template: `
    <div *ngIf="loading$ | async">Laden...</div>
    <div *ngIf="error$ | async as error" class="error">{{ error }}</div>
    
    <app-todo-form></app-todo-form>
    
    <h2>Offene Aufgaben</h2>
    <ul>
      <li *ngFor="let todo of incompleteTodos$ | async">
        <input type="checkbox" [checked]="todo.completed" (change)="toggleTodo(todo.id)">
        {{ todo.title }}
        <button (click)="removeTodo(todo.id)">Löschen</button>
      </li>
    </ul>
    
    <h2>Erledigte Aufgaben</h2>
    <ul>
      <li *ngFor="let todo of completedTodos$ | async">
        <input type="checkbox" [checked]="todo.completed" (change)="toggleTodo(todo.id)">
        <span class="completed">{{ todo.title }}</span>
        <button (click)="removeTodo(todo.id)">Löschen</button>
      </li>
    </ul>
  `,
   styles: [
      `.completed { text-decoration: line-through; }`,
      `.error { color: red; }`
   ]
})
export class TodoListComponent implements OnInit {
   completedTodos$: Observable<Todo[]>;
   incompleteTodos$: Observable<Todo[]>;
   loading$: Observable<boolean>;
   error$: Observable<string | null>;

   constructor(private store: Store) {}

   ngOnInit(): void {
      this.store.dispatch(TodoActions.loadTodos());

      this.completedTodos$ = this.store.select(selectCompletedTodos);
      this.incompleteTodos$ = this.store.select(selectIncompleteTodos);
      this.loading$ = this.store.select(selectTodosLoading);
      this.error$ = this.store.select(selectTodosError);
   }

   toggleTodo(id: string): void {
      this.store.dispatch(TodoActions.toggleTodo({ id }));
   }

   removeTodo(id: string): void {
      this.store.dispatch(TodoActions.removeTodo({ id }));
   }
}

// todo-form.component.ts
@Component({
   selector: 'app-todo-form',
   template: `
    <form [formGroup]="todoForm" (ngSubmit)="onSubmit()">
      <input formControlName="title" placeholder="Neue Aufgabe..." />
      <button type="submit" [disabled]="todoForm.invalid">Hinzufügen</button>
    </form>
  `
})
export class TodoFormComponent implements OnInit {
   todoForm: FormGroup;

   constructor(
           private fb: FormBuilder,
           private store: Store
   ) {}

   ngOnInit(): void {
      this.todoForm = this.fb.group({
         title: ['', [Validators.required, Validators.minLength(3)]]
      });
   }

   onSubmit(): void {
      if (this.todoForm.valid) {
         const { title } = this.todoForm.value;
         this.store.dispatch(TodoActions.addTodo({ title }));
         this.todoForm.reset();
      }
   }
}

Dieses erweiterte NgRx-Beispiel zeigt eine realistische Implementierung mit Fehlermanagement, Ladezuständen und API-Integration.

23.3.4 NgRx Component Store (ab Angular 11)

Mit der Einführung von NgRx Component Store wurde eine leichtgewichtigere Alternative zum vollen NgRx Store angeboten. Dies eignet sich besonders für isolierte Zustandsverwaltung auf Komponentenebene.

23.3.4.1 Beispiel:

// counter.store.ts
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';

interface CounterState {
   count: number;
}

@Injectable()
export class CounterStore extends ComponentStore<CounterState> {
   constructor() {
      super({ count: 0 });
   }

   // Selectors
   readonly count$ = this.select(state => state.count);
   readonly doubleCount$ = this.select(this.count$, count => count * 2);

   // Updaters
   readonly increment = this.updater((state) => ({
      count: state.count + 1
   }));

   readonly decrement = this.updater((state) => ({
      count: state.count - 1
   }));

   readonly reset = this.updater((state) => ({
      count: 0
   }));

   // Effects
   readonly incrementBy = this.effect<number>((amount$) => {
      return amount$.pipe(
              tap((amount) => {
                 this.patchState((state) => ({
                    count: state.count + amount
                 }));
              })
      );
   });
}

// counter.component.ts
@Component({
   selector: 'app-counter',
   template: `
    <h2>Zähler: {{ count$ | async }}</h2>
    <p>Doppelter Wert: {{ doubleCount$ | async }}</p>
    <button (click)="increment()">+</button>
    <button (click)="decrement()">-</button>
    <button (click)="reset()">Reset</button>
    <button (click)="incrementBy5()">+5</button>
  `,
   providers: [CounterStore]
})
export class CounterComponent {
   readonly count$ = this.store.count$;
   readonly doubleCount$ = this.store.doubleCount$;

   constructor(private store: CounterStore) {}

   increment(): void {
      this.store.increment();
   }

   decrement(): void {
      this.store.decrement();
   }

   reset(): void {
      this.store.reset();
   }

   incrementBy5(): void {
      this.store.incrementBy(5);
   }
}

23.3.5 Signals in Angular für reaktives State Management

Mit neueren Angular-Versionen wurden Signals eingeführt, was eine neue Möglichkeit für reaktives State Management bietet.

23.3.5.1 Beispiel mit Signals:

// todo.service.ts
@Injectable({
  providedIn: 'root'
})
export class TodoSignalService {
  private todosSignal = signal<Todo[]>([]);
  private loadingSignal = signal<boolean>(false);
  private errorSignal = signal<string | null>(null);

  // Computed Signals
  readonly completedTodos = computed(() => 
    this.todosSignal().filter(todo => todo.completed)
  );
  
  readonly incompleteTodos = computed(() => 
    this.todosSignal().filter(todo => !todo.completed)
  );
  
  readonly totalTodos = computed(() => 
    this.todosSignal().length
  );

  constructor(private http: HttpClient) {}

  // Getter für die Signals
  get todos() { return this.todosSignal; }
  get loading() { return this.loadingSignal; }
  get error() { return this.errorSignal; }

  // Methoden, die den State aktualisieren
  loadTodos(): void {
    this.loadingSignal.set(true);
    this.errorSignal.set(null);
    
    this.http.get<Todo[]>('api/todos').pipe(
      finalize(() => this.loadingSignal.set(false))
    ).subscribe({
      next: (todos) => this.todosSignal.set(todos),
      error: (err) => this.errorSignal.set(err.message)
    });
  }

  addTodo(title: string): void {
    const newTodo: Todo = {
      id: Date.now().toString(),
      title,
      completed: false
    };
    
    this.todosSignal.update(todos => [...todos, newTodo]);
  }

  toggleTodo(id: string): void {
    this.todosSignal.update(todos => 
      todos.map(todo => 
        todo.id === id 
          ? { ...todo, completed: !todo.completed } 
          : todo
      )
    );
  }

  removeTodo(id: string): void {
    this.todosSignal.update(todos => 
      todos.filter(todo => todo.id !== id)
    );
  }
}

// todo-list.component.ts (mit Signals)
@Component({
  selector: 'app-todo-signal-list',
  template: `
    <div *ngIf="todoService.loading()">Laden...</div>
    <div *ngIf="todoService.error()" class="error">{{ todoService.error() }}</div>
    
    <form (submit)="addTodo(); $event.preventDefault()">
      <input [(ngModel)]="newTodoTitle" name="title" placeholder="Neue Aufgabe..." required />
      <button type="submit" [disabled]="!newTodoTitle">Hinzufügen</button>
    </form>
    
    <h2>Offene Aufgaben ({{ todoService.incompleteTodos().length }})</h2>
    <ul>
      <li *ngFor="let todo of todoService.incompleteTodos()">
        <input type="checkbox" [checked]="todo.completed" (change)="todoService.toggleTodo(todo.id)">
        {{ todo.title }}
        <button (click)="todoService.removeTodo(todo.id)">Löschen</button>
      </li>
    </ul>
    
    <h2>Erledigte Aufgaben ({{ todoService.completedTodos().length }})</h2>
    <ul>
      <li *ngFor="let todo of todoService.completedTodos()">
        <input type="checkbox" [checked]="todo.completed" (change)="todoService.toggleTodo(todo.id)">
        <span class="completed">{{ todo.title }}</span>
        <button (click)="todoService.removeTodo(todo.id)">Löschen</button>
      </li>
    </ul>
  `,
  styles: [
    `.completed { text-decoration: line-through; }`,
    `.error { color: red; }`
  ]
})
export class TodoSignalListComponent implements OnInit {
  newTodoTitle = '';

  constructor(public todoService: TodoSignalService) {}

  ngOnInit(): void {
    this.todoService.loadTodos();
  }

  addTodo(): void {
    if (this.newTodoTitle.trim()) {
      this.todoService.addTodo(this.newTodoTitle);
      this.newTodoTitle = '';
    }
  }
}

23.4 RxState - Ein leichtgewichtiger State Management Ansatz

RxState ist eine weitere sehr leichtgewichtige Alternative zum State Management, die direkt auf RxJS aufbaut.

// counter.state.ts
import { Injectable } from '@angular/core';
import { RxState } from '@rx-angular/state';
import { Subject } from 'rxjs';
import { map } from 'rxjs/operators';

export interface CounterState {
  count: number;
}

const initialState: CounterState = {
  count: 0
};

@Injectable()
export class CounterState extends RxState<CounterState> {
  // UI Events als Subjects
  increment$ = new Subject<void>();
  decrement$ = new Subject<void>();
  reset$ = new Subject<void>();
  add$ = new Subject<number>();

  // Select-Methoden
  readonly count$ = this.select('count');
  readonly doubleCount$ = this.select('count').pipe(
    map(count => count * 2)
  );

  constructor() {
    super();
    this.set(initialState);

    // Connect-Methode für Event-Handling
    this.connect(this.increment$, state => ({
      count: state.count + 1
    }));

    this.connect(this.decrement$, state => ({
      count: state.count - 1
    }));

    this.connect(this.reset$, _ => initialState);

    this.connect(this.add$, (state, amount) => ({
      count: state.count + amount
    }));
  }
}

// counter.component.ts
@Component({
  selector: 'app-rx-counter',
  template: `
    <h2>RxState Zähler: {{ vm.count }}</h2>
    <p>Doppelter Wert: {{ vm.doubleCount }}</p>
    <button (click)="state.increment$.next()">+</button>
    <button (click)="state.decrement$.next()">-</button>
    <button (click)="state.reset$.next()">Reset</button>
    <button (click)="state.add$.next(5)">+5</button>
  `,
  providers: [CounterState]
})
export class RxCounterComponent {
  readonly vm$ = this.state.select(
    this.state.count$,
    this.state.doubleCount$,
    (count, doubleCount) => ({ count, doubleCount })
  );

  constructor(public state: CounterState) {}
}

23.5 Asynchrones State Management

Eine der größten Herausforderungen im State Management ist der Umgang mit asynchronen Operationen wie API-Calls, WebSocket-Verbindungen oder anderen zeitabhängigen Prozessen. Angular bietet verschiedene Möglichkeiten, um asynchronen State effektiv zu verwalten:

23.5.1 Asynchrones State Management mit RxJS

RxJS ist tief in Angular integriert und bietet leistungsstarke Operatoren zum Umgang mit asynchronen Datenströmen:

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private userSubject = new BehaviorSubject<User | null>(null);
  private loadingSubject = new BehaviorSubject<boolean>(false);
  private errorSubject = new BehaviorSubject<string | null>(null);
  
  // Öffentliche Observables
  readonly user$ = this.userSubject.asObservable();
  readonly loading$ = this.loadingSubject.asObservable();
  readonly error$ = this.errorSubject.asObservable();
  
  // Kombinierter State mit allen Informationen
  readonly userState$ = combineLatest([
    this.user$,
    this.loading$,
    this.error$
  ]).pipe(
    map(([user, loading, error]) => ({ user, loading, error }))
  );
  
  constructor(private http: HttpClient) {}
  
  loadUser(id: string): Observable<User> {
    // State-Aktualisierung initiieren
    this.loadingSubject.next(true);
    this.errorSubject.next(null);
    
    return this.http.get<User>(`/api/users/${id}`).pipe(
      tap(user => {
        // State nach erfolgreicher Anfrage aktualisieren
        this.userSubject.next(user);
        this.loadingSubject.next(false);
      }),
      catchError(error => {
        // Fehlerzustand verwalten
        this.errorSubject.next(error.message);
        this.loadingSubject.next(false);
        return throwError(() => error); // Re-throw für den Aufrufer
      }),
      // Starte bei 0, versuche bis zu 3 mal alle 2 Sekunden
      retry({ count: 3, delay: 2000 }),
      // Teile den Datenstrom, so dass mehrere Subscriber dieselbe Anfrage nutzen
      shareReplay(1)
    );
  }
  
  // Abbrechen von laufenden Anfragen mit SwitchMap
  private searchTerms = new Subject<string>();
  readonly searchResults$ = this.searchTerms.pipe(
    // Warte 300ms nach der letzten Eingabe
    debounceTime(300),
    // Ignoriere, wenn sich der Term nicht geändert hat
    distinctUntilChanged(),
    // Setze loading-State
    tap(() => this.loadingSubject.next(true)),
    // Breche vorherige Anfragen ab, wenn neue eintreffen
    switchMap(term => 
      this.http.get<User[]>(`/api/users?search=${term}`).pipe(
        tap(() => this.loadingSubject.next(false)),
        catchError(error => {
          this.loadingSubject.next(false);
          this.errorSubject.next(error.message);
          return of([]); // Leeres Array bei Fehlern zurückgeben
        })
      )
    )
  );
  
  search(term: string): void {
    this.searchTerms.next(term);
  }
}

23.5.2 Asynchrones State Management mit NgRx Effects

NgRx Effects sind speziell dafür konzipiert, Side-Effects wie asynchrone Operationen zu verwalten:

@Injectable()
export class UserEffects {
  loadUser$ = createEffect(() => 
    this.actions$.pipe(
      ofType(UserActions.loadUser),
      exhaustMap(({ id }) => 
        this.userService.getUser(id).pipe(
          map(user => UserActions.loadUserSuccess({ user })),
          catchError(error => of(UserActions.loadUserFailure({ error: error.message })))
        )
      )
    )
  );
  
  // Automatisches Neuladen bei Netzwerkwiederherstellung
  retryOnReconnect$ = createEffect(() =>
    this.networkStatus.reconnected$.pipe(
      withLatestFrom(this.store.select(selectCurrentUserId)),
      filter(([_, userId]) => !!userId),
      map(([_, userId]) => UserActions.loadUser({ id: userId }))
    )
  );
  
  // Optimistic Updates mit Rollback
  updateUser$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UserActions.updateUser),
      // Speichere den vorherigen Zustand für möglichen Rollback
      concatMap(({ user, previousUser }) =>
        this.userService.updateUser(user).pipe(
          map(() => UserActions.updateUserSuccess({ user })),
          catchError(error => of(UserActions.updateUserFailure({ 
            error: error.message,
            // Rollback-Informationen
            rollback: { user: previousUser }
          })))
        )
      )
    )
  );
  
  // Rollback bei Fehler
  handleUpdateFailure$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UserActions.updateUserFailure),
      map(({ rollback }) => UserActions.rollbackUserUpdate({ user: rollback.user }))
    )
  );
  
  constructor(
    private actions$: Actions,
    private userService: UserService,
    private store: Store,
    private networkStatus: NetworkStatusService
  ) {}
}

23.5.3 Asynchrones State Management mit Signals

Mit neueren Angular-Versionen können Signals für asynchrones State Management verwendet werden:

@Injectable({
  providedIn: 'root'
})
export class AsyncSignalService {
  // State Signals
  private usersSignal = signal<User[]>([]);
  private loadingSignal = signal<boolean>(false);
  private errorSignal = signal<string | null>(null);
  
  // Getter für die Signals
  readonly users = this.usersSignal.asReadonly();
  readonly loading = this.loadingSignal.asReadonly();
  readonly error = this.errorSignal.asReadonly();
  
  // Abgeleitete Signals
  readonly hasUsers = computed(() => this.users().length > 0);
  readonly activeUsers = computed(() => 
    this.users().filter(user => user.isActive)
  );
  
  constructor(private http: HttpClient) {}
  
  // Asynchrones Laden mit Signals
  loadUsers(): void {
    // Update loading state
    this.loadingSignal.set(true);
    this.errorSignal.set(null);
    
    // Perform HTTP request
    this.http.get<User[]>('/api/users').pipe(
      finalize(() => this.loadingSignal.set(false))
    ).subscribe({
      next: (users) => {
        this.usersSignal.set(users);
      },
      error: (err) => {
        this.errorSignal.set(err.message);
      }
    });
  }
  
  // Verbindung von Signals zu Observables
  setupPolling(): void {
    // Polling alle 30 Sekunden
    interval(30000).pipe(
      startWith(0), // Sofort starten
      switchMap(() => {
        this.loadingSignal.update(loading => loading); // Loading-State nicht ändern
        return this.http.get<User[]>('/api/users');
      }),
      catchError(err => {
        this.errorSignal.set(err.message);
        return EMPTY; // Polling fortsetzen
      })
    ).subscribe(users => {
      // Aktualisiere den State nur, wenn sich etwas geändert hat
      const currentUsers = this.usersSignal();
      if (!isEqual(currentUsers, users)) {
        this.usersSignal.set(users);
      }
    });
  }
}

23.5.4 Caching und Invalidierung asynchroner Daten

Ein wichtiger Aspekt des asynchronen State Managements ist die Implementierung effektiver Caching-Strategien:

@Injectable({
  providedIn: 'root'
})
export class CachingService {
  private cache = new Map<string, {
    data: any,
    timestamp: number,
    expiresIn: number
  }>();
  
  getData<T>(url: string, options?: { 
    forceRefresh?: boolean, 
    expiresIn?: number  // in milliseconds
  }): Observable<T> {
    const cacheKey = url;
    const expiresIn = options?.expiresIn || 5 * 60 * 1000; // Default: 5 Minuten
    
    // Prüfe, ob gültiger Cache-Eintrag existiert
    if (!options?.forceRefresh && this.hasValidCacheEntry(cacheKey, expiresIn)) {
      const cachedData = this.cache.get(cacheKey)!.data;
      return of(cachedData).pipe(delay(0)); // Async-Operation simulieren
    }
    
    // Keine gültigen Daten im Cache, von API laden
    return this.http.get<T>(url).pipe(
      tap(data => {
        this.cache.set(cacheKey, {
          data,
          timestamp: Date.now(),
          expiresIn
        });
      }),
      // Fallback zu gecachten Daten bei Netzwerkfehlern, falls vorhanden
      catchError(error => {
        if (this.cache.has(cacheKey)) {
          return of(this.cache.get(cacheKey)!.data);
        }
        return throwError(() => error);
      })
    );
  }
  
  private hasValidCacheEntry(key: string, maxAge: number): boolean {
    if (!this.cache.has(key)) return false;
    
    const entry = this.cache.get(key)!;
    const ageInMs = Date.now() - entry.timestamp;
    return ageInMs < maxAge;
  }
  
  // Cache-Eintrag ungültig machen
  invalidate(url: string): void {
    this.cache.delete(url);
  }
  
  // Alle Cache-Einträge mit einem bestimmten Präfix ungültig machen
  invalidateByPrefix(prefix: string): void {
    const keysToDelete: string[] = [];
    this.cache.forEach((_, key) => {
      if (key.startsWith(prefix)) {
        keysToDelete.push(key);
      }
    });
    keysToDelete.forEach(key => this.cache.delete(key));
  }
}

23.5.5 Strategien für die Optimierung der Leistung bei asynchronem State

  1. Optimistic Updates: Aktualisiere den UI-State sofort, bevor der API-Call abgeschlossen ist, und führe einen Rollback durch, wenn der Call fehlschlägt.

  2. Debounce und Throttle: Vermeide übermäßige API-Calls durch Debouncing von Benutzereingaben.

  3. Pagination und Lazy Loading: Lade nur die benötigten Daten, anstatt alle auf einmal.

  4. Caching-Strategien: Implementiere verschiedene Caching-Ansätze basierend auf der Art der Daten (TTL-basiert, Invalidierung bei Aktionen).

  5. Zustandsübergänge visualisieren: Biete klares Feedback zum Ladezustand, um die wahrgenommene Leistung zu verbessern.

23.6 Richtlinien zur Auswahl des richtigen State Management Ansatzes

Ansatz Anwendungsfall Vorteile Nachteile
Komponentenbasiert Kleine Anwendungen, Prototypen Einfach, nativ in Angular Skaliert schlecht, Prop-Drilling-Problem
Services mit BehaviorSubject Mittlere Anwendungen, Feature-Module Mittlere Komplexität, gute Angular-Integration Begrenzte Debugging-Möglichkeiten
NgRx (Redux) Große, komplexe Anwendungen Vollständige Kontrolle, Debugging, Vorhersehbarkeit Steile Lernkurve, viel Boilerplate-Code
NgRx Component Store Isolierte Features, mittlere Komplexität Weniger Boilerplate als NgRx, gute Isolation Nicht für anwendungsweiten State geeignet
Signals Moderne Angular-Anwendungen Reaktiv, wenig Boilerplate, gute Performance Relativ neu, weniger Ökosystem-Tools
RxState Mittlere Anwendungen mit RxJS-Fokus Leichtgewichtig, RxJS-zentriert Weniger Community-Support als NgRx

23.7 Best Practices

  1. Wähle die richtige Lösung für die Komplexität deiner Anwendung: Nicht jede Anwendung benötigt NgRx. Für kleinere Anwendungen können Services oder Component Store ausreichen.

  2. Entwickle ein klares State-Modell: Definiere klar, wie dein State strukturiert sein soll, bevor du mit der Implementierung beginnst.

  3. Halte die Komponenten dumm (Presentational Components): Komponenten sollten idealerweise nur Daten anzeigen und Events auslösen, ohne Logik.

  4. Verwende Selektoren für abgeleitete Daten: Statt Daten in Komponenten zu transformieren, nutze Selektoren oder computed-Eigenschaften.

  5. Achte auf Immutabilität: Ändere niemals direkt den State, sondern erstelle immer neue State-Objekte.

  6. Teste deinen State: Schreibe Unit-Tests für Reducer, Effects und Selektoren.

  7. Nutze Development Tools: Verwende Tools wie Redux DevTools für NgRx oder RxJS DevTools für reaktives Debugging.

State Management in Angular hat sich über die Versionen hinweg stark weiterentwickelt. Von einfachen Services bis hin zu ausgefeilten Lösungen wie NgRx und den neueren Signals - es gibt für jede Anforderung die passende Lösung.

Das reaktive State Management durch Signals bietet eine attraktive Alternative zu traditionellen Ansätzen. Die Wahl des richtigen State Management-Ansatzes sollte immer von der Komplexität der Anwendung, den Teamfähigkeiten und den spezifischen Anforderungen abhängen.