NgRx Signals Store - Is It a NgRx Replacement?
NgRx Signals Store - Is It a NgRx Replacement?

In this post you will learn what is NgRx signals, how to use this library and what is the pros and cons of it.

Not so long ago inside NgRx 17 we got a new library which is called @ngrx/signals.

website

For you it might be really confusing what is the goal of this library. We have things like @ngrx/store which is a global NgRx, we also have a library @ngrx/component-store and now we have ngrx/signals.

You might think "Maybe ngrx/signals is a full replacement of ngrx/store but with signals". This is not true at all. Ngrx with state, reducers and effects are all working just like previously. Yes, sure, you can transform quite easily NgRx stream from the selector to the signals but it is not related to @ngrx/signals library at all.

@ngrx/signals is an additional library which is not related to NgRx stor at all.

You can just think of it like a separate library in NgRx family. A goal of this library is to simplify for developers working with Angular signals. So it is not about NgRx at all, we are talking here only about Angular signals and how to simplify working with them.

NgRx Signals is a standalone library that provides a reactive state management solution and a set of utilities for Angular Signals.

It should be declarative, simple, modular, extensible and scalable.

Installation

The first step here is to install this library.

npm i @ngrx/signals

Inside package.json we have now only @ngrx/signals but not @ngrx/store, @ngrx/effects or any standard NgRx stuff. This is a completely isolated library which works just with signals.

I also prepared a interface for the post that we want to render

// src/app/posts/types/post.interface.ts
export interface PostInterface {
  id: string;
  title: string;
}

and a service which returns a list of posts with the delay.

// src/app/posts/services/posts.service.ts
@Injectable({
  providedIn: 'root',
})
export class PostsService {
  getPosts(): Observable<PostInterface[]> {
    const posts = [
      { id: '1', title: 'First post' },
      { id: '2', title: 'Second post' },
      { id: '3', title: 'Third post' },
    ];
    return of(posts).pipe(delay(2000));
  }
}

Additionally I have a posts.component with just a basic form to add posts.

<div>
  <form [formGroup]="addForm" (ngSubmit)="onAdd()">
    <input type="text" placeholder="Add..." formControlName="title" />
  </form>
</div>

Here we can add a post but we don't have any logic yet. And here is our empty component.

export class PostsComponent {
  fb = inject(FormBuilder);
  postsService = inject(PostsService);
  addForm = this.fb.nonNullable.group({
    title: '',
  });
}

We just injected here a service and created a form but nothing else.

Signal state

But just imagine that this is our posts component where we want to render a list of our posts from the API, we also want to show a loading indicator and an error message when we have some problems and have a possibility to add and remove our posts.

If we are doing it with signals we will typically create several signals just inside our component. Like signals for the list of posts, signal for loading state and signal for an error.

Inside @ngrx/signals the idea is to simplify this logic and make it more declarative.

import {signalState} from '@ngrx/signals';
export class PostsComponent {
  ...
  state = signalState({
    posts: [],
    error: null,
    isLoading: false,
  });
}

Here we used signalState from the library to create multiple signals at once inside an object and provide a default state there. But we must do better because we need to provide a correct interface for our state.

export interface PostsStateInterface {
  posts: PostInterface[];
  isLoading: boolean;
  error: string | null;
}

export class PostsComponent {
  ...
  state = signalState<PostsStateInterface>({
    posts: [],
    error: null,
    isLoading: false,
  });
}

Now it is not possible to throw inside wrong values. Also we see on the glance the whole state of our component at one place.

Let's render some markup now.

...
<div *ngIf="state.isLoading()">Loading...</div>
<div *ngIf="state.error()">{{ state.error() }}</div>

<div *ngFor="let post of state.posts()">
  {{ post.title }}
</div>

Here we just use signals like always but they are stored in a single object state.

This is good but we don't have any posts yet so nothing is rendered. Let's implement onAdd function to be able to create posts.

export class PostsComponent {
  ...
  onAdd(): void {
    const newPost: PostInterface = {
      id: crypto.randomUUID(),
      title: this.addForm.getRawValue().title,
    };
    const updatedPosts = [...state.posts(), newPost];
    patchState(this.state, (state) => ({ ...state, posts: updatedPosts }));
    this.addForm.reset();
  }
}

Here we created newPost with randomID and a title from our form. The we create new array with posts by arring new post to the end. After this we called patchState function which allows us to update a state by returning new state.

patchState is an only way to update our state which is awesome as it keeps things strict.

So inside @ngrx/signals we are getting a really nice utilities on top of the signals to work with them.

Add

As you can see now we can add posts on our page.

Let's implement removing of the posts as a new feature.

{{ post.title }} <span (click)="removePost(post.id)">X</span>

