author

Kien Duong

May 2, 2022

Manage states and data in Angular project

Angular is one of the great frameworks for building application at client side. For a client-side application, we have two types of data that need to be managed:

  • The data from APIs
  • The states inside the app.

First of all, we have to setup a basic Angular application (the reference structure)

manage states data angular 1

In this blog, we will use the public RESTful APIs from CoinCap https://docs.coincap.io/#ee30bea9-bb6b-469d-958a-d3e35d442d7a Inside the services folder, we will create 3 new services:

  • api-base.service.ts
  • coincap-api.service.ts
  • helper.service.ts

The idea is that all api services will extend ApiBaseService service. HelperService will handle helper functions.

    // api-base.service.ts
import { Injectable } from '@angular/core';

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

  protected makeUrl(endPoint: string, params: any = {}, queryParams: any = {}): string {
    let url = endPoint;

    if (params && Object.keys(params).length) {
      Object.keys(params).forEach((key) => {
        url = url.replace(`:${key}`, params[key]);
      });
    }

    if (queryParams && Object.keys(queryParams).length) {
      let index = 0;
      let query = '';

      for (const key in queryParams) {
        if (queryParams.hasOwnProperty(key)) {
          index = index + 1;
          if (index === Object.keys(queryParams).length) {
            query += `${key}=${queryParams[key]}`;
          } else {
            query += `${key}=${queryParams[key]}&`;
          }
        }
      }

      url = `${url}?${query}`;
    }

    return url;
  }
}

  
    // coincap-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

// Services
import { ApiBaseService } from  '../api-base/api-base.service';

// Env
import { environment } from '../../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class CoincapApiService extends ApiBaseService {

  private apiBase: string;

  constructor(
    private http: HttpClient,
  ) {
    super();
    this.apiBase = environment.coincapApiBase;
  }

  assets(): Observable<any> {
    const url = this.makeUrl(`${this.apiBase}/assets`);
    return this.http.get(url);
  }

  exchanges(): Observable<any> {
    const url = this.makeUrl(`${this.apiBase}/exchanges`);
    return this.http.get(url);
  }

}

  
    // helper.service.ts
import { Injectable } from '@angular/core';

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

  constructor() { }

  getFloatNumber(value: string) {
    return parseFloat(value).toLocaleString('en-US', { maximumFractionDigits: 2 });
  }

}

  

The next step, create a new component to handle the data from APIs. We can use a UI component library to develop faster. In this blog, we have been using PrimeNG UI components library https://www.primefaces.org/primeng/ Based on Angular lifecycle, we must subscribe list exchanges endpoint at ngOnInit and don’t forget unsubscribe data at ngOnDestroy. So now we will be see list exchanges

manage states data angular 2

That’s a normal way to handle the APIs data. Next step, we want to show you another way to manage the data by using NgRx https://ngrx.io/guide/store that is a state management library. Like Redux Saga, we will be able to handle side effects when doing an action.

manage states data angular 3

Inside the states folder, we must create some files to manage coicap states

manage states data angular 4

    // coincap.action.ts
import { createAction, props } from '@ngrx/store';

// Interfaces
import { ICoincapAsset } from '../../interfaces';

export const fetchListCoins = createAction('[Coincap] Fetch list coins');

export const fetchListCoinsSuccess = createAction(
  '[Coincap] Fetch list coins successfully',
  props<{ data: ICoincapAsset[] }>()
);

export const fetchListCoinsFailed = createAction(
  '[Coincap] Fetch list coins failed',
  props<{ error: any }>()
);
  

In the coincap.action.ts file, you can see that we’ve defined three actions:

  • fetchListCoins
  • fetchListCoinsSuccess
  • fetchListCoinsFailed

In the coincap.effect.ts, NgRx will listen fetchListCoins action if it is called from somewhere in the app. After that make a request to assets endpoint. If the endpoint responses the data, fetchListCoinsSuccess action will be called and update the success data to the states in coincap.reducer.ts, if not fetchListCoinsFailed will be called.

In order to get the data, you can subscribe selectCoincapData in the coincap.selector.ts from anywhere inside the application.

    // coincap.effect.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { switchMap, map, catchError } from 'rxjs/operators';

// Services
import { CoincapApiService } from '../../services/coincap-api/coincap-api.service';

// Actions
import * as coincapActions from './coincap.action';

@Injectable()
export class CoincapEffects {

  constructor(
    private actions$: Actions,
    private coincapApiService: CoincapApiService,
  ) { }

  fetchListCoins$ = createEffect(() =>
    this.actions$.pipe(
      ofType(coincapActions.fetchListCoins.type),
      switchMap(() =>
        this.coincapApiService.assets().pipe(
          map((data) => {
            return coincapActions.fetchListCoinsSuccess({ data: data.data });
          }),
          catchError((error) =>
            of(coincapActions.fetchListCoinsFailed({ error: error }))
          )
        )
      )
    )
  );

}
  
    // coincap.reducer.ts
