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.
Bevor wir in die spezifischen Implementierungen eintauchen, sollten wir einige grundlegende Konzepte des State Managements verstehen:
Der gesamte Anwendungszustand sollte an einem einzigen Ort gespeichert werden, was Inkonsistenzen vermeidet und das Debugging erleichtert.
Zustände sollten niemals direkt mutiert werden. Stattdessen sollten neue Zustandsobjekte erstellt werden, was die Vorhersehbarkeit erhöht und Side-Effects reduziert.
Daten sollten in einer Richtung fließen, was die Nachvollziehbarkeit von Änderungen verbessert und die Anwendung leichter testbar macht.
Die einfachste Form des State Managements in Angular ist die
Verwendung von Komponenten-Eigenschaften und
@Input/@Output-Dekoratoren.
// 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.
Für mittlere Komplexität können Angular-Services als State Container verwendet werden.
// 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.
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.
// 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.
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.
// 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);
}
}Mit neueren Angular-Versionen wurden Signals eingeführt, was eine neue Möglichkeit für reaktives State Management bietet.
// 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 = '';
}
}
}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) {}
}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:
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);
}
}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
) {}
}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);
}
});
}
}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));
}
}Optimistic Updates: Aktualisiere den UI-State sofort, bevor der API-Call abgeschlossen ist, und führe einen Rollback durch, wenn der Call fehlschlägt.
Debounce und Throttle: Vermeide übermäßige API-Calls durch Debouncing von Benutzereingaben.
Pagination und Lazy Loading: Lade nur die benötigten Daten, anstatt alle auf einmal.
Caching-Strategien: Implementiere verschiedene Caching-Ansätze basierend auf der Art der Daten (TTL-basiert, Invalidierung bei Aktionen).
Zustandsübergänge visualisieren: Biete klares Feedback zum Ladezustand, um die wahrgenommene Leistung zu verbessern.
| 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 |
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.
Entwickle ein klares State-Modell: Definiere klar, wie dein State strukturiert sein soll, bevor du mit der Implementierung beginnst.
Halte die Komponenten dumm (Presentational Components): Komponenten sollten idealerweise nur Daten anzeigen und Events auslösen, ohne Logik.
Verwende Selektoren für abgeleitete Daten: Statt Daten in Komponenten zu transformieren, nutze Selektoren oder computed-Eigenschaften.
Achte auf Immutabilität: Ändere niemals direkt den State, sondern erstelle immer neue State-Objekte.
Teste deinen State: Schreibe Unit-Tests für Reducer, Effects und Selektoren.
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.