I Don’t Need a State Manager in Angular, or am I just delaying his arrival?

When we build an Angular App, the communication between components is something to take care of. We can start using parent-to-child with Input and Output events; the lack of input and output communication is the only parent-to-child or vice versa, but …


This content originally appeared on DEV Community and was authored by Dany Paredes

When we build an Angular App, the communication between components is something to take care of. We can start using parent-to-child with Input and Output events; the lack of input and output communication is the only parent-to-child or vice versa, but when the app starts to grow with routing and more than parent and children components. Hence, the next step is to use Angular Services with Rxjs.

Rxjs services work fine in small applications with little state. Inject a service with a BehaviorSubject property and subscribe to keep it in sync, but if your app has many entities, settings, and user configurations, and these changes need to reflect or react in several components, only using services becomes a bit hard to maintain or maybe not.

We must be aware of the responsibility of a single component or when to refactor it. Creating a new component for a specific scope and taking care of when a single component has many services injected is a red flag. Let's show a small example.

Scenario

We must build an app with the following sections the home, profile, orders, and payment.

  • Each section must allow saving data in the state.

  • The home must render data from each section.

  • Each Order discount from the balance.

We will use Rxjs Behavior subject to keep state and some rxjs operators to simplify and combine some operators.

Setup The Project

First, using the Angular CLI, create the project:

> ng new angular-and-states
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? CSS

When finished, go to the new directory from the terminal and create two pages, home and settings using the Angular/CLI and running the command ng g c and the component name.

PS C:\Users\dany.paredes\Desktop\angular-and-states> ng g c /pages/payments
CREATE src/app/pages/payments/payments.component.html (23 bytes)
CREATE src/app/pages/payments/payments.component.spec.ts (640 bytes)
CREATE src/app/pages/payments/payments.component.ts (283 bytes)
CREATE src/app/pages/payments/payments.component.css (0 bytes)
UPDATE src/app/app.module.ts (774 bytes)
PS C:\Users\dany.paredes\Desktop\angular-and-states> ng g c /pages/orders  
CREATE src/app/pages/orders/orders.component.html (21 bytes)
CREATE src/app/pages/orders/orders.component.spec.ts (626 bytes)
CREATE src/app/pages/orders/orders.component.ts (275 bytes)
CREATE src/app/pages/orders/orders.component.css (0 bytes)
UPDATE src/app/app.module.ts (862 bytes)
PS C:\Users\dany.paredes\Desktop\angular-and-states> ng g c /pages/profile
CREATE src/app/pages/profile/profile.component.html (22 bytes)
CREATE src/app/pages/profile/profile.component.spec.ts (633 bytes)
CREATE src/app/pages/profile/profile.component.ts (279 bytes)
CREATE src/app/pages/profile/profile.component.css (0 bytes)
UPDATE src/app/app.module.ts (954 bytes)

For the navigation, create the component navigation using the same command but in the path components:

ng g c /components/navigation

Update the navigation.component.html markup using the directives to navigate:

<a [routerLink]="['']">Home</a>
<a [routerLink]="['payments']">Payments</a>
<a [routerLink]="['orders']">Orders</a>
<a [routerLink]="['profile']">Profile</a>

Next, remove the default markup in the app.component.html and add the following markup with <router-outlet></router-outlet>

<h1>App</h1>
<app-navigation></app-navigation>
<router-outlet></router-outlet>

Add the routes in the app-routing.module.ts

const routes: Routes = [
  {
    component: HomeComponent,
    path: ''
  },
  {
    component: OrdersComponent,
    path: 'orders'
  },

  {
    component: PaymentsComponent,
    path: 'payments'
  },
  {
    component: ProfileComponent,
    path: 'profile'
  }
];

Save and run the command ng s -o, and the app must render the home component with navigation.

Adding State with Services

We need to save the state for each section, creating a service for each one 'profile', 'orders', and ' payments',

Using the angular/cli run ng g s /services/profile to share data about profile between components using BehaviorSubject.

Read more about BehaviorSubject

Create two properties: -nameSubject$ ss an instance of BehaviorSubject, and it's initialized with the null value.

  • The name$ public property that exposes the nameSubject as an observable.
@Injectable({
  providedIn: 'root'
})
export class ProfileService {

