Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Mastering State Management in Angular: Demystif...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

Mastering State Management in Angular: Demystifying NgRx, the Signal Store, and Events Plugin

Avatar for Fabian Gosebrink

Fabian Gosebrink

October 29, 2026

More Decks by Fabian Gosebrink

Other Decks in Technology

Transcript

  1. 1 2 3 4 5 6 7 todos = signal<Todo[]>([]);

    todos(); todos.set(...); todos.update((items) => { /* ... */ });
  2. 1 2 3 4 5 6 7 todos = signal<Todo[]>([]);

    todos(); todos.set(...); todos.update((items) => { /* ... */ });
  3. 1 2 3 4 5 6 7 todos = signal<Todo[]>([]);

    todos(); todos.set(...); todos.update((items) => { /* ... */ });
  4. 1 2 3 4 5 6 7 todos = signal<Todo[]>([]);

    todos(); todos.set(...); todos.update((items) => { /* ... */ });
  5. 1 2 3 4 5 6 7 todos = signal<Todo[]>([]);

    todos(); todos.set(...); todos.update((items) => { /* ... */ });
  6. 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()); });
  7. 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()); });
  8. 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()); });
  9. 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()); });
  10. 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 }
  11. 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 }
  12. 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 }
  13. 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 }
  14. 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 }
  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 }
  16. 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 }
  17. 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 }
  18. 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 }
  19. 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 }
  20. 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 }
  21. 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 }
  22. 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>
  23. 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>
  24. 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>
  25. 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>
  26. 1 2 3 4 5 6 7 8 9 10

    11 export const TodoStore = signalStore( 12 13 );
  27. 1 2 3 4 5 6 7 8 9 10

    11 export const TodoStore = signalStore( 12 withState(...), 13 );
  28. 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), );
  29. 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), );
  30. 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), );
  31. 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), );
  32. 1 export const TodoStore = signalStore( 2 withState({ /* ...

    */ }), 3 withMethods((...) => ({ 4 // ... 5 })) 6 );
  33. 1 export const TodoStore = signalStore( 2 withState({ /* ...

    */ }), 3 withMethods((...) => ({ 4 // ... 5 })) 6 );
  34. 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 );
  35. 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 );
  36. 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 );
  37. 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 );
  38. 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 );
  39. 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 );
  40. 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 );
  41. 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 );
  42. 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 );
  43. 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(); } }
  44. 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(); } }
  45. 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(); } }
  46. 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(); } }
  47. 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(); } }
  48. 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 );
  49. 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 );
  50. 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(); } }
  51. 1 2 3 4 5 6 7 @Component({ // ...

    providers: [TodoStore], }) export class AppComponent { readonly store = inject(TodoStore); }
  52. 1 export const TodoStore = signalStore( 2 withState(initialState), 3 4

    withComputed((state) => ({ 5 /* ... */ 6 })), 7 8 withMethods(/* ... */), 9 withHooks(/* ... */) 10 );
  53. 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 );
  54. 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 );
  55. 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 );
  56. 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 );
  57. 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 );
  58. 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); }
  59. 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); }
  60. 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; }), })) ); }
  61. 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; }), })) ); }
  62. 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; }), })) ); }
  63. 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; }), })) ); }
  64. 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; }), })) ); }
  65. 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; }), })) ); }
  66. 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 } }; }
  67. 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();
  68. 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();
  69. 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();
  70. 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();
  71. 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 }>(), }, });
  72. 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>(), }, });
  73. 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 }, })), );
  74. 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)), ), );
  75. 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 }, );
  76. 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))), }), ), );
  77. @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); }
  78. @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()); } }
  79. classic store signal store signal store events plugin biggest and

    clearest separation actions, reducer, effects, store, ...
  80. 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
  81. 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