How to manage component state in Angular using @ngrx/component-store

Post Editor

In this tutorial, I will explain how to manage your component’s state with @ngrx/component-store. You will do it in a more organised way and minimise bugs and UI inconsistencies.

13 min read
0 comments
post

How to manage component state in Angular using @ngrx/component-store

In this tutorial, I will explain how to manage your component’s state with @ngrx/component-store. You will do it in a more organised way and minimise bugs and UI inconsistencies.

post
post
13 min read
0 comments
0 comments

Managing the state of your Angular application has always been a challenge. In this tutorial, I will explain how to manage your component’s state with @ngrx/component-store. You will do it in a more organised way and minimise bugs and UI inconsistencies.

What are we going to build?
Link to this section

An application to manage car parking and it will have the following parts:

  1. store.service: Where we will manage all our state and all the logic of the UI
  2. parking-lot.service: To communicate with the backend (for the demo)
  3. app.component: Parent component. We consume the state and add cars to the parking lot
  4. car-list.component: To show the list of parked cars

If you wish, you can jump to source code, without obligation.

What is the "state"?
Link to this section

First I want to let very clear what is a state and I quote:

“The state of an application (or anything else, really) is its condition or quality of being at a given moment in time - its state of being.”

Differe Stateful vs stateless is explained here.

It is important to highlight “at a given moment in time” because the state an application is constantly changing in different ways:

  • Networks request
  • User events
  • Changes in the router
  • Among others

Is very important to understand that there are different types of state:

  • Server state
  • Persistent state
  • The URL and router state
  • Client state
  • Local UI state

And there are more. Victor Savkin wrote an incredible post about it.

In this post we are going to focus on “Local UI state” more specifically in the component state.

“UI is the visual representation of the state of an app.”

In short, a state is:
Link to this section

  1. It is an object that represents the view of your component
  2. The data that comes from the server could be your state or be part of it depending on the UI requirements.
  3. It can have as many levels as you need
  4. It is immutable. When you need to update a property, you don’t change it directly but create a new object with the modified property

The problem
Link to this section

Sometimes we find ourselves creating a container component that coordinates several presentational components.

When you have multiples of these and each one with various inputs and outputs keeping the Local UI State changes under control can be a tedious task. Let me show you a way to manage without sweat.

Content imageContent image

The solution: @ngrx/component-store
Link to this section

The same NgRx team developed @ngrx/component-store and the official definition:

“A stand-alone library that helps to manage local/component state. It's an alternative to reactive push-based "Service with a Subject" approach.”

It allows you to keep all the business logic outside the component (or components) and only subscribes to the state and updates the UI when it changes.

The service you create by extending ComponentStore is unique to a particular component and its children and should be injected directly into the component’s providers property.

When to use an @ngrx/store or @ngrx/component-store?
Link to this section

In your application, you can use both. Both libraries complement each other.

  1. If the state needs to persist when you change the URL, that state goes into your global state
  2. If the state needs to be cleaned up when you change the URL that goes in your component store

More information in Comparison of ComponentStore and Store.

My recommendation
Link to this section

If you don’t have any state management in your app and want to start with one, I recommend starting with @ngrx/component-store and evaluating if you need something more complicated in the future.

In this way, you can start implementing state management in parts of your app and scale efficiently.

@ngrx/component-store concepts
Link to this section

It has only three very simple concepts that you have to learn:

  1. Selectors: You select and subscribe to the state, either all or parts of it
  2. Updater: To update the state. It can be parts or in whole
  3. Effects: It is also to update the state but do some other necessary task beforehand. For example, an HTTP request to an API

Getting started
Link to this section

The application will have a UI with three sections:

  1. Form to add the cart
  2. Table with parked cars
  3. Error messages
Content imageContent image

Creating utilities
Link to this section

First thing is to create a “Car” interface:

<>Copy
export interface Car { plate: string brand: string model: string color: string }

The above is the very basic model of the car.

