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:
this.state.add({title: 'hi'})All of the state is exposed automatically via signals:
console.log(this.state.checklists());NgRx Component Store
If you are not new to Angular then maybe you have heard of NgRx already. When people refer to NgRx they generally mean NgRx Store specifically, which has quite a reputation for being complex. However, NgRx is a collection of libraries, and NgRx Store is just one of those.
Whilst NgRx Store has a reputation for being complex, NgRx Component Store is quite the opposite. It is an extremely lightweight state management solution that is primarily intended to manage local state for components, but it can also be used as a simple global store for shared state.
Before I switched to this new state management approach, NgRx Component Store was pretty much all I used for a long time — I like it because it is simple, but it is also flexible and powerful enough for pretty much every situation. It also integrates very well with a reactive/declarative approach to building Angular applications.
The reason I switched away from it is just because I think the RxJS/Signals approach we have been talking about is a bit more declarative and a bit easier to use. This is very heavily preference based though — you might find you like the NgRx approach more.
In the lesson on local state, we already covered the idea of creating a service and providing it to just one component, e.g:
@Component({ // snip... providers: [ChecklistItemStore],})This is the general idea behind Component Store as well — we will create a service (we will call it a “store”) and we provide that to a single component. However, the service we create with Component Store will be a little special:
export interface MoviesState { movies: Movie[];}
@Injectable()export class MoviesStore extends ComponentStore<MoviesState> {
constructor() { super({movies:[]}); }
readonly movies$: Observable<Movie[]> = this.select(state => state.movies);}We create an interface to define the specific type of state we want to store,
and then our service extends ComponentStore using that state. If you are
not too familiar with Object Oriented Programming, the extends keyword
here means our MoviesStore will also include everything in the
ComponentStore class.
By extending ComponentStore our service will have access to a bunch of goodies
— like this select method that is being used above to return state.movies as
an observable stream. The basic idea behind using Component Store to manage
state is that we:
- Define the state we want to store in an interface
- Create a service that extends
ComponentStore - Read state as observable streams by using
select - Write state by using the
ComponentStoremethods likeupdater,setState, orpatchState - Create side effects and handle asynchronous code like API requests using the
effectmethod
You might notice that a lot of these concepts are similar to what we have already been talking about. A lot of state management libraries and different approaches share many of the same concepts.
As with most state management libraries, there are some concepts that you will need to learn before you can be effective with it. That is not the goal of this lesson though, I just want to give you a basic overview of different options.
For more information, check out the documentation.
NgRx Store
NgRx Store is the one that is more complex and seen as something more suited to large and highly complex applications. I think this is at least somewhat true, there probably isn’t an explicit need for NgRx Store for more simple applications, but if you learn the patterns you might find you like it for applications of all sizes.
Unlike Component Store which is intended for local/component level state (although it can be used globally as well), Store is a solution explicitly for handling shared/global state.
The way we handle state with NgRx Store is actually quite similar in concept to our manual approach. It relies heavily on triggering actions and then reacting to those actions in reducers. The general process in NgRx Store might look something like this:
- Our application triggers a pre-defined action like
LOAD_TODOSorADD_TODOwhich can optionally have a payload associated with it (e.g. data for the todo we want to add) - We have reducers that listen to actions that are dispatched in the application, and determine how the state should be updated as a result of that action
- We also have effects that listen to actions, and can trigger some side effect (like making a HTTP request) as a result. These effects can then also trigger additional actions (e.g.
LOAD_TODOS_SUCCESSorLOAD_TODOS_FAILUREafter fetching the todos from an API)
Again, this probably sounds quite similar to our general philosophy. One way in
which our approach is a bit more declarative though is that we can have a data
source to load data (e.g. as we did for our checklist example) which is
triggered automatically and set into our state. NgRx relies more explicitly on
triggering an imperative action like LOAD_CHECKLIST.
There are a lot of concepts to learn here, if you are interested I do have a public video that breaks down the key NgRx Store concepts: I bet you can understand NgRx after watching this video.
NgXs
NgXs is often touted as an appealing alternative to NgRx that is much simpler (generally it is being compared to the global NgRx Store not Component Store).
Like NgRx Store it provides a “single source of truth” which sounds fancy but generally means a single shared object that contains the current state of your application.
The goals of NgXs are to provide a simpler approach to state management with less boilerplate required, and also (unlike NgRx) it has less of a reliance on RxJS for its public API.
In a lot of ways it is quite similar to NgRx store, as I mentioned the key idea with NgRx was:
- Actions —> Reducers (or effects) —> Update state
With NgXs the flow is more like:
- Actions —> State files (where actions are handled) —> Update state
For more information on NgXs, you can check out the documentation.
Elf
Elf is the successor of the popular Akita state management library. Again, Elf aims to be a state management library with less boiler plate but this one embraces RxJS more so than NgXs.
There is, I think, less conceptual overhead with Elf. You update state in much more of a “normal” way like our Component Store approach rather than having these concepts like actions and reducers.
With Elf, you would create a store to hold some state like todos:
const store = createStore( { name: 'todos' }, withProps<TodosProps>({ filter: 'ALL' }), withEntities<Todo>());and to update the state, you use the built-in setEntities method:
export function setTodos(todos: Todo[]) { store.update(setEntities(todos));}For more information on Elf, check out the documentation.
When should you use a state management library?
The simple answer to that is when the need arises or when you want to. You may join a project that uses a particular approach, so you will need to learn that. You might just want to experiment with different approaches to managing state.
I would definitely encourage playing around with different state management options. I am strongly of the opinion that the concepts we are focusing on learning in this course are important and a great way to go about making good software. But this also comes from a history of experimenting with many different approaches — it is one thing for me to “teach” you why the approach we will be using is good, but it is another to understand why by having the context of other approaches too (or perhaps you come to a different conclusion — I don’t get to decide the best way to write code!)