  private nameSubject = new BehaviorSubject<string | null>(null);
  public name$ = this.nameSubject.asObservable()

  public saveName(name: string) {
    const message = `Hi ${name} `
    this.nameSubject.next(message);
  }

}

Repeat the process for PaymentService, which holds the account balance.

ng g s /services/payments
CREATE src/app/services/payments.service.spec.ts (367 bytes)
CREATE src/app/services/payments.service.ts (137 bytes)

Create two properties, paymentSubject$ starting with balance in 0 and paymentBalance$, and add two methods:

  • addBalance Save the balance in the behaviorSubject.

  • updateBalance Decrease the currentBalance.

import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class PaymentsService {

  private paymentSubject$ = new BehaviorSubject<number | null>(null);
  public paymentBalance$ = this.paymentSubject.asObservable()

  public updateBalance(balance: number) {
    const currentBalance = this.paymentSubject.getValue();
    if (currentBalance) {
      const totalBalance = currentBalance - balance;
      this.paymentSubject$.next(totalBalance);
    }
  }

  public addBalance(balance: number) {
    this.paymentSubject$.next(balance);
  }

}

Finally, create the OrdersServices to hold all orders.

ng g s /services/orders  
CREATE src/app/services/orders.service.spec.ts (357 bytes)
CREATE src/app/services/orders.service.ts (135 bytes)

The service contains an array of orderIds.

import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs/internal/BehaviorSubject';

@Injectable({
  providedIn: 'root'
})
export class OrdersService {

  private orderSubject = new BehaviorSubject<number[]>([]);
  public orders$ = this.orderSubject.asObservable()

  public addOrder(orderId: number) {
    const orders = [...this.orderSubject.value, orderId]
    this.orderSubject.next(orders);
  }
}

We already have our services states; next, we will use these services in the components.

Using Services State In Components

We have the services to save the data. Our next step is to use it on each page. The process is to inject the service into the component and use the method and the subject to get the value from the service.

The ProfileComponent, allows saving his name, storing it in the observable, and providing the saveName method from the service.

First, inject the service ProfileService, and add a new variable name$ to link with the observable name$ from the service.

Next, add a new method, save, with the parameter name, in the method body, to use the saveName from the service.

The final code looks like this:

import { ProfileService } from './../../services/profile.service';
import { Component, inject, OnInit } from '@angular/core';

@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.css'],
})
export class ProfileComponent {
  name$ = this.profileService.name$;

  constructor(private profileService: ProfileService) {}

  save(name: string) {
    this.profileService.saveName(name);
  }
}

In the component HTML markup, add a new input with the template variable #name to get access to the input value, and add a button to call the save method, passing the variable name.value refers to the method.

To render the name, use the pipe async subscribe to name$ observable to show the nameSaved in the service.

The final code looks like this:

<h3>Please type your Name:</h3>
<input #name type="text">
<button (click)="save(name.value)">Save</button>
<span *ngIf="(name$ | async ) as nameSaved">{{nameSaved}}</span>

We already set up the state for the profile section, saved the changes, and ran the app with ng s -o. It opens the app to the profile section, adds your name, and moves between other sections, storing the name.

Repeat the same steps for payment.

  • Add the services

  • Add input to Markup for the user to add the value.

Payment Component

import {PaymentsService} from './../../services/payments.service';
import {Component} from '@angular/core';

@Component({
  selector: 'app-payments',
  templateUrl: './payments.component.html',
  styleUrls: ['./payments.component.css']
})
export class PaymentsComponent {

  balance$ = this.paymentService.paymentBalance$;

  constructor(private paymentService: PaymentsService) {

  }

  updateBalance(balance: HTMLInputElement) {
    this.paymentService.addBalance(balance.valueAsNumber);
  }

}

Update the HTML Markup with input and button, and subscribe to the balance$ observable.

<h2>Add balance:</h2>
<input #payment type="number">
<button (click)="updateBalance(payment)">Update</button>
<span *ngIf="(balance$ | async ) as balance">You current Balance is: {{balance$ | currency}}</span>

Orders Component

Repeat the process with orders but with minor changes, The only difference is the orders are an array, and we use ngFor to render all sections.

import { Component, OnInit } from '@angular/core';
import { OrdersService } from 'src/app/services/orders.service';