Then you create a service that will communicate with the “backend” (only for the demo). You run the command:Inside the parking-lot.service.ts you add:

<>Copy
import { Injectable } from '@angular/core' import { Observable, of, throwError } from 'rxjs' import { delay } from 'rxjs/operators' import { Car } from '../models/car' const data: Car[] = [ { plate: '2FMDK3', brand: 'Volvo', model: '960', color: 'Violet', }, { plate: '1GYS4C', brand: 'Saab', model: '9-3', color: 'Purple', }, { plate: '1GKS1E', brand: 'Ford', model: 'Ranger', color: 'Indigo', }, { plate: '1G6AS5', brand: 'Volkswagen', model: 'Golf', color: 'Aquamarine', }, ] const FAKE_DELAY = 600 @Injectable({ providedIn: 'root', }) export class ParkingLotService { private cars: Car[] = [] constructor() {} add(plate: string): Observable<Car> { try { const existingCar = this.cars.find((eCar: Car) => eCar.plate === plate) if (existingCar) { throw `This car with plate ${plate} is already parked` } const car = this.getCarByPlate(plate) this.cars = [...this.cars, car] return of(car).pipe(delay(FAKE_DELAY)) } catch (error) { return throwError(error) } } private getCarByPlate(plate: string): Car { const car = data.find((item: Car) => item.plate === plate) if (car) { return car } throw `The car with plate ${plate} is not registered` } }

data: A list of the cars registered in our system. It will act as your car database for the demo.

FAKE_DELAY: To simulate a small delay to the API request using the delay operator from rxjs

Methods:

add: which receives the vehicle license plate and if it exists adds it to the list of parked cars and if it does not return an error.

getCarByPlate: this private method only searches our “database” (data) for the car using the plate, and if it does not exist, it throws an error.

Properties:

car: To keep track of the cars parked in the “backend”.

Defining the state
Link to this section

To define the state, let’s see the application requirements:

User will add cars by license plate (a request to an API)

You must indicate to the user the errors:

  • The vehicle plate does not exist in the API
  • The vehicle is already parked

You must show indicators in the UI when a request is happening

  • Loading: change the button text while the request happening
  • Disable: the button and the text field while the request happening
  • Show the error when it occurs

Based on these requirements, the state would be as follows:

<>Copy
export const enum LoadingState { INIT = "INIT", LOADING = "LOADING", LOADED = "LOADED" } export interface ErrorState { errorMsg: string; } export type CallState = LoadingState | ErrorState; // The state model interface ParkingState { cars: Car[]; // render the table with cars callState: CallState; }

ParkingState
Link to this section

  1. A list of parked cars
  2. And the callState property that will hold the loading and error state.

Adding the @ngrx/component-store
Link to this section

To add @ngrx/component-store to your app, use npm:

npm install @ngrx/component-store --save

Creating the store service
Link to this section

Create the file: app/store.service.ts and add the following code:

<>Copy
export const enum LoadingState { INIT = "INIT", LOADING = "LOADING", LOADED = "LOADED" } export interface ErrorState { errorMsg: string; } export type CallState = LoadingState | ErrorState; // The state model interface ParkingState { cars: Car[]; // render the table with cars callState: CallState; } // Utility function to extract the error from the state function getError(callState: CallState): LoadingState | string | null { if ((callState as ErrorState).errorMsg !== undefined) { return (callState as ErrorState).errorMsg; } return null; } @Injectable() export class StoreService extends ComponentStore<ParkingState> { constructor(private parkingLotService: ParkingLotService) { super({ cars: [], callState: LoadingState.INIT }); } }

This code is the base of your StoreService:

  1. We use the Injectable decorator (like any other service) and ComponentStore
  2. You created a ParkingState interface that defines the state of your component
  3. You created the StoreService class that extends from ComponentStore and pass the interface
  4. You initialized the UI state through the constructor makes the state immediately available to the ComponentStore consumers.

