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.