NgRx Signal Store & Angular: Getting started
In this blog post, I will provide an overview of the Angular NgRx Signal Store and demonstrate how to use it in your applications.
NgRx Signal Store & Angular: Getting started
Introduction
“Normal” state management with NgRx is a very common approach when working with state management in your Angular application. I would not want to go without it in an Angular project. The classic NgRx approach relies on RxJS which lets you use RxJS all the way, or half of the way if you prefer. You can use the store, effects and reducer in a classic RxJS way and use selectSignal
to read the data out as signals.
As Angular introduced Signals as stable with the last version, the NgRx Team published a version of the store which completely relies on signals.
Let us dive into it and compare it a little to the classic store approach, NgRx provides.
Recap: The classic way
NgRx provides state management with a reducer, effects, actions and selectors.
Actions
Actions are the messages we pass around to trigger an effect and/or to modify the store through the reducer. Effects and Reducers are listening to the actions and if you want them to react when an action is being called, you have to implement the corresponding effect or part of the reducer. Actions can also have a content. Like a letter you send around.
export const TodoActions = createActionGroup({
source: 'Todo',
events: {
'Load All Todos': emptyProps(),
'Load All Todos Finished': props<{ todos: Todo[] }>(),
// ... more actions if needed
},
});
Actions can be called with
private readonly store = inject(Store);
this.store.dispatch(TodoActions.loadAllTodos());
Reducer
It is a pure function, taking the old state and returning a new state. If you pass it a state object (A) and an action with or without a content (B), the result will always be the same (C). This makes your app predictable and incredibly easy to test.
export interface TodoState {
items: Todo[];
selectedItem: Todo;
loading: boolean;
}
export const initialState: TodoState = {
items: [],
selectedItem: null,
loading: false,
};
export const todoReducer = createReducer(
initialState,
on(TodoActions.loadAllTodosFinished, (state, { todos }) => {
return {
...state,
loading: false,
items: todos,
};
);
Effects
Effects are for asynchronous operations, which almost every Angular app has. Like a reducer, the effects listen to actions and react, if an action of a type they are implemented for is being dispatched. They react to an action, do something, and when finished they return a new action. They can be functional or be in a class. This example listens to an action loadAllTodos
and returns loadAllTodosFinished
with some content when finished. The reducer above listens to the loadAllTodosFinished
action and modifies the state.
@Injectable()
export class TodoEffects {
private readonly actions$ = inject(Actions);
private readonly todoService = inject(TodoService);
loadTodos$ = createEffect(() =>
this.actions$.pipe(
ofType(TodoActions.loadAllTodos),
concatMap(() =>
this.todoService.getItems().pipe(
map((todos) => TodoActions.loadAllTodosFinished({ todos })),
catchError((error) => of(error))
)
)
)
);
}
Selectors
Your state is always an object { … } and to not always inject the complete state in a component and then reading the properties, you can write selectors which provide you the property you need. They can also calculate the value you need based on the properties on your state. So you do not need a new state property for every information you want to show, you can have selectors which are based on one or more properties on the state and calculate your specific new property out of the existing ones.
export const featureName = 'todoFeature';
export const getTodoFeatureState =
createFeatureSelector<TodoState>(featureName);
export const getAllItems = createSelector(
getTodoFeatureState,
(state: TodoState) => state.items
);
The whole state can be organised like this:
All the logic is now separated from your components, and they only listen to the state now through selectors
export class TodoMainComponent {
private readonly store = inject(Store);
items$ = this.store.select(getAllItems);
ngOnInit(): void {
this.store.dispatch(TodoActions.loadAllTodos());
}
}
If you want to have signals now, you can just change the selector, and you have signals in your template
// items$ = this.store.select(getAllItems);
items$ = this.store.selectSignal(getAllItems);
Why Signal Store?
Lightweight: The @ngrx/signals library is incredibly lightweight, with its size in the final bundle ranging from 500 bytes to 2 kilobytes depending on the utilized APIs. This represents a significant improvement compared to the Store and ComponentStore libraries.
Reusability: By constructing reusable features, we can eliminate code duplication and seamlessly integrate them across multiple stores.
Flexible Scalability: Custom features offer flexible scalability by allowing us to structure our stores in a modular fashion, like the NgRx Store + Effects approach. However, we’re not constrained by this pattern and can maintain a cohesive structure if preferred.
Features: The NgRx team provides ready to use features like the entity-package and the RxJS-interop, if you want to handle RxJS. More features will be provided in the future.
How it works
First, we have to install the new signals package with
npm install @ngrx/signals
Then we can create a state (not store!) with
import { signalState } from '@ngrx/signals';
const state = signalState({ /* State goes here */ });
And if we want to manipulate the state, we can use the patchState
method, passing in the state and a pure function which manipulates the state.
someMethod() {
patchState(this.state, (state) => ({ someProp: state.someProp + 1 }));
}
We already know this principle from the reducer we saw earlier. The difference is, that the patchState
method does not have to return the full state always. It literally only patches the properties you want to change.
Let us see how this looks in a further example.
Creating a store
If we want to create a store, we can use the signalStore
method
import { signalStore, withHooks, withState } from '@ngrx/signals';
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState({ /* state goes here */ }),
);
We can now add our state, let’s say a todoState
like we already know it and create a todoStore
for this
export interface TodoState {
items: Todo[];
loading: boolean;
}
export const initialState: TodoState = {
items: [],
loading: false,
};
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
);
One of the greatest parts of the store is, that it is extendable and providing more functionality directly on the store itself with no action classes needed. So what we can do is, we can add methods to the store, which are provided through the store and can be called.
Manipulating the state with methods
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState({ /* state goes here */ ),
withMethods((store, todoService = inject(TodoService)) => ({
loadAllTodos() {
// use Todoservice and then
patchState(store, { items });
},
}))
);
This method loadAllTodos
is now available directly through the store itself. So in the component we could do:
@Component({
// ...
providers: [TodoStore],
})
export class AppComponent implements OnInit {
readonly store = inject(TodoStore);
ngOnInit() {
this.store.loadAllTodos();
}
}
So no actions are needed anymore. But it gets even better!
The signal Store has its own hooks
. So we can get the component even cleaner. You can pass the methods you just implemented into the hooks to call them!
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withMethods(/* ... */),
withHooks({
onInit({ loadAllTodos }) {
loadAllTodos();
},
onDestroy() {
console.log('on destroy');
},
})
);
And your component goes absolutely clean to load the data
@Component({
providers: [TodoStore],
templates: `<div> {{ store.items }} </div> `
})
export class AppComponent implements OnInit {
readonly store = inject(TodoStore);
// Yes, that is it :)
}
Using RxJS or Promises in methods
Imagine you want to load all to-dos, it could go like this
import { rxMethod } from '@ngrx/signals/rxjs-interop';
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState({ /* state goes here */ ),
withMethods((store, todoService = inject(TodoService)) => ({
loadAllTodos: rxMethod<void>(
pipe(
switchMap(() => {
patchState(store, { loading: true });
return todoService.getItems().pipe(
tapResponse({
next: (items) => patchState(store, { items }),
error: console.error,
finalize: () => patchState(store, { loading: false }),
})
);
})
)
)
}))
);
What I find incredibly flexible is that you can use RxJS or Promises to call your data. In the above example, you can see that we are using an RxJS interop part of NgRx to use RxJS in our methods. The tapResponse
method helps us to use the response and manipulate the state with patchState
again.
But you can also use promises. The caller of the method (the hooks in this case) do not care.
import { rxMethod } from '@ngrx/signals/rxjs-interop';
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState({ /* state goes here */ ),
withMethods((store, todoService = inject(TodoService)) => ({
loadAllTodos(store, todoService: TodoService) { ... },
async loadAllTodosByPromise() {
patchState(store, { loading: true });
const items = await todoService.getItemsAsPromise();
patchState(store, { items, loading: false });
},
}))
);
We are calling this methods from the hook on the OnInit
, but we can pass both methods and call them like
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
// ...
withHooks({
onInit({ loadAllTodosByPromise, loadAllTodos }) {
console.log('on init');
loadAllTodosByPromise();
// or loadAllTodos();
},
onDestroy() {
console.log('on destroy');
},
})
);
Reading the data from the store
But we want to read the data from the store as well. We could always do this by using the store.items
in the template, but maybe we want to compute values, and calculate them based on the properties on our state. Like selectors in the “classic” way.
The signal store provides this functionality with the withComputed
method. In this method, we are getting passed the state as well.
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed((state) => ({
/* ... */
})),
withMethods(/* ... */),
withHooks(/* ... */)
);
So we can add properties there, which are again available through the injected store in the component. These properties hide a composition of data we can calculate with the info from the store
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ items }) => ({
doneCount: computed(() => items().filter((x) => x.done).length),
undoneCount: computed(() => items().filter((x) => !x.done).length),
percentageDone: computed(() => {
const done = items().filter((x) => x.done).length;
const total = items().length;
if (total === 0) {
return 0;
}
return (done / total) * 100;
}),
})),
withMethods(/* ... */),
withHooks(/* ... */)
);
In our component we can use this selectors then
@Component({
providers: [TodoStore],
templates: `
<div>
{{ store.doneCount() }} / {{ store.undoneCount() }}
{{ store.percentageDone() }}
</div> `
})
export class AppComponent implements OnInit {
readonly store = inject(TodoStore);
}
Making it more beautiful
As we know now that we have selectors (withComputed
), effects (withMethods
) and do not need actions anymore, let us make this a little more beautiful and separate to logic a little.
As mentioned, the store is extendable using the signalStoreFeature
method. With this, we can extract the methods and selectors to make the store even more beautiful. This method again has withComputed
, withHooks
, withMethods
for itself, so you can build your own features and hang them into the store. This is material for another blog post ;-)
Let’s start with the computed values. We create a file todo.selectors.ts
and can implement a feature there, providing the selectors.
export function withTodosSelectors() {
return signalStoreFeature(
{ state: type<TodoState>() },
withComputed(({ items, loading }) => ({
doneCount: computed(() => items().filter((x) => x.done).length),
undoneCount: computed(() => items().filter((x) => !x.done).length),
percentageDone: computed(() => {
const done = items().filter((x) => x.done).length;
const total = items().length;
if (total === 0) {
return 0;
}
return (done / total) * 100;
}),
}))
);
}
Using the { state: type<TodoState>() }
the withComputed
knows which signals are present on the state, and we can work with them.
We can use this now in our store with
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withTodosSelectors(), // <-- ADD THIS
// ...
withHooks({
onInit({ loadAllTodosByPromise }) {
console.log('on init');
loadAllTodosByPromise();
},
onDestroy() {
console.log('on destroy');
},
})
);
Let us go on with the methods. We can extract them in a new file todo.methods.ts
.
import { inject } from '@angular/core';
import { tapResponse } from '@ngrx/operators';
import {
patchState,
signalStoreFeature,
type,
withMethods,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { pipe, switchMap } from 'rxjs';
import { Todo } from '../models/todo';
import { TodoService } from '../todo.service';
import { TodoState } from './todo.state';
export function withTodosMethods() {
return signalStoreFeature(
{ state: type<TodoState>() },
withMethods((state, todoService = inject(TodoService)) => ({
loadAllTodos: rxMethod<void>(
pipe(
switchMap(() => {
patchState(state, { loading: true });
return todoService.getItems().pipe(
tapResponse({
next: (items) => patchState(state, { items }),
error: console.error,
finalize: () => patchState(state, { loading: false }),
})
);
})
)
),
async loadAllTodosByPromise() {
patchState(state, { loading: true });
const items = await todoService.getItemsAsPromise();
patchState(state, { items, loading: false });
},
addTodo: rxMethod<string>(
pipe(
switchMap((value) => {
patchState(state, { loading: true });
return todoService.addItem(value).pipe(
tapResponse({
next: (item) =>
patchState(state, { items: [...state.items(), item] }),
error: console.error,
finalize: () => patchState(state, { loading: false }),
})
);
})
)
),
moveToDone: rxMethod<Todo>(
pipe(
switchMap((todo) => {
patchState(state, { loading: true });
const toSend = { ...todo, done: !todo.done };
return todoService.updateItem(toSend).pipe(
tapResponse({
next: (updatedTodo) => {
const allItems = [...state.items()];
const index = allItems.findIndex((x) => x.id === todo.id);
allItems[index] = updatedTodo;
patchState(state, {
items: allItems,
});
},
error: console.error,
finalize: () => patchState(state, { loading: false }),
})
);
})
)
),
deleteTodo: rxMethod<Todo>(
pipe(
switchMap((todo) => {
patchState(state, { loading: true });
return todoService.deleteItem(todo).pipe(
tapResponse({
next: () => {
patchState(state, {
items: [...state.items().filter((x) => x.id !== todo.id)],
});
},
error: console.error,
finalize: () => patchState(state, { loading: false }),
})
);
})
)
),
}))
);
}
We can use this method withTodosMethods
now and place it in our state:
export interface TodoState {
items: Todo[];
loading: boolean;
}
export const initialState: TodoState = {
items: [],
loading: false,
};
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withTodosSelectors(),
withTodosMethods(),
withHooks({
onInit({ loadAllTodosByPromise }) {
loadAllTodosByPromise();
},
onDestroy() {
console.log('on destroy');
},
})
);
Which looks nice and clean.
Our component then can get the Store injected and use the methods from the store, the selectors and using the hooks indirectly.
@Component({
selector: 'app-root',
standalone: true,
// ...
providers: [TodoStore],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
readonly store = inject(TodoStore);
addTodo() {
this.store.addTodo(this.form.value.todoValue);
this.form.reset();
}
}
Conclusion
In this blog post, we have seen how easy and more functional, a signal store can be set up and used inside an angular app. However, we did not take a look at the sub package `entities` yet. This will be the next blog post. Also, building custom feature for the store is definitely worth to take a look at! Because this is where the store shines.
The new signal store provides a missing piece if you want to go with full signals all the way. We can erase the usage of RxJS to the bare minimum and only work with signals and promises mostly. However: Migrating all apps to this signal store would be overkill, as well can keep existing RxJS effects and use the selectSignal
selector to work with signals in our templates. If you are starting a new project, you might want to consider using the signal store right away. It feels more lightweight, and it is an absolute pleasure to work with.
Thanks, Fabian
Repo: https://github.com/FabianGosebrink/ngrx-signal-store-todo