import { Action, createReducer, on } from '@ngrx/store';
import * as coincapActions from './coincap.action';
import { initialState, CoincapState } from './coincap.state';

const coincapReducer = createReducer(
  initialState,
  on(coincapActions.fetchListCoinsSuccess, (state, { data }) => ({
    ...state,
    data,
  })),
);

export function reducer(state: CoincapState | undefined, action: Action) {
  return coincapReducer(state, action);
}
  
    // coincap.selector.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { CoincapState } from './coincap.state';

export const selectCoincap = createFeatureSelector<CoincapState>('coincap');

export const selectCoincapData = createSelector(
  selectCoincap,
  (state: CoincapState) => state.data
);

  
    // coincap.state.ts
// Interfaces
import { ICoincapAsset } from '../../interfaces';

export interface CoincapState {
  data: ICoincapAsset[];
}

export const initialState: CoincapState = {
  data: [],
};
  

Come back to the home component, we create the logic to subscribe the data from NgRx states this.store.select(selectCoincapData)

manage states data angular 5

    // home.component.ts
import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { Store } from '@ngrx/store';

// Services
import { CoincapApiService } from '../../services/coincap-api/coincap-api.service';
import { HelperService } from '../../services/helper/helper.service';

// States
import { selectCoincapData } from '../../states/coincap';

// Interfaces
import { ICoincapAsset, ICoincapExchange, ITableColumn } from '../../interfaces';

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

  public coinColumns: ITableColumn[] = [];
  public exchangeColumns: ITableColumn[] = [];
  public listCoins: ICoincapAsset[] = [];
  public listExchanges: ICoincapExchange[] = [];

  private subscriptions: Subscription[] = [];

  constructor(
    private store: Store,
    private coincapApiService: CoincapApiService,
    private helperService: HelperService,
  ) { }

  ngOnInit() {
    this.coinColumns = [
      { field: 'rank', header: 'Rank' },
      { field: 'id', header: 'ID' },
      { field: 'symbol', header: 'Symbol' },
      { field: 'name', header: 'Name' },
      { field: 'priceUsd', header: 'Price' },
    ];

    this.exchangeColumns = [
      { field: 'rank', header: 'Rank' },
      { field: 'id', header: 'ID' },
      { field: 'exchangeUrl', header: 'Url' },
      { field: 'volumeUsd', header: 'Volume' },
    ];

    this.subscriptions.push(
      this.store.select(selectCoincapData).subscribe(res => {
        if (res && res.length) {
          this.listCoins = res;
        }
      })
    );

    this.getListCoins();
  }

  public getFloatNumber(value: string) {
    return this.helperService.getFloatNumber(value);
  }

  private async getListCoins() {
    try {
      const result = await this.handleListExchanges();
      this.listExchanges = result.data;
    } catch (e) {
      console.log(e);
    }
  }

  private handleListExchanges(): Promise {
    return new Promise((resolve, reject) => {
      this.subscriptions.push(
        this.coincapApiService.exchanges().subscribe(res => {
          resolve(res);
        }, err => {
          reject(err);
        })
      );
    });
  }

  ngOnDestroy() {
    if (this.subscriptions.length) {
      this.subscriptions.map(sub => {
        if (sub) {
          sub.unsubscribe();
        }
      });
    }
  }

}

  
    // home.component.html
<div class="container home-content">
  <div class="row">
    <div class="col-md-6 col-12">
      <div class="table">
        <h1>List Coins</h1>

        <p-table [columns]="coinColumns" [value]="listCoins">
          <ng-template pTemplate="header" let-columns>
            <tr>
              <th *ngFor="let col of columns">
                {{ col.header }}
              </th>
            </tr>
          </ng-template>
    
          <ng-template pTemplate="body" let-rowData let-columns="columns">
            <tr>
              <td *ngFor="let col of columns">
                <span *ngIf="col.field === 'priceUsd'">
                  ${{ getFloatNumber(rowData[col.field]) }}
                </span>

                <span *ngIf="col.field !== 'priceUsd'">
                  {{ rowData[col.field] }}
                </span>
              </td>
            </tr>
          </ng-template>
        </p-table>
      </div>
    </div>

    <div class="col-md-6 col-12">
      <div class="table">
        <h1>List Exchanges</h1>

        <p-table [columns]="exchangeColumns" [value]="listExchanges">
          <ng-template pTemplate="header" let-columns>
            <tr>
              <th *ngFor="let col of columns">
                {{ col.header }}
              </th>
            </tr>
          </ng-template>
    
          <ng-template pTemplate="body" let-rowData let-columns="columns">
            <tr>
              <td *ngFor="let col of columns">
                <span *ngIf="col.field === 'volumeUsd'">
                  ${{ getFloatNumber(rowData[col.field]) }}
                </span>

                <span *ngIf="col.field !== 'volumeUsd'">
                  {{ rowData[col.field] }}
                </span>
              </td>
            </tr>
          </ng-template>
        </p-table>
      </div>
    </div>
  </div>
</div>
  

Recent Blogs