Now you are going to add the rest of the code, selects, updaters and effects. Your service code would be:

<>Copy
import { Injectable } from "@angular/core"; import { ComponentStore, tapResponse } from "@ngrx/component-store"; import { EMPTY, Observable } from "rxjs"; import { catchError, concatMap, tap } from "rxjs/operators"; import { Car } from "./models/car"; import { ParkingLotService } from "./services/parking-lot.service"; export const enum LoadingState { INIT = "INIT", LOADING = "LOADING", LOADED = "LOADED" } export interface ErrorState { errorMsg: string; } export type CallState = LoadingState | ErrorState; // The state model interface ParkingState { cars: Car[]; // render the table with cars callState: CallState; } // Utility function to extract the error from the state function getError(callState: CallState): LoadingState | string | null { if ((callState as ErrorState).errorMsg !== undefined) { return (callState as ErrorState).errorMsg; } return null; } @Injectable() export class StoreService extends ComponentStore<ParkingState> { constructor(private parkingLotService: ParkingLotService) { super({ cars: [], callState: LoadingState.INIT }); } // SELECTORS private readonly cars$: Observable<Car[]> = this.select(state => state.cars); private readonly loading$: Observable<boolean> = this.select( state => state.callState === LoadingState.LOADING ); private readonly error$: Observable<string> = this.select(state => getError(state.callState) ); // ViewModel for the component readonly vm$ = this.select( this.cars$, this.loading$, this.error$, (cars, loading, error) => ({ cars, loading, error }) ); // UPDATERS readonly updateError = this.updater((state: ParkingState, error: string) => { return { ...state, callState: { errorMsg: error } }; }); readonly setLoading = this.updater((state: ParkingState) => { return { ...state, callState: LoadingState.LOADING }; }); readonly setLoaded = this.updater((state: ParkingState) => { return { ...state, callState: LoadingState.LOADED }; }); readonly updateCars = this.updater((state: ParkingState, car: Car) => { return { ...state, error: "", cars: [...state.cars, car] }; }); // EFFECTS readonly addCarToParkingLot = this.effect((plate$: Observable<string>) => { return plate$.pipe( concatMap((plate: string) => { this.setLoading(); return this.parkingLotService.add(plate).pipe( tapResponse( car => { this.setLoaded(); this.updateCars(car); }, (e: string) => this.updateError(e) ), catchError(() => EMPTY) ); }) ); }); }

It’s quite a bit of code, so I will explain it to you in parts and start with the selectors.

Selectors
Link to this section

To create a selector, the select method is used as follows:

<>Copy
private readonly cars$: Observable<Car[]> = this.select(state => state.cars); private readonly loading$: Observable<boolean> = this.select( state => state.callState === LoadingState.LOADING ); private readonly error$: Observable<string> = this.select(state => getError(state.callState) );

The select method expects a function that receives the full state. With this state, we can return to the components what is needed; in this case, it returns the entire state.

In this app we created 3 private selectors:

  1. cars$: to get the array of cars
  2. loading$: to get a boolean while LoadingState.LOADING
  3. error$: to get the error message
<>Copy
// ViewModel for the component readonly vm$ = this.select( this.cars$, this.loading$, this.error$, (cars, loading, error) => ({ cars, loading, error }) );

And the fourth selector vm$ is the View Model for the component which collects all the data needed for the template. In this case we use the select method to combine other selectors. Yes! You can do that too.

Updaters
Link to this section

To update the state, you will need three updaters:

  1. To add or remove the error message
  2. To update the loading
  3. To add cars to the parking lot

To create updaters, use the update method provided by the ComponentStore class.

The method receives a function with two parameters, the first is the current state, and the second is the payload the component sent to update the state. This method only has to return the new state.

