Share
Embed
Mastering State Management in Angular: Demystifying NgRx, the Signal Store, and Events Plugin
Fabian Gosebrink
October 29, 2026
Transcript
139 slides
-
1 2 3 4 5 6 7 todos = signal<Todo[]>([]);
todos(); todos.set(...); todos.update((items) => { /* ... */ }); -
1 2 3 4 5 6 7 todos = signal<Todo[]>([]);
todos(); todos.set(...); todos.update((items) => { /* ... */ }); -
1 2 3 4 5 6 7 todos = signal<Todo[]>([]);
todos(); todos.set(...); todos.update((items) => { /* ... */ }); -
1 2 3 4 5 6 7 todos = signal<Todo[]>([]);
todos(); todos.set(...); todos.update((items) => { /* ... */ }); -
1 2 3 4 5 6 7 todos = signal<Todo[]>([]);
todos(); todos.set(...); todos.update((items) => { /* ... */ }); -
1 2 3 4 5 6 7 8 9 todos
= signal<Todo[]>([]); count = computed(() => todos().length); editableCount = linkedSignal(() => todos().length); effect(() => { console.log('Todos count changed to ', count()); }); -
1 2 3 4 5 6 7 8 9 todos
= signal<Todo[]>([]); count = computed(() => todos().length); editableCount = linkedSignal(() => todos().length); effect(() => { console.log('Todos count changed to ', count()); }); -
1 2 3 4 5 6 7 8 9 todos
= signal<Todo[]>([]); count = computed(() => todos().length); editableCount = linkedSignal(() => todos().length); effect(() => { console.log('Todos count changed to ', count()); }); -
1 2 3 4 5 6 7 8 9 todos
= signal<Todo[]>([]); count = computed(() => todos().length); editableCount = linkedSignal(() => todos().length); effect(() => { console.log('Todos count changed to ', count()); }); -
1 export class TodoMainComponent implements OnInit { private url =
`${environment.apiUrl}todos`; 2 3 private http = inject(HttpClient); private todos = signal<Todo[]>([]); 4 5 count = computed(() => this.todos().length); 6 7 doneItems = computed(() => this.todos().filter((item) => item.done).length); 8 9 10 openItems = computed(() => this.todos().filter((item) => !item.done).length); 11 12 sortedTodos = computed(() => [...this.todos()].sort((a, b) => +a.done - +b.done 13 // Method logic with http... 14 15 } -
1 export class TodoMainComponent implements OnInit { private url =
`${environment.apiUrl}todos`; 2 3 private http = inject(HttpClient); private todos = signal<Todo[]>([]); 4 5 6 count = computed(() => this.todos().length); 7 8 doneItems = computed(() => this.todos().filter((item) => item.done).length); 9 10 openItems = computed(() => this.todos().filter((item) => !item.done).length); 11 12 sortedTodos = computed(() => [...this.todos()].sort((a, b) => +a.done - +b.done 13 14 // Method logic with http... 15 } -
1 export class TodoMainComponent implements OnInit { 2 private url
= `${environment.apiUrl}todos`; 3 private http = inject(HttpClient); 4 private todos = signal<Todo[]>([]); 5 count = computed(() => this.todos().length); 6 7 doneItems = computed(() => this.todos().filter((item) => item.done).length); 8 9 10 openItems = computed(() => this.todos().filter((item) => !item.done).length); 11 12 sortedTodos = computed(() => [...this.todos()].sort((a, b) => +a.done - +b.done 13 14 // Method logic with http... 15 } -
1 @Injectable({ providedIn: 'root' }) 2 export class TodoService {
3 private url = `${environment.apiUrl}todos`; 4 private http = inject(HttpClient); 5 private todos = signal<Todo[]>([]); 6 7 count = computed(() => this.todos().length); 8 doneItems = computed(() => this.todos().filter((item) => item.done).length); 9 openItems = computed(() => this.todos().filter((item) => !item.done).length); 10 sortedTodos = computed(() => [...this.todos()].sort((a, b) => +a.done - +b.done)); 11 12 getItems() { 13 this.http.get<Todo[]>(this.url).subscribe((todos) => this.todos.set(todos)); 14 } 15 16 replaceItem() { 17 this.todos.update((items) => [...items]); 18 } 19 20 addItem(value: string) { 21 this.http.post<Todo>(this.url, { value }).subscribe((addedTodo) => { 22 this.todos.update((items) => [addedTodo, ...items]); 23 }); 24 } 25 26 updateItem(value: Todo) { 27 // ... 28 } 29 30 removeItem(id: string) { 31 this.http.delete(`${this.url}/${id}`).subscribe(() => { 32 this.todos.update((items) => [...items.filter((item) => item.id !== id)]); 33 }); 34 } 35 } -
1 @Injectable({ providedIn: 'root' }) 2 export class TodoService {
3 private url = `${environment.apiUrl}todos`; 4 private http = inject(HttpClient); 5 private todos = signal<Todo[]>([]); 6 7 count = computed(() => this.todos().length); 8 doneItems = computed(() => this.todos().filter((item) => item.done).length); 9 openItems = computed(() => this.todos().filter((item) => !item.done).length); 10 sortedTodos = computed(() => [...this.todos()].sort((a, b) => +a.done - +b.done)); 11 12 getItems() { 13 this.http.get<Todo[]>(this.url).subscribe((todos) => this.todos.set(todos)); 14 } 15 16 replaceItem() { 17 this.todos.update((items) => [...items]); 18 } 19 20 addItem(value: string) { 21 this.http.post<Todo>(this.url, { value }).subscribe((addedTodo) => { 22 this.todos.update((items) => [addedTodo, ...items]); 23 }); 24 } 25 26 updateItem(value: Todo) { 27 // ... 28 } 29 30 removeItem(id: string) { 31 this.http.delete(`${this.url}/${id}`).subscribe(() => { 32 this.todos.update((items) => [...items.filter((item) => item.id !== id)]); 33 }); 34 } 35 } -
1 @Injectable({ providedIn: 'root' }) 2 export class TodoService {
3 private url = `${environment.apiUrl}todos`; 4 private http = inject(HttpClient); 5 private todos = signal<Todo[]>([]); 6 7 count = computed(() => this.todos().length); 8 doneItems = computed(() => this.todos().filter((item) => item.done).length); 9 openItems = computed(() => this.todos().filter((item) => !item.done).length); 10 sortedTodos = computed(() => [...this.todos()].sort((a, b) => +a.done - +b.done)); 11 12 getItems() { 13 this.http.get<Todo[]>(this.url).subscribe((todos) => this.todos.set(todos)); 14 } 15 16 replaceItem() { 17 this.todos.update((items) => [...items]); 18 } 19 20 addItem(value: string) { 21 this.http.post<Todo>(this.url, { value }).subscribe((addedTodo) => { 22 this.todos.update((items) => [addedTodo, ...items]); 23 }); 24 } 25 26 updateItem(value: Todo) { 27 // ... 28 } 29 30 removeItem(id: string) { 31 this.http.delete(`${this.url}/${id}`).subscribe(() => { 32 this.todos.update((items) => [...items.filter((item) => item.id !== id)]); 33 }); 34 } 35 } -
1 @Injectable({ providedIn: 'root' }) 2 export class TodoService {
3 private url = `${environment.apiUrl}todos`; 4 private http = inject(HttpClient); 5 private todos = signal<Todo[]>([]); 6 7 count = computed(() => this.todos().length); 8 doneItems = computed(() => this.todos().filter((item) => item.done).length); 9 openItems = computed(() => this.todos().filter((item) => !item.done).length); 10 sortedTodos = computed(() => [...this.todos()].sort((a, b) => +a.done - +b.done)); 11 12 getItems() { 13 this.http.get<Todo[]>(this.url).subscribe((todos) => this.todos.set(todos)); 14 } 15 16 replaceItem() { 17 this.todos.update((items) => [...items]); 18 } 19 20 addItem(value: string) { 21 this.http.post<Todo>(this.url, { value }).subscribe((addedTodo) => { 22 this.todos.update((items) => [addedTodo, ...items]); 23 }); 24 } 25 26 updateItem(value: Todo) { 27 // ... 28 } 29 30 removeItem(id: string) { 31 this.http.delete(`${this.url}/${id}`).subscribe(() => { 32 this.todos.update((items) => [...items.filter((item) => item.id !== id)]); 33 }); 34 } 35 } -
1 export class TodoMainComponent implements OnInit { 2 private todoService
= inject(TodoService); 3 4 count = this.todoService.count; doneItems = this.todoService.doneItems; 5 6 openItems = this.todoService.openItems; 7 sortedTodos = this.todoService.sortedTodos; 8 9 ngOnInit(): void { 10 this.todoService.getItems(); 11 } 12 13 addTodo(value: string) { 14 this.todoService.addItem(value); 15 } 16 17 deleteTodo(item: Todo): void { 18 this.todoService.removeItem(item.id); 19 } 20 21 markAsDone(item: Todo): void { 22 this.todoService.updateItem(item); 23 } 24 } -
logic store ui business logic, events, rxjs, websockets, ... computed,
(a)sync glue signals, methods -
1 @Component({ /* ... */ }) 2 export class TodoMainComponent
implements OnInit { 3 private readonly store = inject(Store); 4 5 items$ = this.store.select(getAllItems); 6 7 ngOnInit(): void { 8 this.store.dispatch(TodoActions.loadAllTodos()); 9 } 10 } -
1 @Component({ /* ... */ }) 2 export class TodoMainComponent
implements OnInit { 3 private readonly store = inject(Store); 4 5 items$ = this.store.select(getAllItems); 6 7 ngOnInit(): void { 8 this.store.dispatch(TodoActions.loadAllTodos()); 9 } 10 } -
1 @Component({ /* ... */ }) 2 export class TodoMainComponent
implements OnInit { 3 private readonly store = inject(Store); 4 5 items$ = this.store.select(getAllItems); 6 7 ngOnInit(): void { 8 this.store.dispatch(TodoActions.loadAllTodos()); 9 } 10 } -
1 @Component({ /* ... */ }) 2 export class TodoMainComponent
implements OnInit { 3 private readonly store = inject(Store); 4 5 items$ = this.store.select(getAllItems); 6 7 ngOnInit(): void { 8 this.store.dispatch(TodoActions.loadAllTodos()); 9 } 10 } -
1 @Component({ /* ... */ }) 2 export class TodoMainComponent
implements OnInit { 3 private readonly store = inject(Store); 4 5 items$ = this.store.select(getAllItems); 6 7 ngOnInit(): void { 8 this.store.dispatch(TodoActions.loadAllTodos()); 9 } 10 } 1 2 3 <app-todo-list [items]="items$ | async" ></app-todo-list> -
1 @Component({ /* ... */ }) 2 export class TodoMainComponent
implements OnInit { 3 private readonly store = inject(Store); 4 5 items$ = this.store.select(getAllItems); 6 7 ngOnInit(): void { 8 this.store.dispatch(TodoActions.loadAllTodos()); 9 } 10 } 1 2 3 <app-todo-list [items]="items$ | async" ></app-todo-list> -
1 @Component({ /* ... */ }) 2 export class TodoMainComponent
implements OnInit { 3 private readonly store = inject(Store); 4 5 items = this.store.selectSignal(getAllItems); 6 7 ngOnInit(): void { 8 this.store.dispatch(TodoActions.loadAllTodos()); 9 } 10 } 1 2 3 <app-todo-list [items]="items()" ></app-todo-list> -
1 @Component({ /* ... */ }) 2 export class TodoMainComponent
implements OnInit { 3 private readonly store = inject(Store); 4 5 items = this.store.selectSignal(getAllItems); 6 7 ngOnInit(): void { 8 this.store.dispatch(TodoActions.loadAllTodos()); 9 } 10 } 1 2 3 <app-todo-list [items]="items()" ></app-todo-list> -
1 2 3 4 5 6 7 8 9 10
11 export const TodoStore = signalStore( 12 13 ); -
1 2 3 4 5 6 7 8 9 10
11 export const TodoStore = signalStore( 12 withState(...), 13 ); -
1 2 3 4 5 6 7 8 9 10
11 12 13 export interface TodoState { items: Todo[]; loading: boolean; } export const initialState: TodoState = { items: [], loading: false, }; export const TodoStore = signalStore( withState(initialState), ); -
1 2 3 4 5 6 7 8 9 10
11 12 13 export interface TodoState { items: Todo[]; loading: boolean; } export const initialState: TodoState = { items: [], loading: false, }; export const TodoStore = signalStore( withState(initialState), ); -
1 2 3 4 5 6 7 8 9 10
11 12 13 export interface TodoState { items: Todo[]; loading: boolean; } export const initialState: TodoState = { items: [], loading: false, }; export const TodoStore = signalStore( withState(initialState), ); -
1 2 3 4 5 6 7 8 9 10
11 12 13 export interface TodoState { items: Todo[]; loading: boolean; } export const initialState: TodoState = { items: [], loading: false, }; export const TodoStore = signalStore( withState(initialState), ); -
1 export const TodoStore = signalStore( 2 withState({ /* ...
*/ }), 3 withMethods((...) => ({ 4 // ... 5 })) 6 ); -
1 export const TodoStore = signalStore( 2 withState({ /* ...
*/ }), 3 withMethods((...) => ({ 4 // ... 5 })) 6 ); -
1 export const TodoStore = signalStore( 2 withState({ /* ...
*/ }), 3 withMethods((store, todoService = inject(TodoService)) => ({ 4 loadAllTodos() { 5 // use TodoService and then 6 patchState(store, { items }); 7 }, 8 })) 9 ); -
1 export const TodoStore = signalStore( 2 withState({ /* ...
*/ }), 3 withMethods((store, todoService = inject(TodoService)) => ({ 4 loadAllTodos() { 5 // use TodoService and then 6 patchState(store, { items }); 7 }, 8 })) 9 ); -
1 export const TodoStore = signalStore( 2 withState({ /* state
goes here */ }), 3 withMethods((store, todoService = inject(TodoService)) => ({ 4 loadAllTodos: rxMethod<void>( 5 switchMap(() => { 6 patchState(store, {loading: true}); 7 8 return todoService.getItems().pipe( 9 tapResponse({ 10 next: (items) => patchState(store, {items}), 11 error: console.error, 12 finalize: () => patchState(store, {loading: false}), 13 }) 14 ); 15 }) 16 ), 17 })) 18 ); -
1 export const TodoStore = signalStore( 2 withState({ /* state
goes here */ }), 3 withMethods((store, todoService = inject(TodoService)) => ({ 4 loadAllTodos: rxMethod<void>( 5 switchMap(() => { 6 patchState(store, {loading: true}); 7 8 return todoService.getItems().pipe( 9 tapResponse({ 10 next: (items) => patchState(store, {items}), 11 error: console.error, 12 finalize: () => patchState(store, {loading: false}), 13 }) 14 ); 15 }) 16 ), 17 })) 18 ); -
1 export const TodoStore = signalStore( 2 withState({ /* state
goes here */ }), 3 withMethods((store, todoService = inject(TodoService)) => ({ 4 loadAllTodos: rxMethod<void>( 5 switchMap(() => { 6 patchState(store, {loading: true}); 7 8 return todoService.getItems().pipe( 9 tapResponse({ 10 next: (items) => patchState(store, {items}), 11 error: console.error, 12 finalize: () => patchState(store, {loading: false}), 13 }) 14 ); 15 }) 16 ), 17 })) 18 ); -
1 export const TodoStore = signalStore( 2 withState({ /* state
goes here */ }), 3 withMethods((store, todoService = inject(TodoService)) => ({ 4 loadAllTodos: rxMethod<void>( 5 switchMap(() => { 6 patchState(store, {loading: true}); 7 8 return todoService.getItems().pipe( 9 tapResponse({ 10 next: (items) => patchState(store, {items}), 11 error: console.error, 12 finalize: () => patchState(store, {loading: false}), 13 }) 14 ); 15 }) 16 ), 17 })) 18 ); -
1 export const TodoStore = signalStore( 2 withState({ /* state
goes here */ }), 3 withMethods((store, todoService = inject(TodoService)) => ({ 4 loadAllTodos: rxMethod<void>( 5 switchMap(() => { 6 patchState(store, {loading: true}); 7 8 return todoService.getItems().pipe( 9 tapResponse({ 10 next: (items) => patchState(store, {items}), 11 error: console.error, 12 finalize: () => patchState(store, {loading: false}), 13 }) 14 ); 15 }) 16 ), 17 })) 18 ); -
1 export const TodoStore = signalStore( 2 withState({ /* state
goes here */ }), 3 withMethods((store, todoService = inject(TodoService)) => ({ 4 loadAllTodos: rxMethod<void>(/* ... */), 5 6 async loadAllTodosByPromise() { 7 patchState(store, { loading: true }); 8 9 const items = await todoService.getItemsAsPromise(); 10 11 patchState(store, { items, loading: false }); 12 }, 13 })) 14 ); -
1 export const TodoStore = signalStore( 2 withState({ /* state
goes here */ }), 3 withMethods((store, todoService = inject(TodoService)) => ({ 4 loadAllTodos: rxMethod<void>(/* ... */), 5 6 async loadAllTodosByPromise() { 7 patchState(store, { loading: true }); 8 9 const items = await todoService.getItemsAsPromise(); 10 11 patchState(store, { items, loading: false }); 12 }, 13 })) 14 ); -
1 2 3 4 5 6 7 8 9 10
11 @Component({ // ... providers: [TodoStore], }) export class AppComponent implements OnInit { readonly store = inject(TodoStore); ngOnInit() { this.store.loadAllTodos(); } } -
1 2 3 4 5 6 7 8 9 10
11 @Component({ // ... providers: [TodoStore], }) export class AppComponent implements OnInit { readonly store = inject(TodoStore); ngOnInit() { this.store.loadAllTodos(); } } -
1 2 3 4 5 6 7 8 9 10
11 @Component({ // ... providers: [TodoStore], }) export class AppComponent implements OnInit { readonly store = inject(TodoStore); ngOnInit() { this.store.loadAllTodos(); } } -
1 2 3 4 5 6 7 8 9 10
11 @Component({ // ... providers: [TodoStore], }) export class AppComponent implements OnInit { readonly store = inject(TodoStore); ngOnInit() { this.store.loadAllTodos(); } } -
1 2 3 4 5 6 7 8 9 10
11 12 @Component({ // ... template: `{{ store.items() }} {{ store.loading() }}`, providers: [TodoStore], }) export class AppComponent implements OnInit { readonly store = inject(TodoStore); ngOnInit() { this.store.loadAllTodos(); } } -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 withMethods(/*
... */), 4 withHooks({ 5 // Hooks 6 }) 7 ); -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 withMethods(/*
... */), 4 withHooks({ 5 // Hooks 6 }) 7 ); -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 withMethods(/*
... */), 4 withHooks({ 5 onInit({ loadAllTodos }) { 6 loadAllTodos(); 7 }, 8 onDestroy() { 9 console.log('on destroy'); 10 }, 11 }) 12 ); -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 withMethods(/*
... */), 4 withHooks({ 5 onInit({ loadAllTodos }) { 6 loadAllTodos(); 7 }, 8 onDestroy() { 9 console.log('on destroy'); 10 }, 11 }) 12 ); -
1 2 3 4 5 6 7 8 9 10
11 @Component({ // ... providers: [TodoStore], }) export class AppComponent implements OnInit { readonly store = inject(TodoStore); ngOnInit() { this.store.loadAllTodos(); } } -
1 2 3 4 5 6 7 @Component({ // ...
providers: [TodoStore], }) export class AppComponent { readonly store = inject(TodoStore); } -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 4
withComputed((state) => ({ 5 /* ... */ 6 })), 7 8 withMethods(/* ... */), 9 withHooks(/* ... */) 10 ); -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 4
withComputed(({ items }) => ({ doneCount: computed(() => items().filter((x) => x.done).length), 5 6 undoneCount: computed(() => items().filter((x) => !x.done).length), 7 percentageDone: computed(() => { 8 const done = items().filter((x) => x.done).length; 9 const total = items().length; 10 11 if (total === 0) { 12 return 0; 13 } 14 15 return (done / total) * 100; 16 }), 17 })), 18 19 withMethods(/* ... */), 20 withHooks(/* ... */) 21 ); -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 4
withComputed(({ items }) => ({ doneCount: computed(() => items().filter((x) => x.done).length), 5 6 undoneCount: computed(() => items().filter((x) => !x.done).length), 7 percentageDone: computed(() => { 8 const done = items().filter((x) => x.done).length; 9 const total = items().length; 10 11 if (total === 0) { 12 return 0; 13 } 14 15 return (done / total) * 100; 16 }), 17 })), 18 19 withMethods(/* ... */), 20 withHooks(/* ... */) 21 ); -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 4
withComputed(({ items }) => ({ doneCount: computed(() => items().filter((x) => x.done).length), 5 6 undoneCount: computed(() => items().filter((x) => !x.done).length), 7 percentageDone: computed(() => { 8 const done = items().filter((x) => x.done).length; 9 const total = items().length; 10 11 if (total === 0) { 12 return 0; 13 } 14 15 return (done / total) * 100; 16 }), 17 })), 18 19 withMethods(/* ... */), 20 withHooks(/* ... */) 21 ); -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 4
withComputed(({ items }) => ({ 5 doneCount: computed(() => items().filter((x) => x.done).length), 6 undoneCount: computed(() => items().filter((x) => !x.done).length), 7 percentageDone: computed(() => { 8 const done = items().filter((x) => x.done).length; 9 const total = items().length; 10 11 if (total === 0) { 12 return 0; 13 } 14 15 return (done / total) * 100; 16 }), 17 })), 18 19 withMethods(/* ... */), 20 withHooks(/* ... */) 21 ); -
1 export const TodoStore = signalStore( 2 withState(initialState), 3 4
withComputed(({ items }) => ({ 5 doneCount: computed(() => items().filter((x) => x.done).length), 6 undoneCount: computed(() => items().filter((x) => !x.done).length), 7 percentageDone: computed(() => { 8 const done = items().filter((x) => x.done).length; 9 const total = items().length; 10 11 if (total === 0) { 12 return 0; 13 } 14 15 return (done / total) * 100; 16 }), 17 })), 18 19 withMethods(/* ... */), 20 withHooks(/* ... */) 21 ); -
1 2 3 4 5 6 7 8 9 10
11 @Component({ providers: [TodoStore], template: ` <div> {{ store.doneCount() }} / {{ store.undoneCount() }} {{ store.percentageDone() }} </div> ` }) export class AppComponent { readonly store = inject(TodoStore); } -
1 2 3 4 5 6 7 8 9 10
11 @Component({ providers: [TodoStore], template: ` <div> {{ store.doneCount() }} / {{ store.undoneCount() }} {{ store.percentageDone() }} </div> ` }) export class AppComponent { readonly store = inject(TodoStore); } -
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20 21 22 import { computed } from '@angular/core'; import { signalStoreFeature, withComputed, withState } from '@ngrx/signals'; export type RequestStatus = 'idle' | 'pending' | 'fulfilled' | { error: string }; export type RequestStatusState = { requestStatus: RequestStatus }; export function withRequestStatus() { return signalStoreFeature( withState<RequestStatusState>({ requestStatus: 'idle' }), withComputed(({ requestStatus }) => ({ isPending: computed(() => requestStatus() === 'pending'), isFulfilled: computed(() => requestStatus() === 'fulfilled'), error: computed(() => { const status = requestStatus(); return typeof status === 'object' ? status.error : null; }), })) ); } -
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20 21 22 import { computed } from '@angular/core'; import { signalStoreFeature, withComputed, withState } from '@ngrx/signals'; export type RequestStatus = 'idle' | 'pending' | 'fulfilled' | { error: string }; export type RequestStatusState = { requestStatus: RequestStatus }; export function withRequestStatus() { return signalStoreFeature( withState<RequestStatusState>({ requestStatus: 'idle' }), withComputed(({ requestStatus }) => ({ isPending: computed(() => requestStatus() === 'pending'), isFulfilled: computed(() => requestStatus() === 'fulfilled'), error: computed(() => { const status = requestStatus(); return typeof status === 'object' ? status.error : null; }), })) ); } -
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20 21 22 import { computed } from '@angular/core'; import { signalStoreFeature, withComputed, withState } from '@ngrx/signals'; export type RequestStatus = 'idle' | 'pending' | 'fulfilled' | { error: string }; export type RequestStatusState = { requestStatus: RequestStatus }; export function withRequestStatus() { return signalStoreFeature( withState<RequestStatusState>({ requestStatus: 'idle' }), withComputed(({ requestStatus }) => ({ isPending: computed(() => requestStatus() === 'pending'), isFulfilled: computed(() => requestStatus() === 'fulfilled'), error: computed(() => { const status = requestStatus(); return typeof status === 'object' ? status.error : null; }), })) ); } -
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20 21 22 import { computed } from '@angular/core'; import { signalStoreFeature, withComputed, withState } from '@ngrx/signals'; export type RequestStatus = 'idle' | 'pending' | 'fulfilled' | { error: string }; export type RequestStatusState = { requestStatus: RequestStatus }; export function withRequestStatus() { return signalStoreFeature( withState<RequestStatusState>({ requestStatus: 'idle' }), withComputed(({ requestStatus }) => ({ isPending: computed(() => requestStatus() === 'pending'), isFulfilled: computed(() => requestStatus() === 'fulfilled'), error: computed(() => { const status = requestStatus(); return typeof status === 'object' ? status.error : null; }), })) ); } -
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20 21 22 import { computed } from '@angular/core'; import { signalStoreFeature, withComputed, withState } from '@ngrx/signals'; export type RequestStatus = 'idle' | 'pending' | 'fulfilled' | { error: string }; export type RequestStatusState = { requestStatus: RequestStatus }; export function withRequestStatus() { return signalStoreFeature( withState<RequestStatusState>({ requestStatus: 'idle' }), withComputed(({ requestStatus }) => ({ isPending: computed(() => requestStatus() === 'pending'), isFulfilled: computed(() => requestStatus() === 'fulfilled'), error: computed(() => { const status = requestStatus(); return typeof status === 'object' ? status.error : null; }), })) ); } -
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20 21 22 import { computed } from '@angular/core'; import { signalStoreFeature, withComputed, withState } from '@ngrx/signals'; export type RequestStatus = 'idle' | 'pending' | 'fulfilled' | { error: string }; export type RequestStatusState = { requestStatus: RequestStatus }; export function withRequestStatus() { return signalStoreFeature( withState<RequestStatusState>({ requestStatus: 'idle' }), withComputed(({ requestStatus }) => ({ isPending: computed(() => requestStatus() === 'pending'), isFulfilled: computed(() => requestStatus() === 'fulfilled'), error: computed(() => { const status = requestStatus(); return typeof status === 'object' ? status.error : null; }), })) ); } -
1 2 3 4 5 6 7 8 9 10
11 export function setPending(): RequestStatusState { return { requestStatus: 'pending' }; } export function setFulfilled(): RequestStatusState { return { requestStatus: 'fulfilled' }; } export function setError(error: string): RequestStatusState { return { requestStatus: { error } }; } -
1 2 3 4 5 6 7 8 9 10
11 12 13 export const TodoStore = signalStore( withState(initialState), withRequestStatus(), withComputed(/* ... */), withMethods(/* ... */), withHooks(/* ... */) ); readonly store = inject(TodoStore); store.requestStatus(); store.isPending(); store.isFulfilled(); -
1 2 3 4 5 6 7 8 9 10
11 12 13 export const TodoStore = signalStore( withState(initialState), withRequestStatus(), withComputed(/* ... */), withMethods(/* ... */), withHooks(/* ... */) ); readonly store = inject(TodoStore); store.requestStatus(); store.isPending(); store.isFulfilled(); -
1 2 3 4 5 6 7 8 9 10
11 12 13 export const TodoStore = signalStore( withState(initialState), withRequestStatus(), withComputed(/* ... */), withMethods(/* ... */), withHooks(/* ... */) ); readonly store = inject(TodoStore); store.requestStatus(); store.isPending(); store.isFulfilled(); -
1 2 3 4 5 6 7 8 9 10
11 12 13 export const TodoStore = signalStore( withState(initialState), withRequestStatus(), withComputed(/* ... */), withMethods(/* ... */), withHooks(/* ... */) ); readonly store = inject(TodoStore); store.requestStatus(); store.isPending(); store.isFulfilled(); -
import { createActionGroup, emptyProps, props } from '@ngrx/store'; export const
usersPageActions = createActionGroup({ source: 'Users Page', events: { 'Opened': emptyProps(), 'Refreshed': emptyProps(), }, }); export const usersApiActions = createActionGroup({ source: 'Users API', events: { 'Users Loaded Success': props<{ users: User[] }>(), 'Users Loaded Error': props<{ error: string }>(), }, }); -
import { type } from '@ngrx/signals'; import { eventGroup }
from '@ngrx/signals/events'; export const usersPageEvents = eventGroup({ source: 'Users Page', events: { opened: type<void>(), refreshed: type<void>(), }, }); export const usersApiEvents = eventGroup({ source: 'Users API', events: { usersLoadedSuccess: type<User[]>(), usersLoadedError: type<string>(), }, }); -
export const usersReducer = createReducer( initialUsersState, on(usersPageActions.opened, usersPageActions.refreshed, (state) =>
({ ...state, requestStatus: 'pending', })), on(usersApiActions.usersLoadedSuccess, (state, { users }) => ({ ...state, users, requestStatus: 'fulfilled', })), on(usersApiActions.usersLoadedError, (state, { error }) => ({ ...state, requestStatus: { error }, })), ); -
import { on, withReducer } from '@ngrx/signals/events'; export const UsersStore
= signalStore( { providedIn: 'root' }, withEntities<User>(), withRequestStatus(), withReducer( on(usersPageEvents.opened, usersPageEvents.refreshed, setPending), on(usersApiEvents.usersLoadedSuccess, ({ payload }) => [ setAllEntities(payload), setFulfilled(), ]), on(usersApiEvents.usersLoadedError, ({ payload }) => setError(payload)), ), ); -
export const loadUsers$ = createEffect( (actions$ = inject(Actions), usersService =
inject(UsersService)) => actions$.pipe( ofType(usersPageActions.opened, usersPageActions.refreshed), exhaustMap(() => usersService.getAll().pipe( map((users) => usersApiActions.usersLoadedSuccess({ users })), catchError((error: { message: string }) => of(usersApiActions.usersLoadedError({ error: error.message })), ), ), ), ), { functional: true }, ); -
export const UsersStore = signalStore( /* ... */ withEventHandlers( (_,
events = inject(Events), usersService = inject(UsersService)) => ({ loadUsers$: events .on(usersPageEvents.opened, usersPageEvents.refreshed) .pipe( exhaustMap(() => usersService.getAll().pipe( mapResponse({ next: (users) => usersApiEvents.usersLoadedSuccess(users), error: (error: { message: string }) => usersApiEvents.usersLoadedError(error.message), }), ), ), ), logError$: events .on(usersApiEvents.usersLoadedError) .pipe(tap(({ payload }) => console.log(payload))), }), ), ); -
@Component({ selector: 'app-users', template: ` <h1>Users</h1> @if (usersStore.isPending()) { <p>Loading...</p>
} <ul> @for (user of usersStore.entities(); track user.id) { <li>{{ user.name }}</li> } </ul> `, }) export class UsersComponent { readonly usersStore = inject(UsersStore); } -
@Component({ template: ` <h1>Users</h1> <button (click)="onRefresh()">Refresh</button> <!-- ... --> `,
}) export class UsersComponent implements OnInit { readonly usersStore = inject(UsersStore); readonly dispatcher = inject(Dispatcher); ngOnInit() { this.dispatcher.dispatch(usersPageEvents.opened()); } onRefresh(): void { this.dispatcher.dispatch(usersPageEvents.refreshed()); } } -
classic store signal store signal store events plugin biggest and
clearest separation actions, reducer, effects, store, ... -
classic store signal store signal store events plugin biggest and
clearest separation actions, reducer, effects, store, ... each store as a service no separate actions, reducer, etc. extensible with custom features calling methods on store -
classic store signal store signal store events plugin biggest and
clearest separation actions, reducer, effects, store, ... each store as a service no separate actions, reducer, etc. extensible with custom features calling methods on store everything signal store has actions, reducer, effects separated dispatching actions signal store w/ classic ngrx