@Component({
  selector: 'app-orders',
  templateUrl: './orders.component.html',
  styleUrls: ['./orders.component.css'],
})
export class OrdersComponent {
  orders$ = this.ordersService.orders$;

  constructor(private ordersService: OrdersService) {}

  addOrder(order: HTMLInputElement) {
    if (order.value) {
      this.ordersService.addOrder(order.valueAsNumber);
      order.value = '';
    }
  }
}
<h2>Add your order</h2>
<input #order type="number">
<button (click)="addOrder(order)">add order</button>
<div *ngIf="(orders$ | async ) as orders">
  Your current active orders are:
  <div *ngFor="let order of orders">
    {{order}}
  </div>
</div>

Finally, we have a state in our entities, and the next challenge is to read each service's value to show the data in the home component.

Combine States and Keep Sync

We have three values in our application:

  • payment balance.

  • profile info

  • orders

Our first challenge when adding a new order in the OrderComponent to discount the balance in paymentService.

In the orders.component, we must make the following points:

  • Add paymentService and ordersService in the constructor.

  • Declare a new variable for storing the currentBalance.

  • Edit the addOrder to update the balance with 2 when submitting the order.

The final code looks like this:

import {PaymentsService} from '../../services/payments.service';
import {Component} from '@angular/core';
import {OrdersService} from 'src/app/services/orders.service';
import {startWith} from 'rxjs/operators';

@Component({
  selector: 'app-orders',
  templateUrl: './orders.component.html',
  styleUrls: ['./orders.component.css'],
})
export class OrdersComponent {
  orders$ = this.ordersService.orders$;
  currentBalance$ = this.paymentService.paymentBalance$

  constructor(
    private ordersService: OrdersService,
    private paymentService: PaymentsService
  ) {

  }

  addOrder(order: HTMLInputElement) {
    this.ordersService.addOrder(order.valueAsNumber);
    this.paymentService.updateBalance(2);
    order.value = '';
  }
}

In the HTML Markup, add subscribe to currentBalance$. If not, show the template noBalance and disable the button if balance= 0, final code looks like this:

<div *ngIf="currentBalance$ | async as balance; else noBalance">
  <h2>You have {{balance | currency }} as balance, add your orders</h2>
  <input #order type="number">
  <button (click)="addOrder(order)" [disabled]="balance <= 0">add order</button>
</div>
<ng-template #noBalance>
  <h2> Insuficient Balance, please add.</h2>
</ng-template>
<div *ngIf="orders$ | async  as orders">
  You have ({{orders.length}}) orders.
  <div *ngFor="let order of orders">
    {{order}}
  </div>
</div>

Save the Changes, and the app reloads; add an initial balance, and when you add the order, the balance for each order.

Combine All States

The challenge with Orders was simple, we added a single service, but what do we want to get data from all services?

Rxjs provide a few operators to combine Observables, like merge and concat, but in our scenario, we use combineLatest.

We combine each observable in a single object using the map operator and consume it in the template.

  • Inject ProfileService, OrdersService, and PaymentsService

  • Use the combineLatest operator to combine each service data in a single object and subscribe to the template

import { Component } from '@angular/core';
import { combineLatest } from 'rxjs';
import { OrdersService } from 'src/app/services/orders.service';
import { PaymentsService } from 'src/app/services/payments.service';
import { ProfileService } from 'src/app/services/profile.service';
import { map } from 'rxjs/operators';
@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
})
export class HomeComponent {
  customerStatus$ = combineLatest([
    this.paymentService.paymentBalance$,
    this.orderService.orders$,
    this.profileService.name$,
  ]).pipe(
    map(([balance, orders, profileName]) => ({ balance, orders, profileName }))
  );

  constructor(
    private profileService: ProfileService,
    private orderService: OrdersService,
    private paymentService: PaymentsService
  ) { }
}

The HTML Markup subscribe to the observable using the pipe async provides the name, balance, and orders.

<div *ngIf="customerStatus$ | async as customerStatus">
  <p>Hey! {{customerStatus.profileName}}, your balance is {{ customerStatus.balance | currency }}
    in {{customerStatus.orders.length}}</p>

  <div *ngFor="let order of customerStatus.orders">
    {{ order }}
  </div>

</div>

Save changes, and the browser reloads when each observable emits the home component to get the data for each one.

