How to use NgRx SignalStore?

Alain Chautard
Angular Training
Published in
6 min readJan 18, 2024

--

With the release of Angular Signals, RxJS-based state management libraries had to adapt and create signal-based alternatives, which NgRx has done with its version 17.

In this post, we’re diving into how to use NgRx SignalStore, the syntax and the different options, and how different it is from regular signals.

My example comes from my free Angular Signals workshop, which is now available on YouTube.

How to install NgRx SignalStore?

This is the easiest step. Run that command in your Angular project:

npm install @ngrx/signals

Note that NgRx SignalStore is independent of NgRx itself, which means you can use it without using NgRx and without even knowing about NgRx (that said, if you want to learn about state management and NgRx, I have a few short articles here).

Creating a local state with NgRx Signal

This is the most basic approach to NgRx Signal. If you don’t need any complexity and want to manage a simple state in a component, you use the signalState function:

import { signalState } from '@ngrx/signals';

const state = signalState({ currentCurrency: "USD", rate: 1});

This creates signals for every single piece of state. In our case:

// These signals are auto-created
state.currentCurrency(); // returns "USD"
state.rate(); // returns 1

Then, to update that state, we have to use the patchState function:

import { patchState } from '@ngrx/signals';

patchState(state, (state) => ({ currentCurrency: "EUR", rate: 1.2 }));

And that’s it! Now you know about the most basic approach to using NgRx Signals.

Creating a Store with NgRx SignalStore

Let’s say we have a global state managed with signals in a service, such as (full code example here on Stackblitz):

@Injectable({
providedIn: 'root',
})
export class CurrencyService {
readonly currencies = signal<Currency[]>(DEFAULT_CURRENCIES);
readonly currentCurrency = signal<Currency>(DEFAULT_CURRENCIES[0]);
}

We can turn that service into a NgRx signal store as follows:


import {signalStore } from '@ngrx/signals';

export const CurrencyStore = signalStore(
{ providedIn: 'root' },
withState({
currencies: DEFAULT_CURRENCIES,
currentCurrency: DEFAULT_CURRENCIES[0]
})
);

The above creates an injectableservice called CurrencyStore with automatically created signals for each piece of state in our store. A component can use it like so:

export class CurrencySwitcherComponent {
// Inject store in component class
currencyStore = inject(CurrencyStore);
}

And in the component’s template (using the latest control flow syntax as a bonus):

<select #dropdown>
@for (currency of currencyStore.currencies(); track currency.code) {
<option [value]="currency.code">
{{ currency.code }} ({{ currency.symbol }})
</option>
}
</select>
Angular Certification Exam

What about computed signals?

Our original store service had a computed signal to return an exchange rate whenever we changed a currency or the exchange rates:

@Injectable({
providedIn: 'root',
})
export class CurrencyService {
readonly currencies = signal<Currency[]>(DEFAULT_CURRENCIES);
readonly currentCurrency = signal<Currency>(DEFAULT_CURRENCIES[0]);
readonly exchangeRates = signal<ExchangeRates>(DEFAULT_EXCHANGE_RATES);

readonly exchangeRate = computed<number>(
() => this.exchangeRates()[this.currentCurrency().code]
);

With NgRx Signal store, we can achieve the same thing using the withComputed function:

export const CurrencyStore = signalStore(
{ providedIn: 'root' },
withState({
currencies: DEFAULT_CURRENCIES,
currentCurrency: DEFAULT_CURRENCIES[0],
exchangeRates: DEFAULT_EXCHANGE_RATES,
}),
withComputed(({ exchangeRates, currentCurrency }) => ({
exchangeRate: computed(() => exchangeRates()[currentCurrency().code]),
}))
);

The result is another signal called exchangeRate available in our store, which means components can do this:

The price is: 
{{currencyStore.currentCurrency().symbol}}
{{priceInUsd / currencyStore.exchangeRate()}}
Weekly Angular Newsletter Subscription Link

How do I update the contents of the store?

With vanilla NgRx, we would use actions and reducers to update our state. When we use NgRx SignalStore, we can create methods that perform state updates.

As a result, the following plain signal implementation in our service:

@Injectable({
providedIn: 'root',
})
export class CurrencyService {
readonly currencies = signal<Currency[]>(DEFAULT_CURRENCIES);
readonly currentCurrency = signal<Currency>(DEFAULT_CURRENCIES[0]);


setCurrency(currencyCode: Currency['code']): void {
const newCurr = this.currencies().find((c) => c.code === currencyCode);
if (newCurr) this.currentCurrency.set(newCurr);
}
}

Becomes the following method appended to our store implementation thanks to the withMethods function:

export const CurrencyStore = signalStore(
{ providedIn: 'root' },
withState({
currencies: DEFAULT_CURRENCIES,
currentCurrency: DEFAULT_CURRENCIES[0],
exchangeRates: { EUR: 1, GBP: 1, USD: 1 } as ExchangeRates,
}),
withComputed(({ exchangeRates, currentCurrency }) => ({
exchangeRate: computed(() => exchangeRates()[currentCurrency().code]),
})),
withMethods((store, http = inject(HttpClient)) => ({
setCurrency(currCode: Currency['code']) {
const newCurr = store.currencies().find((c) => c.code === currCode);
patchState(store, { currentCurrency: newCurr });
}
}))
);

Now, our components can change that state using the setCurrency method:

<select #dropdown (change)="currencyStore.setCurrency(dropdown.value)">
// ...
</select>

How to load async data in our store?

The last piece I want to cover is how to make an HTTP request to fetch server data and put it in our store. With Angular Signals, I used the HTTP client and the toSignal function to turn my Observable into a signal:

