ANGULAR START v19 has launched! ... Get 25% off LEARN MORE

👋 Hey there!

I'm Josh Morony, and this website is a collection of my free content centered around creating modern Angular applications using the latest Angular features, and concepts like reactive and declarative code.

Tutorial hero
Lesson icon

Why NgRx Component Store is a Great Default for State Management in Angular

Originally published February 02, 2022 Time 9 mins

A little while ago, we built an animated 3D login form with the help of Chau Tran and his Angular Three library. You can check out the video here if you’re interested in the rest of the app.

In this blog post, we’ll focus on using NgRx Component Store to manage local state in Angular.

This article is adapted from the following video:

The Scenario

For the app in the video, we needed to keep track of the state of the login form, which could be in either the pending, authenticating, success, or error states. To do this, we used NgRx Component Store. NgRx is often thought of as the Redux-based global state management library for Angular (NgRx Store). However, NgRx also provides other packages, one of which is NgRx Component Store.

NgRx Component Store in Action

Let’s take a look at how NgRx Component Store helps us manage the state in our login component. You’ll notice that we have an additional file inside our feature called login.store.ts:

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { catchError, delay, EMPTY, Observable, switchMap, tap } from 'rxjs';
import { AuthService } from '../auth/auth.service';
import { LoginCredentials, LoginStatus } from './login.model';

export interface LoginState {
  status: LoginStatus;
}

@Injectable()
export class LoginStore extends ComponentStore<LoginState> {
  status$ = this.select((state) => state.status);

  login = this.effect((credentials$: Observable<LoginCredentials>) =>
    credentials$.pipe(
      tap(() => this.setState({ status: 'authenticating' })),
      switchMap((credentials) =>
        this.authService.login(credentials).pipe(
          tap({
            next: (success) => this.setState({ status: 'success' }),
            error: (err) => this.setState({ status: 'error' }),
            finalize: () => this.pending(),
          }),
          catchError(() => EMPTY)
        )
      )
    )
  );

  pending = this.effect(($) =>
    $.pipe(
      delay(1500),
      tap(() => this.setState({ status: 'pending' }))
    )
  );

  constructor(private authService: AuthService) {
    super({ status: 'pending' });
  }
}

This is all that’s required for Component Store (and technically, we don’t even need this file if we don’t want).

We create a service in this file, and note that we don’t provide it in the root, so it’s not available globally.

Then, in the component’s decorator, we add it as a provider. Now we can inject and use this service:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { LoginStore } from './login.store';

@Component({
  selector: 'app-login',
  templateUrl: 'login.page.html',
  styleUrls: ['login.page.scss'],
  providers: [LoginStore],
})
export class LoginPage implements OnInit {
  public loginForm: FormGroup;

  constructor(private fb: FormBuilder, private loginStore: LoginStore) {
    this.loginForm = this.fb.group({
      username: ['', Validators.required],
      password: ['', Validators.required],
    });
  }

  ngOnInit() {}

  handleLogin() {
    this.loginStore.login(this.loginForm.value);
  }
}

Extending Component Store

What Component Store does for us is it adds a little sprinkling of that NgRx architecture goodness to our service. You might notice that we’re extending ComponentStore in this class, and we provide type information for the state that we want to store.

In this case, that’s just a single status value that can be any of the four authentication states.

export type LoginStatus = 'pending' | 'authenticating' | 'success' | 'error';

export interface LoginState {
  status: LoginStatus;
}

Creating Selectors and Updating State

Now, let’s jump back into the store itself and see this in action:

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { catchError, delay, EMPTY, Observable, switchMap, tap } from 'rxjs';
import { AuthService } from '../auth/auth.service';
import { LoginCredentials, LoginStatus } from './login.model';

export interface LoginState {
  status: LoginStatus;
}

@Injectable()
export class LoginStore extends ComponentStore<LoginState> {
  status$ = this.select((state) => state.status);

  constructor(private authService: AuthService) {
    super({ status: 'pending' });
  }
}

We use the select method from ComponentStore to create an observable stream of a specific portion of the available state. In this case, we have just one piece of state - the status - so we return that.