Here in HTML we just provide an ID of the post that we want to remove.

removePost(id: string): void {
  const updatedPosts = this.state.posts().filter((post) => post.id !== id);
  patchState(this.state, (state) => ({ ...state, posts: updatedPosts }));
}

We do similar stuff with reading the signal first and then using patchState to update our state. Now we can easily remove elements in browser.

signalState covers 70% of the cases that you might have with signals.

NgRx signals store

But if you have something more complex or you want to move state from component completely it is also possible. You can create a store outside of the component and access it inside.

export const PostsStore = signalStore(
  withState<PostsStateInterface>({
    posts: [],
    error: null,
    isLoading: false,
  })
)

@Component({
  ...
  providers: [PostsStore],
})
export class PostsComponent {
  ...
  store = inject(PostsStore);

Here we created PostsStore on the outside and injected it inside as a property store. withState function allows us to provide a state inside our store.

withComputed

But there is much more than that. For example we can also create computed properties for the component.

export const PostsStore = signalStore(
  withState<PostsStateInterface>({
    posts: [],
    error: null,
    isLoading: false,
  }),
  withComputed((store) => ({
    postsCount: computed(() => store.posts().length),
  })),
)

Here we created a property postsCount which is a computed signal and it will be available in our component by accessing store.postsCount().

withMethods

But we still did not move addPost and removePost outside of the component in the store.

export const PostsStore = signalStore(
  withState<PostsStateInterface>({
    posts: [],
    error: null,
    isLoading: false,
  }),
  withComputed((store) => ({
    postsCount: computed(() => store.posts().length),
  })),
  withMethods((store) => ({
    addPost(title: string) {
      const newPost: PostInterface = {
        id: crypto.randomUUID(),
        title,
      };
      const updatedPosts = [...store.posts(), newPost];
      patchState(store, { posts: updatedPosts });
    },
    removePost(id: string) {
      const updatedPosts = store.posts().filter((post) => post.id !== id);
      patchState(store, { posts: updatedPosts });
    },
  })),
);

We used withMethods where we can define methods of the store. I fully moved whole business logic of addPost and removePost except of form resetting to the store. As you can see we still used patchState but provided a store inside and the data that we want to update.

But we must still change our html.

<div>
  <form [formGroup]="addForm" (ngSubmit)="onAdd()">
    <input type="text" placeholder="Add..." formControlName="title" />
  </form>

  <div>Total: {{ store.postsCount() }}</div>
  <div *ngIf="store.isLoading()">Loading...</div>
  <div *ngIf="store.error()">{{ store.error() }}</div>

  <div *ngFor="let post of store.posts()">
    {{ post.title }} <span (click)="store.removePost(post.id)">X</span>
  </div>
</div>

Now we are using store instead of state everywhere. It is important that we used store.removePost directly but onAdd not because we need additional component logic inside. Let's do this now.

onAdd(): void {
  this.store.addPost(this.addForm.getRawValue().title);
  this.addForm.reset();
}

Here we just called addPost from the store and resetted our form. So the whole business logic is moved outside and we just work with view logic.

RxJs Method

But it is not all. At some point to might want to fetch data from the API and set them in store. We can do it in easy way.

addPosts(posts: PostInterface[]) {
  patchState(store, { posts });
}

...

ngOnInit(): void {
  this.postsService.getPosts().subscribe((posts) => {
    this.store.addPosts(posts);
  });
}

Here we created a method addPosts in our store which just sets a list of posts inside. Now we can use this method together with subscribe to update our state. This is an amazing approach that I like to use.

But we can do it differently. We can write RxJS code of fetching data inside the store and call getting data directly after initializing of the store.

export const PostsStore = signalStore(
  withState<PostsStateInterface>({
    ...
  }),
  withComputed((store) => ({
    ...
  })),
  withMethods((store, postsService = inject(PostsService)) => ({
    ...
    loadPosts: rxMethod<void>(
      pipe(
        switchMap(() => {
          return postsService.getPosts().pipe(
            tap((posts) => {
              patchState(store, { posts });
            })
          );
        })
      )
    ),
  })),
  withHooks({
    onInit(store) {
      store.loadPosts();
    },
  })
);

We injected PostsService in our withMethods and we created a function loadPosts which returns an RxJS stream. This code is extremely similar to effects in NgRx. We call a service and set posts inside our store with patchState.

Additionally inside withHooks we used onInit hook where we call loadPosts. Now we don't even need to do anything on initialize in the component. It will be automatically fetched when we inject store in our component.

result

As you can see we load data from the API and they are rendered automatically from the store.

And actually if you want to learn Angular with NgRx from empty folder to a fully functional production application make sure to check my Angular and NgRx - Building Real Project From Scratch course.

📚 Source code of what we've done