Zum Inhalt springen

Signal Store live

Ein vollständiges, kommentiertes Signal-Store-Beispiel aus der Tasks-Domäne.

tasks.store.ts
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { computed, inject } from '@angular/core';
import { TasksService } from '../infrastructure/tasks.service';
type TasksState = {
tasks: Task[];
selectedTaskId: string | null;
status: 'idle' | 'loading' | 'success' | 'error';
};
const initialState: TasksState = {
tasks: [],
selectedTaskId: null,
status: 'idle',
};
export const TasksStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ tasks, selectedTaskId }) => ({
selectedTask: computed(() => tasks().find((t) => t.id === selectedTaskId()) ?? null),
openTasksCount: computed(() => tasks().filter((t) => t.status === 'open').length),
})),
withMethods((store) => {
const service = inject(TasksService);
return {
async loadAll() {
patchState(store, { status: 'loading' });
try {
const tasks = await service.getAll();
patchState(store, { tasks, status: 'success' });
} catch {
patchState(store, { status: 'error' });
}
},
selectTask(id: string) {
patchState(store, { selectedTaskId: id });
},
};
}),
);
tasks.facade.ts
@Injectable({ providedIn: 'root' })
export class TasksFacade {
private store = inject(TasksStore);
readonly tasks = this.store.tasks;
readonly selectedTask = this.store.selectedTask;
readonly isLoading = computed(() => this.store.status() === 'loading');
loadAll() {
this.store.loadAll();
}
selectTask(id: string) {
this.store.selectTask(id);
}
}
@Component({
template: `
@if (facade.isLoading()) {
<p>Lädt…</p>
}
<app-task-list [tasks]="facade.tasks()" (select)="facade.selectTask($event)" />
`,
})
export class TasksContainerComponent {
protected facade = inject(TasksFacade);
}

Store kapselt Zustand. Facade kapselt Store. Komponente kennt nur die Facade.