In this deep dive, we’ll explore advanced features like lifecycle hooks, private store members, and rxMethod. These tools will help you enhance your app’s state management, making it more efficient and flexible. Let’s dive in and unlock the full potential of @ngrx/signals!
Lifecycle Hooks
The @ngrx/signals package includes a feature called withHooks that lets you add lifecycle hooks to your SignalStore. This means you can run specific code when your store is initialized or when it’s destroyed.
The withHooks feature can be used in two ways:
- Simple Usage: The first way is to pass an object with
onInitand/oronDestroymethods. These methods get the store instance as an argument. TheonInitmethod runs in a special context that allows you to inject dependencies or use functions that need this context, liketakeUntilDestroyed.
1import { computed } from '@angular/core';2import { signalStore, withComputed, withState } from '@ngrx/signals';3import { ShoppingCarItems } from '../models/shopping-car-item.model';4
5type ShoppingCarState = { ... };6
7const initialCarState: ShoppingCarState = { ... };8
9export const ShoppingCarStore = signalStore(10 withState(initialCarState),11 withComputed(({ items }) => ({12 totalItems: computed(() => items().reduce((sum, item) => sum + item.quantity, 0)),13 totalPrice: computed(() => items().reduce((sum, item) => sum + item.price * item.quantity, 0)),14 })),15 withMethods((store, shoppingCarService = inject(ShoppingCarService)) => ({18 collapsed lines
16 addItem(item: ShoppingCarItems) {17 patchState(store, { items: [...store.items(), item] });18 },19 loadItems: rxMethod<void>(20 pipe(21 switchMap(() => shoppingCarService.loadItems().pipe(tap((result) => patchState(store, { items: [...result] }))))22 )23 ),24 })),25 withHooks({26 onInit(store) {27 store.loadItems();28 },29 onDestroy(store) {30 console.log('totalItems on destroy', store.totalItems());31 },32 })33);- Flexible Usage: The second way is more flexible and useful if you need to share code between the
onInitandonDestroyhooks or use injected dependencies in theonDestroyhook. Here, you pass a factory function instead of just an object. This function takes the store instance, returns an object withonInitand/oronDestroymethods, and runs in the same special context, giving you more control and reusability.
1export const ShoppingCarStore = signalStore(2 withState(initialCarState),3 withComputed(({ items }) => ({ ... })),4 withMethods((store, shoppingCarService = inject(ShoppingCarService)) => ({ ... })),5 withHooks((store) => {6 const logger = inject(Logger);7
8 return {9 onInit() {10 logger.log('OnInit');11 store.loadItems();12 },13 onDestroy() {14 logger.log('OnDestroy');15 },3 collapsed lines
16 };17 }),18);Private Store Members
In SignalStore, you can define private members that are inaccessible from outside the store by prefixing them with an underscore (_). This applies to root-level state slices, computed signals, and methods, ensuring they remain internal to the store.
1import { signalStore, withState } from '@ngrx/signals';2import { ShoppingCardItems } from '../models/shopping-card-item.model';3
4type ShoppingCarState = {5 items: ShoppingCardItems[];6 isLoading: boolean;7 filter: { query: string; order: 'asc' | 'desc' };,8 _privateItems: ShoppingCardItems[]9};10
11const initialCarState: ShoppingCarState = {12 items: [],13 isLoading: false,14 filter: {15 query: '',6 collapsed lines
16 order: 'asc',17 },18 _privateItems: []19};20
21export const ShoppingCarStore = signalStore(withState(initialCarState));RxMethod
The rxMethod function is designed to manage side effects using RxJS within SignalStore. It allows you to create reactive methods that handle various input types static values, signals, or observables by chaining RxJS operators.
Basic Usage
The rxMethod function accepts a chain of RxJS operators via the pipe function and returns a reactive method. This method can process inputs such as numbers, signals, or observables. The example below demonstrates how to log the double of a number:
1import { Component, OnInit } from '@angular/core';2import { map, pipe, tap } from 'rxjs';3import { rxMethod } from '@ngrx/signals/rxjs-interop';4
5@Component({6 /* ... */7})8export class WeatherComponent implements OnInit {9 // This reactive method will process temperature readings10 readonly logTripledTemperature = rxMethod<number>(11 pipe(12 map((temp) => temp * 3),13 tap((tripledTemp) => console.log(`Tripled Temperature: ${tripledTemp}°C`)),14 ),15 );6 collapsed lines
16
17 ngOnInit(): void {18 this.logTripledTemperature(15); // console: Tripled Temperature: 45°C19 this.logTripledTemperature(20); // console: Tripled Temperature: 60°C20 }21}Handling Signals and Observables
When using signals or observables, the reactive method executes the chain every time the input value changes or a new value is emitted:
1import { Component, OnInit, signal } from '@angular/core';2import { map, pipe, tap } from 'rxjs';3import { rxMethod } from '@ngrx/signals/rxjs-interop';4
5@Component({6 /* ... */7})8export class NumbersComponent implements OnInit {9 number = signal(10);10 readonly logDoubledNumber = rxMethod<number>(11 pipe(12 map((num) => num * 2),13 tap(console.log),14 ),15 );9 collapsed lines
16
17 ngOnInit(): void {18 this.logDoubledNumber(this.number); // console: 2019 }20
21 addNumber() {22 this.number.set(2); // console: 423 }24}For observables:
1import { Component, OnInit } from '@angular/core';2import { interval, of, pipe, tap } from 'rxjs';3import { rxMethod } from '@ngrx/signals/rxjs-interop';4
5@Component({6 /* ... */7})8export class NumbersComponent implements OnInit {9 readonly logDoubledNumber = rxMethod<number>(10 pipe(11 map((num) => num * 2),12 tap(console.log),13 ),14 );15
8 collapsed lines
16 ngOnInit(): void {17 const num1$ = of(100, 200, 300);18 this.logDoubledNumber(num1$); // console: 200, 400, 60019
20 const num2$ = interval(2000);21 this.logDoubledNumber(num2$); // console: 0, 2, 4, ... (every 2 seconds)22 }23}API Call Handling
The rxMethod is ideal for API calls.
1import { Component, inject, OnInit, signal } from '@angular/core';2import { concatMap, filter, pipe } from 'rxjs';3import { rxMethod } from '@ngrx/signals/rxjs-interop';4import { tapResponse } from '@ngrx/operators';5import { NewsService } from './news.service';6
7@Component({8 /* ... */9})10export class NewsComponent implements OnInit {11 private readonly newsService = inject(NewsService);12 readonly articles = signal<Record<string, any>>({});13 readonly selectedKeyword = signal<string | null>(null);14
15 readonly fetchArticlesByKeyword = rxMethod<string | null>(24 collapsed lines
16 pipe(17 filter((keyword) => !!keyword && !this.articles()[keyword]),18 concatMap((keyword) =>19 this.newsService.getByKeyword(keyword).pipe(20 tapResponse({21 next: (fetchedArticles) => this.addArticles(fetchedArticles),22 error: console.error,23 }),24 ),25 ),26 ),27 );28
29 ngOnInit(): void {30 this.fetchArticlesByKeyword(this.selectedKeyword);31 }32
33 addArticles(articles: any[]): void {34 this.articles.update((currentArticles) => ({35 ...currentArticles,36 [article.id]: article,37 }));38 }39}Additional Features
- Reactive Methods without Arguments: Use
voidas the generic type to create methods without arguments. - Manual Cleanup: You can manually unsubscribe reactive methods by calling the
unsubscribemethod. - Initialization Outside of Injection Context: To initialize outside an injection context, pass an injector as the second argument.