Error and loading
<>Copy
// UPDATERS readonly updateError = this.updater((state: ParkingState, error: string) => { return { ...state, callState: { errorMsg: error } }; }); readonly setLoading = this.updater((state: ParkingState) => { return { ...state, callState: LoadingState.LOADING }; }); readonly setLoaded = this.updater((state: ParkingState) => { return { ...state, callState: LoadingState.LOADED }; });

The updateError receives the error message and uses the spread operator to combine with the old state and return the new state.

The setLoading and setLoaded works the same as the previous one but with the callState with the LoadingState enum.

Add cars to parking

This updater receives a car and just adds it to the cars array using the spread operator. Also set the error property to empty string because if we add a car it means we have no errors.

<>Copy
readonly updateCars = this.updater((state: ParkingState, car: Car) => { return { ...state, error: "", cars: [...state.cars, car] }; });

IMPORTANT: When you update the state, you don't mutate the object (changing some property directly) but return a new object always.

Effects

To add a car to the parking lot, you have to create an effect because you have to make a request to an API with the car’s license plate, and when it responds, the state is updated.

We use the effect method that receives a callback with the value that we pass as an Observable to create effects. Keep in mind that each new call of the effect would push the value into that Observable.

<>Copy
// EFFECTS readonly addCarToParkingLot = this.effect((plate$: Observable<string>) => { return plate$.pipe( concatMap((plate: string) => { this.setLoading(); return this.parkingLotService.add(plate).pipe( tapResponse( car => { this.setLoaded(); this.updateCars(car); }, (e: string) => this.updateError(e) ), catchError(() => EMPTY) ); }) ); });

In this code, you can see that the effect:

  1. Receive the car license plate as an Observable
  2. Update the state of loading
  3. Request the API to add the car to the parking lot using the ParkingLotService

Using concatMap so that if the effect gets called multiple times before the call ends, it will resolve all the calls. This RxJS operator will wait until the previous request completes to do the next one.

tapResponse which is also provided from the @ngrx/component-store package. Allows you to handle the response of the effect without any extra boilerplate and It enforces that the error case is handled and that the effect would still be running even if an error occurs.

And the catchError to handle potential errors within the internal pipe.

Creating the component
Link to this section

In the components/car-list.component.ts file, add the following code:

<>Copy
import { Component, Input } from '@angular/core' import { Car } from '../../models/car' @Component({ selector: 'app-car-list', templateUrl: './car-list.component.html', styleUrls: ['./car-list.component.css'], providers: [], }) export class CarListComponent { @Input() cars: Car[] = [] constructor() {} }

In the components/car-list.component.html file, add the following code:

<>Copy
<table *ngIf="cars.length; else noCars"> <tr> <th>Plate</th> <th>Brand</th> <th>Model</th> <th>Color</th> </tr> <ng-template ngFor let-car [ngForOf]="cars" let-i="index"> <tr> <td>{{car.plate}}</td> <td>{{car.brand}}</td> <td>{{car.model}}</td> <td>{{car.color}}</td> </tr> </ng-template> </table> <ng-template #noCars> <p>No cars in the parking lot</p> </ng-template>

Finally, make sure that angular-cli added the car-list component to the module.

Open the app/app.module.ts file, look into the declarations array, and if it is not there, you can add the CarListComponent class manually.

Adding the FormModule
Link to this section

As you are going to have a small form with [(ngModel)] in the app.component, you must add the FormModule to the app.module.

Open the app/app.module.ts file and add the FormsModule to the imports array. The final code looks like this:

<>Copy
import { BrowserModule } from '@angular/platform-browser' import { NgModule } from '@angular/core' import { AppComponent } from './app.component' import { CarListComponent } from './components/car-list/car-list.component' import { FormsModule } from '@angular/forms' @NgModule({ declarations: [AppComponent, CarListComponent], imports: [BrowserModule, FormsModule], bootstrap: [AppComponent], }) export class AppModule {}

Consuming the store service
Link to this section

You created the Store Service specifically for the app.component and its children.

app/app.component.ts

Add replace all the code with:

