Dev Notes Blog

Deep Dive into @ngrx/signal Store

23rd August 2024
NgRx
angular
ngrx
Last updated:6th September 2025
5 Minutes
967 Words

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 onInit and/or onDestroy methods. These methods get the store instance as an argument. The onInit method runs in a special context that allows you to inject dependencies or use functions that need this context, like takeUntilDestroyed.
shopping-car.store.ts
1
import { computed } from '@angular/core';
2
import { signalStore, withComputed, withState } from '@ngrx/signals';
3
import { ShoppingCarItems } from '../models/shopping-car-item.model';
4
5
type ShoppingCarState = { ... };
6
7
const initialCarState: ShoppingCarState = { ... };
8
9
export 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 onInit and onDestroy hooks or use injected dependencies in the onDestroy hook. Here, you pass a factory function instead of just an object. This function takes the store instance, returns an object with onInit and/or onDestroy methods, and runs in the same special context, giving you more control and reusability.
shopping-car.store.ts
1
export 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.

shopping-car.store.ts
1
import { signalStore, withState } from '@ngrx/signals';
2
import { ShoppingCardItems } from '../models/shopping-card-item.model';
3
4
type ShoppingCarState = {
5
items: ShoppingCardItems[];
6
isLoading: boolean;
7
filter: { query: string; order: 'asc' | 'desc' };,
8
_privateItems: ShoppingCardItems[]
9
};
10
11
const initialCarState: ShoppingCarState = {
12
items: [],
13
isLoading: false,
14
filter: {
15
query: '',
6 collapsed lines
16
order: 'asc',
17
},
18
_privateItems: []
19
};
20
21
export 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:

weather.component.ts
1
import { Component, OnInit } from '@angular/core';
2
import { map, pipe, tap } from 'rxjs';
3
import { rxMethod } from '@ngrx/signals/rxjs-interop';
4
5
@Component({
6
/* ... */
7
})
8
export class WeatherComponent implements OnInit {
9
// This reactive method will process temperature readings
10
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°C
19
this.logTripledTemperature(20); // console: Tripled Temperature: 60°C
20
}
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:

numbers.component.ts
1
import { Component, OnInit, signal } from '@angular/core';
2
import { map, pipe, tap } from 'rxjs';
3
import { rxMethod } from '@ngrx/signals/rxjs-interop';
4
5
@Component({
6
/* ... */
7
})
8
export 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: 20
19
}
20
21
addNumber() {
22
this.number.set(2); // console: 4
23
}
24
}

For observables:

numbers.component.ts
1
import { Component, OnInit } from '@angular/core';
2
import { interval, of, pipe, tap } from 'rxjs';
3
import { rxMethod } from '@ngrx/signals/rxjs-interop';
4
5
@Component({
6
/* ... */
7
})
8
export 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, 600
19
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.

news.component.ts
1
import { Component, inject, OnInit, signal } from '@angular/core';
2
import { concatMap, filter, pipe } from 'rxjs';
3
import { rxMethod } from '@ngrx/signals/rxjs-interop';
4
import { tapResponse } from '@ngrx/operators';
5
import { NewsService } from './news.service';
6
7
@Component({
8
/* ... */
9
})
10
export 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 void as the generic type to create methods without arguments.
  • Manual Cleanup: You can manually unsubscribe reactive methods by calling the unsubscribe method.
  • Initialization Outside of Injection Context: To initialize outside an injection context, pass an injector as the second argument.
Article title:Deep Dive into @ngrx/signal Store
Article author:Andrés Arias
Release time:23rd August 2024