We have three injections in the home.component.ts to provide the component information. It works but is too much noise in the home.component.ts; maybe we can simplify it.

Centralize The Behavior Subject

One solution is to create appService; it provide all state required by our app; instead of injecting all services into each page, we have a single entry point. Let start:

  • Create a new service AppService

  • Inject ProfileService, OrdersService, and PaymentsService

  • Use the combineLatest operator to combine each service data in a single object and expose it in the property customerInfo$

  • Add two property orders$ and balance$.

  • Add the method addOrder to update the balance and the orders.

import {Injectable} from '@angular/core';

import {ProfileService} from "./profile.service";
import {OrdersService} from "./orders.service";
import {PaymentsService} from "./payments.service";
import {map} from 'rxjs/internal/operators/map';
import {combineLatest} from 'rxjs';


@Injectable({
  providedIn: 'root'
})
export class AppService {
  public customerAndBalance$ = combineLatest([
    this.paymentService.paymentBalance$,
    this.profileService.name$,
  ]).pipe(
    map(([balance, name]) => ({balance, name}))
  );

  customer$ = this.profileService.name$;
  orders$ = this.orderService.orders$;
  balance$ = this.paymentService.paymentBalance$;

  constructor(
    private profileService: ProfileService,
    private orderService: OrdersService,
    private paymentService: PaymentsService
  ) {
  }

    //comment with ngJörger 
  addOrder(order: number) {
    this.orderService.addOrder(order);
    this.paymentService.updateBalance(1);
  }
}

In the home.component.html remove the services, and add the appService in the constructor, and update the reference of customerStatus to point to the customerInfo$ method from appService.

Save, and everything continues working as expected.

import {Component} from '@angular/core';
import {HomeService} from "./home.service";

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
})
export class HomeComponent {
  customerStatus$ = this.appService.customerInfo$;

  constructor(private appService: AppService) {
  }

}

The home service has fewer dependencies, so our code looks clean. We can simplify our solution and delegate some responsibility to a specific component.

Component Responsibility

The home page has two sections, which repeat in pages, the username with the balance and the list of orders.

Let's start with the OrderListComponent, using the Angular/CLI to generate a new component ng g c /components/orderlist:

  • Inject the AppService in the constructor.

  • Declare an orders$ observable to read all orders in the services.

  • Open the HTML Markup and subscribe to the orders$ observable using the pipe async and iterate with the ngFor.

The final code looks like this:

import {Component} from '@angular/core';
import {AppService} from "../../services/app.service";

@Component({
  selector: 'app-order-list',
  templateUrl: './order-list.component.html',
  styleUrls: ['./order-list.component.css']
})
export class OrderListComponent {

  orders$ = this.appService.orders$;

  constructor(private appService: AppService) {
  }

}
<div *ngIf="orders$ | async  as orders">
  You have ({{orders.length}}) orders.
  <div *ngFor="let order of orders">
    {{order}}
  </div>
</div>

The exact process with the customer message creates a component with Angular/CLI.

  • Run the command ng g c /components/customer-message

  • Inject the AppService in the constructor

  • Declare a customerInfo$ observable to read the customerAndBalance$ value.

  • Open the HTML Markup and subscribe to the customerInfo$ observable using the pipe async.

The final code looks like this:

import {Component} from '@angular/core';
import {AppService} from "../../services/app.service";


@Component({
  selector: 'app-customer-message',
  templateUrl: './customer-message.component.html',
  styleUrls: ['./customer-message.component.css']
})
export class CustomerMessageComponent {

  public customerInfo$ = this.appService.customerAndBalance$;

  constructor(private appService: AppService) {
  }
}
<div *ngIf="customerInfo$ | async as customer">
  <div *ngIf="customer.name && customer.balance; else updateInfo">
    <p>Hey! {{customer.name}}, your balance is {{ customer.balance | currency }}</p>
  </div>
</div>
<ng-template #updateInfo>
  Please add your name and balance
</ng-template>

We already inject the AppService orderList and customerMessage, which helps simplify the app and refactor other components.

Refactor Components

We have two components to simplify the home and orders.

The Home component works like a container for the customerMessage and orderList components, so lets to clean up:

  • Remove the appService injection from the constructor.

  • In the template, use the customer-message and listOrder components.

