, ,

NgRx Signal Store & Angular: Getting started

Dec 03, 2023 reading time 15 minutes

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.

The patchState method can be compared to the reducer. It takes the state and a function, which returns a manipulated new state.

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(/* ... */)
);

The properties in the withComputed can be compared to selectors. With them, you can read and compose data from the store.

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 selectSignalselector 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