But you could create multiple different selectors to select different parts of the state. Since we have injected this store into our component, we can access that stream of status in the template.

<ng-container *ngIf="loginStore.status$ | async as status">
  <ion-button [disabled]="status === 'authenticating'" type="submit" expand="full">
    log in
    <ion-spinner *ngIf="status === 'authenticating'"></ion-spinner>
  </ion-button>
</ng-container>

Handling Effects

The other key part of this is updating the state. Now, the effects are the interesting part of this, but let’s take a look at making simple state updates first.

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { catchError, delay, EMPTY, Observable, switchMap, tap } from 'rxjs';
import { AuthService } from '../auth/auth.service';
import { LoginCredentials, LoginStatus } from './login.model';

export interface LoginState {
  status: LoginStatus;
}

@Injectable()
export class LoginStore extends ComponentStore<LoginState> {
  status$ = this.select((state) => state.status);

  login = this.effect((credentials$: Observable<LoginCredentials>) =>
    credentials$.pipe(
      tap(() => this.setState({ status: 'authenticating' })),
      switchMap((credentials) =>
        this.authService.login(credentials).pipe(
          tap({
            next: (success) => this.setState({ status: 'success' }),
            error: (err) => this.setState({ status: 'error' }),
            finalize: () => this.pending(),
          }),
          catchError(() => EMPTY)
        )
      )
    )
  );

  pending = this.effect(($) =>
    $.pipe(
      delay(1500),
      tap(() => this.setState({ status: 'pending' }))
    )
  );

  constructor(private authService: AuthService) {
    super({ status: 'pending' });
  }
}

There are a few ways you could do this, and we’ve set up a couple of examples.

In the first, we call setState and supply the new state - simple enough. In this case, we force the robot into the error state, which causes it to explode.

We’re doing the same thing in the second example, except we’re using the updater method instead. This is similar to a reducer in the full-blown NgRx Store if you’re familiar with that. It takes in the current state and a value, and then it returns the new state.

This is fine for synchronous updates, but now let’s imagine we want to subscribe to some observable and respond to that. Maybe we make an HTTP request to an API.

We could do that inside our methods here, but now we’re getting into the territory of having to manually manage subscriptions.

For this scenario, ComponentStore has the effect method. This takes in an observable of credentials. If we take a look at the component, you can see we call this method and provide it with the credentials from the form. It turns these values into an observable stream. Every time we call login with some value, that new value will be emitted on the stream.

Then, we use RxJS operators to respond to that stream however we want. We can create side effects on this stream with the tap operator, which reacts to values in the stream but leaves the stream unchanged, to update our state without ever needing to subscribe to anything.

There are some general RxJS concepts being used here, so if you aren’t familiar with RxJS, this is probably going to be a bit confusing. I’ll link to some additional resources on understanding RxJS operators in the video.

In this case, we instantly update the state to authenticating. Then we use switchMap to change our observable stream into the login observable stream from our auth service. We listen for values from that login stream. If the authentication succeeded, we set the state to success; if it errors, we set it to error.

As an addition from Chau, if the authentication errors, we don’t want the robot to be permanently dead, so this calls another effect with no input value that waits 1.5 seconds and then updates the status to pending, which resurrects our robot friend. Now all of this state management is neatly tucked away inside our store, and we can interact with it from our component.

Comparing with Other Approaches

So, how is this different from just using a service with a subject? It’s not really all that different, but ComponentStore does some of the heavy lifting for you in regard to managing observable streams, and it also provides a well-defined architecture to follow.

It gives you a bit more structure than a simple service but is not quite as complex as the full NgRx Store approach. It creates a nice middle ground where you can use it as a default solution for state management, and then if it becomes necessary, you can add in the global NgRx Store (and you can even use them together if you like - you could use ComponentStore to manage local state and Store for any state that needs to be shared globally).

It’s also worth mentioning that the architecture in this blog post is not the only way you can use ComponentStore. For example, instead of using a separate store service, it’s also possible to provide ComponentStore itself directly to the component. I’ll link to a great guide in the description of the video, which demonstrates different patterns you can use.

Learn to build modern Angular apps with my course