<>Copy
import { Component } from '@angular/core' import { StoreService } from './store.service' @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [StoreService], }) export class AppComponent { plate = '' vm$ = this.store.vm$ constructor(private store: StoreService) {} onSubmit($event: Event) { $event.preventDefault() this.store.addCarToParkingLot(this.plate) this.plate = ""; } addPlate($event: Event) { const target = $event.target as HTMLButtonElement if (target.nodeName === 'BUTTON') { this.plate = target.innerHTML } } }

StoreService handles all the business logic, which results in a tiny component. Let’s see the code part by part:

Providers

providers: [StoreService]: You inject the service at the component level so that this instance only has this component and its children.

Properties

plate: For the form model, the user will enter the car plate to add to the parking lot.

vm$ It is the observable state from our StoreService and is updated every time the state changes. We will subscribe to this in the HTML in the next step.

Methods

constructor(private store: StoreService) {}: You inject the StoreService into the constructor, just like a regular service.

onSubmit(): You call it when the form is submitted, and the only thing it does is call the store method addCarToParkingLot (effect) with the car plate entered by the user in the form.

addPlate(): This method is unnecessary, but for demo purposes, I added it to enter some plates by clicking on some buttons.

app/app.component.html

Add replace all the code with:

<>Copy
<header> <h1>Parking Lot Control</h1> </header> <ng-container *ngIf="vm$ | async as vm"> <div class="messages"> <p class="error" *ngIf="vm.error">{{vm.error}}</p> </div> <div class="box"> <form (submit)="onSubmit($event)"> <input type="text" [(ngModel)]="plate" [ngModelOptions]="{standalone: true}" placeholder="Ex: 2FMDK3, 1GYS4C, 1GKS1E,1G6AS5" [disabled]="vm.loading" /> <button type="submit" [disabled]="vm.loading || !plate.length"> <ng-container *ngIf="vm.loading; else NotLoading"> Loading... </ng-container> <ng-template #NotLoading> Add Car </ng-template> </button> </form> <div class="shortcuts"> <h5>Shortcuts</h5> <p (click)="addPlate($event)" class="examples"> <button>2FMDK3</button> <button>1GYS4C</button> <button>1GKS1E</button> <button>1G6AS5</button> </p> </div> </div> <app-car-list [cars]="vm.cars"></app-car-list> </ng-container>

<ng-container *ngIf="vm$ | async as vm">: The first thing is to obtain the ViewModel of the vm$ property that we created in the component class, we use async pipe to subscribe, and we make a static variable vm that the rest of our HTML will be able to use.

Error message

The error is a string, so we just have to show it in the HTML and using interpolation:

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

Form

We create a form for the user to enter the car’s plate that they want to add to the parking lot, and we bind the onSubmit event.

<form (submit)="onSubmit()">

It is a small form with a textfield for the user to enter the plate and a button to execute the add action.

<input>: Enable/disable based on the state’s loading property.

<button>: It is enabled/disabled with the loading property of the state but also if the plate property of the component is empty (it prevents an empty string from being sent to the store service)

In the onSubmit method of the component, we call the effect with the plate number entered by the user, and this is where our ComponentStore service does everything.

That's it
Link to this section

Go to your browser: https://localhost:4200 and see your app working.

Summary
Link to this section

  1. You created a service that communicates with the API: ParkingLotService
  2. You created a service that handles all the logic and state of the StoreService component that extends ComponentStore
  3. Your UI subscribes to the state of StoreService, and every time it changes, your UI is updated.

Using this approach, you will end up with a single “source of truth” for your UI, easy to use without having to change code in many places to update or improve.

Conclusion
Link to this section

As you could see, it is better to start managing the state at the component level before jumping to a complete architecture.

A state is simply an object representing how your interface looks like, and using @ngrx/component-store and its three basic concepts: select, update and effect, you can handle it in a simple, direct, and more painless way test.


Comments (0)

Be the first to leave a comment

Share

Featured articles