 private rates$ = this.http.get<ExchangeRates>(
'https://lp-store-server.vercel.app/rates'
);

private exchangeRates = toSignal(this.rates$, {
initialValue: DEFAULT_EXCHANGE_RATES,
});

With NgRx SignalStore, we can define a specific method to load our data and then use an OnInit hook to get that data loaded when the store is created. Note how we use the same patchState function as in our very first example of SignalState:

withMethods((store, http = inject(HttpClient)) => ({
// ...
loadExchangeRates: rxMethod<ExchangeRates>(
pipe(
switchMap(() =>
http.get<ExchangeRates>('https://lp-store-server.vercel.app/rates')
),
tap((exchangeRates) => patchState(store, { exchangeRates }))
)
),
})),
withHooks({
onInit: ({ loadExchangeRates }) => {
loadExchangeRates(of({}));
},
})

In the end, our entire CurrencyStore looks like this:

export const CurrencyStore = signalStore(
{ providedIn: 'root' },
withState({
currencies: DEFAULT_CURRENCIES,
currentCurrency: DEFAULT_CURRENCIES[0],
exchangeRates: { EUR: 1, GBP: 1, USD: 1 } as ExchangeRates,
}),
withComputed(({ exchangeRates, currentCurrency }) => ({
exchangeRate: computed(() => exchangeRates()[currentCurrency().code]),
})),
withMethods((store, http = inject(HttpClient)) => ({
setCurrency(currencyCode: Currency['code']) {
const newCurrency = store
.currencies()
.find((c) => c.code === currencyCode);
patchState(store, { currentCurrency: newCurrency });
},
loadExchangeRates: rxMethod<ExchangeRates>(
pipe(
switchMap(() =>
http.get<ExchangeRates>('https://lp-store-server.vercel.app/rates')
),
tap((exchangeRates) => patchState(store, { exchangeRates }))
)
),
})),
withHooks({
onInit: ({ loadExchangeRates }) => {
loadExchangeRates(of({}));
},
})
);

You can compare it to the original service that wasn’t using NgRx SignalStore:

@Injectable({
providedIn: 'root',
})
export class CurrencyService {
readonly currencies = signal<Currency[]>(DEFAULT_CURRENCIES);
readonly currentCurrency = signal<Currency>(DEFAULT_CURRENCIES[0]);
private http = inject(HttpClient);
private rates$ = this.http.get<ExchangeRates>(
'https://lp-store-server.vercel.app/rates'
);

private exchangeRates = toSignal(this.rates$, {
initialValue: DEFAULT_EXCHANGE_RATES,
});

readonly exchangeRate = computed<number>(
() => this.exchangeRates()[this.currentCurrency().code]
);

setCurrency(currencyCode: Currency['code']): void {
const newCurrency = this.currencies().find((c) => c.code === currencyCode);
if (newCurrency) this.currentCurrency.set(newCurrency);
}
}

Here is the code and demo for the original service (no NgRx code) and the code and demo after “migration” to NgRx SignalStore, both on Stackblitz.

Conclusion

If we compare NgRx SignalStore with regular NgRx, I believe SignalStore is more accessible to configure and understand. That said, if we compare that same SignalStore implementation with our previous service, I think the original service is easier to read and understand (no RxJs needed!), though it’s pretty close.

In any case, both implementations are signal-based and require a good understanding of signals and the computed function.

NgRx SignalStore can be seen as a utility to create multiple signals out of our state (similar to FormBuilder for Reactive Forms), which might simplify the syntax of very complex stores.

My name is Alain Chautard. I am a Google Developer Expert in Angular and a consultant and trainer at Angular Training, where I help web development teams learn and become comfortable with Angular.

If you enjoyed this article, please clap for it or share it. Your help is always appreciated. You can also subscribe to my articles and the Weekly Angular Newsletter for helpful Angular tips, updates, and tutorials.

--

--