State Management Libraries
The state management approach we have looked at is something that can scale well — I think it also adheres to the idea of declarative and reactive code more closely than many other state management libraries.
There is no specific reasons that you have to use a state management library — but certain libraries might provide you with certain conveniences or use a particular style that you like. Perhaps you join a team where state is handled using a state management library.
It is not a goal of this course to cover how to integrate various state management libraries, but I do want to give you an idea of some of the popular options out there that you might come across and what their general philosophy is.
SignalStore
NgRx SignalStore (not to be confused with other libraries offered by NgRx) is quite new, but is quickly becoming a sort of defacto state management solution for signals in the Angular world.
A basic implementation looks like this:
import { signalStore, withState } from '@ngrx/signals';
import { Book } from './book.model';
type BooksState = {
books: Book[];
isLoading: boolean;
filter: { query: string; order: 'asc' | 'desc' };
};
const initialState: BooksState = {
books: [],
isLoading: false,
filter: { query: '', order: 'asc' },
};
export const BooksStore = signalStore(
withState(initialState)
);
This BooksStore
can then be injected wherever you need to use it:
import { Component, inject } from '@angular/core';
import { BooksStore } from './books.store';
@Component({
providers: [BooksStore],
})
export class BooksComponent {
readonly store = inject(BooksStore);
}
Alternatively, so that you wouldn’t have to add it to the providers
of a
particular component, you can provide it globally like this:
export const BooksStore = signalStore(
{ providedIn: 'root' },
withState(initialState)
);
At a basic level, we pass whatever state we want to keep track of to the store
using withState
. We will then be given signals for all of that state,
including the nested state like query
and order
, e.g:
console.log(store.books())
console.log(store.isLoading())
console.log(store.filter.query())
console.log(store.filter.order())
As well as withState
, SignalStore provides many other features that provide
you a great deal of control over how to use and update your state.
StateAdapt
StateAdapt is a very new state management library, which does make it risky to use. However, I wanted to give it a quick special mention because I think it is the state management library that deals with the idea of coding reactive/declaratively the best.
In general, the philosophy is very much the same as the approach we are using — that is due in large part to the fact that I was heavily inspired by it when deciding on the approach we are using in this course.
The API to use it is perhaps a bit more confronting than what we have covered, but it offers a bunch of extra features.
signalSlice
Full disclosure: I co-created this utility and designed it around the way I like to think about state, so naturally this is a solution I am biased toward.
signalSlice is a lightweight state utility that is designed around similar ideas to those we have already discussed in terms of declarative code.
A basic implementation looks like this:
initialState = {
checklists: []
};
state = signalSlice({
initialState: this.initialState,
sources: [this.loadChecklists$],
actionSources: {
add: (state, action$: Observable<AddChecklist>) =>
action$.pipe(
map((checklist) => ({
checklists: [...state().checklists, checklist],
})),
),
}
});
The general idea is that we provide it with the state we want to track, and the
only way that state can be updated is via any of the sources
emitting values
that are mapped to the state object, or via the actionSources
that can be
triggered on the state object, e.g: