Learn NgRx Component Store - Local Store With NgRx
Learn NgRx Component Store - Local Store With NgRx

If you are using Angular probably you are familiar with NgRx. But not a lot of people know about component store that we have inside NgRx. It allows us to isolate NgRx inside a component.

NgRx store vs NgRx component store

Typically we are using ngrx/store and ngrx/effects. But there is one more package that we can install and it is ngrx/component-store. You can use it as an additional library or without global NgRx store at all. What is the difference?

NgRx store

NgRx store is just a Redux which means it is a completely global state and you dispatch actions to update that state. Then values it that state are synchronized with all our components and subscriptions.

Component store works in a completely different way. We organize our code in the Redux way but just inside a single component. It is not global at all. It is tight to component and everything is destroyed when our component is destroyed.

Component store is the idea to organize your component in NgRx way.

Installation

To start working we must install and additional package

npm i @ngrx/component-store

Initial project

Here I have an application which is created with NgRx. We see here a list of posts that we fetch from the API and we can create here a new post.

Moving to component store

The goal of this post is to move NgRx store to NgRx component store. Here you have 2 possible variants. You can either write all component store logic directly inside the component. Another variant which is recommended is to create an additional class for all business logic of the component and leave your component as a view layer.

// posts.store.ts
export interface PostsComponentState {
  isLoading: boolean;
  error: string | null;
  posts: PostInterface[];
}

@Injectable()
export class PostsStore extends ComponentStore<PostsComponentState> {}

Here we created a class as a services and we extended it from ComponentStore. Additionally we create a state for all properties that we want to use in our component just like we did in NgRx.

Now we need to set all properties inside the state to initial values.

export class PostsStore extends ComponentStore<PostsComponentState> {
  constructor(private postsService: PostsService) {
    super({
      isLoading: false,
      error: null,
      posts: [],
    });
  }
}

Here we used super to set all initial values to our store. Additionally we injected PostsService here which is our service to make API calls.

Reading values from state

At some point we want to read values from the state. In the same way like with selectors we can select values from the state.

export class PostsStore extends ComponentStore<PostsComponentState> {
  private isLoading$ = this.select((state) => state.isLoading);
  private error$ = this.select((state) => state.error);
  private posts$ = this.select((state) => state.posts);
  ...
}

So this.select is an alternative to this.store.select that we typically use in NgRx.


export class PostsStore extends ComponentStore<PostsComponentState> {
  private isLoading$ = this.select((state) => state.isLoading);
  private error$ = this.select((state) => state.error);
  private posts$ = this.select((state) => state.posts);
  vm$ = this.select({
    isLoading: this.isLoading$,
    error: this.error$,
    posts: this.posts$,
  });
  ...
}

Here we did something interesting. We made all selected properties private and created just a single vm$ property which groups all streams of data in a single object. Which means inside our view component we have access only to vm$ property (which means view model).

Rendering properties

Let's try to use our PostsStore in the component.

export class PostsComponent implements OnInit {
  vm$ = this.postsStore.vm$;

  constructor(private fb: FormBuilder, private postsStore: PostsStore) {}
  ...

Here we injected PostsStore like a normal service and created a vm$ property which just references our store property. Now we need to update our html.

<ng-container *ngIf="vm$ | async as vm">
  <h1>Posts Page</h1>

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

  <div *ngIf="vm.isLoading">Loading...</div>

  <div *ngIf="vm.error">{{ vm.error }}</div>

  <div *ngFor="let post of vm.posts">
    {{ post.title }}
  </div>
</ng-container>

Here we wrapped everything with vm$ property which we resolve to local property vm. It allows us to have just a single async pipe in the application. We can simply read all properties from there and we are good to go.

By now our PostsStore is not registered anywhere. We want to register it in our component because it is directly tight to it.

@Component({
  selector: 'posts',
  templateUrl: './posts.component.html',
  providers: [PostsStore],
})

Effects in store

For now we binded all properties but our effects don't work so we are not fetching any data. So we need to create an effect to get the list of posts.

export class PostsStore extends ComponentStore<PostsComponentState> {
  ...
  getPosts = this.effect((trigger$) => {
    return trigger$.pipe(
      tap(() => {
        this.setIsLoading();
      }),
      exhaustMap(() => {
        return this.postsService.getPosts().pipe(
          tapResponse(
            (posts) => this.addPosts(posts),
            (err: HttpErrorResponse) => this.setError(err)
          )
        );
      })
    );
  });
  ...
}

As you can see our effect is super similar to NgRx effect. The only difference is that we start writing it on this.effect and we call our own functions instead of dispatching actions. As here we called setIsLoading, setError and addPosts we must create them accordingly.

Using updater

In order to update our state we want to use updater.

export class PostsStore extends ComponentStore<PostsComponentState> {
  ...
  setIsLoading = this.updater((state) => ({ ...state, isLoading: true }));
  setError = this.updater((state, error: HttpErrorResponse) => ({
    ...state,
    isLoading: false,
    error: error.message,
  }));
  addPosts = this.updater((state, posts: PostInterface[]) => ({
    ...state,
    isLoading: false,
    posts,
  }));
}

All 3 functions use this.updater which has access to our current state and must return new state. Just like with NgRx reducers we must return a new state and not mutate our old state.

Now inside our view component we can trigger our effect to get a list of posts.

export class PostsComponent implements OnInit {
  ngOnInit(): void {
    this.postsStore.getPosts();
  }
}

Initial project

As you can see in browser our app is working as before and we successfully fetched our posts from the API.

Creating a post

Our last step is to add create post effect so this API call also works.

export class PostsStore extends ComponentStore<PostsComponentState> {
  ...
  addPost = this.updater((state, post: PostInterface) => ({
    ...state,
    isLoading: false,
    posts: [...state.posts, post],
  }));

  createPost = this.effect((post$: Observable<{ title: string }>) => {
    return post$.pipe(
      tap(() => {
        this.setIsLoading();
      }),
      exhaustMap((post) => {
        return this.postsService.createPost(post).pipe(
          tapResponse(
            (post) => this.addPost(post),
            (err: HttpErrorResponse) => this.setError(err)
          )
        );
      })
    );
  });
}

The same code as before. We trigger the effect and want to get post data inside. After successful API call we want to change our state with addPost method.

Now we just need to update our view component.

export class PostsComponent implements OnInit {
  ...
  onAdd(): void {
    this.postsStore.createPost(this.addForm.getRawValue());
    this.addForm.reset();
  }
}

As you can see in browser it works as previously.

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