The Final Code looks like this:

import {Component} from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
})
export class HomeComponent {

}
<app-customer-message></app-customer-message>
<app-order-list></app-order-list>

The Orders component becomes simple using the AppService and the orderlist component.

  • Inject the AppService in the constructor

  • Declare a balance$ observable to read the balance value from AppService.

  • Update the addOrder method to call the same from appService.

  • Open the HTML Markup and subscribe to the balance$ observable using the pipe async.

  • Add the customer and orderlist component to show the user data and list of orders.

The final code looks like this:

import {Component} from '@angular/core';
import {AppService} from "../../services/app.service";

@Component({
  selector: 'app-orders',
  templateUrl: './orders.component.html',
  styleUrls: ['./orders.component.css'],
})
export class OrdersComponent {


  balance$ = this.appService.balance$

  constructor(
    private appService: AppService
  ) {

  }

  addOrder(order: HTMLInputElement) {
    this.appService.addOrder(order.valueAsNumber)
    order.value = '';
  }
}
<div *ngIf="balance$ | async as balance; else noBalance">
  <app-customer-message></app-customer-message>
  <input #order type="number">
  <button (click)="addOrder(order)" [disabled]="balance <= 0">add order</button>
</div>
<ng-template #noBalance>
  <h2> Insufficient Balance, please add.</h2>
</ng-template>

<app-order-list></app-order-list>

What I Learn

It was a tiny example; we had time to review and take time without time, market, team speed, and company pressure.

  • Use Rxjs Observable, which allows us to build a reactive application fast without too much complexity.

  • When each entity has its service to keep the state, if one component needs all information, it requires injecting all of them.

  • To reduce the amount of injection, we combine all in "bridge" services to connect with multiple behavior subjects like AppService

  • We must detect which components can work isolated, provide functionality without too many dependencies, and simplify our components.

  • Using the pattern smart and dump components help to simplify the component's composition.

Do I need a State Manager?

If your app is an MVP or small like this, the BehaviorSubject is ok; I want to take some time to build the same app again using some state manager like NgRx or NGXS.

See you soon!

Photo by Agê Barros on Unsplash


This content originally appeared on DEV Community and was authored by Dany Paredes


Print Share Comment Cite Upload Translate Updates
APA

Dany Paredes | Sciencx (2023-02-23T18:23:51+00:00) I Don’t Need a State Manager in Angular, or am I just delaying his arrival?. Retrieved from https://www.scien.cx/2023/02/23/i-dont-need-a-state-manager-in-angular-or-am-i-just-delaying-his-arrival/

MLA
" » I Don’t Need a State Manager in Angular, or am I just delaying his arrival?." Dany Paredes | Sciencx - Thursday February 23, 2023, https://www.scien.cx/2023/02/23/i-dont-need-a-state-manager-in-angular-or-am-i-just-delaying-his-arrival/
HARVARD
Dany Paredes | Sciencx Thursday February 23, 2023 » I Don’t Need a State Manager in Angular, or am I just delaying his arrival?., viewed ,<https://www.scien.cx/2023/02/23/i-dont-need-a-state-manager-in-angular-or-am-i-just-delaying-his-arrival/>
VANCOUVER
Dany Paredes | Sciencx - » I Don’t Need a State Manager in Angular, or am I just delaying his arrival?. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/02/23/i-dont-need-a-state-manager-in-angular-or-am-i-just-delaying-his-arrival/
CHICAGO
" » I Don’t Need a State Manager in Angular, or am I just delaying his arrival?." Dany Paredes | Sciencx - Accessed . https://www.scien.cx/2023/02/23/i-dont-need-a-state-manager-in-angular-or-am-i-just-delaying-his-arrival/
IEEE
" » I Don’t Need a State Manager in Angular, or am I just delaying his arrival?." Dany Paredes | Sciencx [Online]. Available: https://www.scien.cx/2023/02/23/i-dont-need-a-state-manager-in-angular-or-am-i-just-delaying-his-arrival/. [Accessed: ]
rf:citation
» I Don’t Need a State Manager in Angular, or am I just delaying his arrival? | Dany Paredes | Sciencx | https://www.scien.cx/2023/02/23/i-dont-need-a-state-manager-in-angular-or-am-i-just-delaying-his-arrival/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.