Skip to content

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 ComponentStore methods like updater, setState, or patchState
  • Create side effects and handle asynchronous code like API requests using the effect method

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_TODOS or ADD_TODO which 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_SUCCESS or LOAD_TODOS_FAILURE after 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!)