NgRxでAngularの状態管理をする!NgRxの概要と使い方を解説。

NgRxでAngularの状態管理をする!NgRxの概要と使い方を解説。

この記事では、Angularのための状態管理ライブラリNgRxについてその特徴やメリット、基本的な使い方について説明したい。

Webアプリケーションの進化は非常に早い。アプリに求められる機能やパフォーマンスはとても多くなっている。そんな中で大きな問題となってきたのがアプリにおける状態の管理だ。

ReactのReduxやAngularのNgRxはそれらの問題を解消するためのライブラリだ。導入する企業も年々増えてきており、フロントエンドの開発者であれば使いこなせるようになっておきたいところだ。

では、見ていこう!

NgRxとは?

NgRxはAngularアプリでリアクティブな状態管理をおこなうためのライブラリ。

NgRxを使えばアプリ内で繰り返し使用されるユーザ情報などのデータを、効率よく整合性の保たれた状態で管理することができる。

NgRxはMeta社(旧Facebook社)の提唱するFluxアーキテクチャを基に設計されている。

ReduxがおもにReactアプリに使用されるのに対して、NgRxはAngularアプリに使用される。

NgRxホームページ:: https://ngrx.io

NgRxのパッケージ

NgRxには以下の7つのパッケージが含まれており、以下のように分類することができる。

State

  • Store – NgRxのメインパッケージ。状態管理をおこなう。
  • Effects – 状態管理にともなうサイドエフェクトを制御する。
  • Router Store – AngularルーターとNgRxを紐づけるパッケージ。
  • Entity – エンティティ管理をおこなうための拡張パッケージ。
  • ComponentStore – コンポーネントの状態管理のためのスタンドアロンパッケージ。

Data

  • Data – エンティティ管理をシンプルにする拡張パッケージ。

View

  • Component – リアクティブテンプレートのためのパッケージ。

なお、この記事ではNgRxのメインであるStoreを中心に解説する。

Fluxアーキテクチャ

Fluxアーキテクチャについて説明したい。

下の図はFluxアーキテクチャにおけるデータフローを表している。

Fluxアーキテクチャにおいてはデータフローが常にひとつの方向に流れていく。

図の構成要素を説明すると以下のようになる。

  • Action ボタンクリックや文字入力などのユーザアクション。
  • Dispatcher 更新の指示をおこなう関数。アクションによってトリガーされる。
  • Store 状態を保持するオブジェクト。
  • View  Storeの更新に伴い、ビューがアップデートされる。

Fluxアーキテクチャにおいては、Dispatcher以外の存在がStoreを書き換えることを制限する。

NgRxの使い方

ではNgRxをAngularアプリ内で使用してみよう。

ここでは簡易のショッピングカートを作成してNgRx Storeをどのようにアプリで使用するのか見てみたい。

プロジェクトを準備する

npmコマンドで新しいAngularプロジェクトを作成する。

作成したらアプリで使用する商品オブジェクトとサンプルの商品を用意する。

export interface Product {
  id: number,
  name: string,
  price: number
}

export const products = [
  {
    id: 1,
    name: 'マウス',
    price: 980
  },
  {
    id: 2,
    name: 'キーボード',
    price: 1480
  },
  {
    id: 3,
    name: 'デスクトップPC',
    price: 59800
  },
  {
    id: 4,
    name: 'ノートPC',
    price: 120800
  }
]

AppComponentのHTMLとTypeScriptを書き換える。

<div *ngFor="let product of products">
  {{ product.id }}
  {{ product.name }}
  ¥{{ product.price }}
  <button (click)="addItem(product)">+</button>
  <button (click)="removeItem(product)">-</button>
</div>
<hr>
<div>
  合計金額: ¥{{ totalPrice$ | async }}
  <button (click)="clear()">
    カートを空にする
  </button>
</div>
import {Component} from '@angular/core';
import {Product, products} from "./product";
import {Observable} from "rxjs";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'ngrx-angular';

  public products: Product[] = products;
  public totalPrice$: Observable<number>;

  addItem(product: Product) {
  }

  removeItem(product: Product) {
  }

  clear() {
  }
}

NgRxをインストールする

ここから先はNgRxを使用していこう。

まずはnpmコマンドを使用してNgRx Storeをインストールする。

npm install @ngrx/store --save

ActionとReducerを定義する

次にショッピングカートに変更を加えるためのActionとReducerを定義する。

  • Action – 更新の内容・タイプを指示するオブジェクト。
  • Reducer – Stateをアップデートする関数。StateとActionを引数として持つ。

コードの例を見てみよう。

cart.action.tsというファイルを作成してActionの内容を定義する。

createAction関数にて、Actionの名前と引数を定義する。

import {createAction, props} from "@ngrx/store";
import {Product} from "../product";

export const addItem = createAction('Add Item', props<Product>());
export const removeItem = createAction('Remove Item', props<Product>());
export const clear = createAction('Clear');

次にcart.reducer.tsを作成してActionごとの更新内容を定義する。

createReducer関数にて、イニシャルの値と各Actionに対応する関数を定義する。

import {createReducer, on} from "@ngrx/store";
import {Product} from "../product";
import {addItem, removeItem, clear} from "./cart.action";

export const initialCartEntries: Product[] = [];

export const cartReducer = createReducer(
  initialCartEntries,
  on(addItem, (entries, product) => {
    return [...entries, product];
  }),
  on(removeItem, (entries, product) => {
    const newEntries: Product[] = [...entries];
    const found = newEntries.find(entry => entry.id == product.id);
    if (found) newEntries.splice(newEntries.indexOf(found), 1);

    return newEntries;
  }),
  on(clear, _=> [])
);

Storeに保存された値を更新する際は元のデータに変更を加えるのでは無く、元のデータのコピーに変更を加えて返す。

app.moduleを更新する

NgRxをアプリ内で使用するにはモジュールファイルを更新する必要がある。

importsにStoreModuleを追加して、先ほど作成したcartReducerを登録する。

import {StoreModule} from "@ngrx/store";
import {cartReducer} from "./cart/cart.reducer";

imports: [
  StoreModule.forRoot({ cartEntries: cartReducer})
]

dispatch関数でActionを発生する

AppComponent内でActionを呼び出してみよう。

Storeにあるdispatch関数を使用して、登録したreducerを呼び出す。

constructor(private store: Store) {}

addItem(product: Product) {
  this.store.dispatch(addItem(product));
}

removeItem(product: Product) {
  this.store.dispatch(removeItem(product));
}

clear() {
  this.store.dispatch(clear());
}

Selectorsを登録する

ここではショッピングカートの合計金額を表示するためにStoreのSelectorsを使用する。

Selectorsを使用することによりStoreに保存されたデータを任意の形式で得ることができる。

cart.selector.tsを作成して合計金額を表示するための関数を定義する。

import {Product} from "../product";
import {createFeatureSelector, createSelector} from "@ngrx/store";

export const selectTotalPrice = createSelector(
  createFeatureSelector('cartEntries'),
  (products: Product[]) => {
    let total = 0;
    products.forEach(p => total += p.price);
    return total;
  }
);

定義した関数をAppComponent内で使用する。

constructor(private store: Store) {
  this.totalPrice$ = store.select(selectTotalPrice);
}

ここまで進んだら、以下のように簡易のショッピングカートが作成できたはずだ。

Chrome Redux DevTools

NgRxをGoogle Chromeで使用するなら、Chromeの拡張機能である「Redux DevTools」をインストールするようにしよう。

Redux DevToolsをNgRxに使用する際は、拡張機能をブラウザにインストールしたあとにStore DevToolsをインストールする。

npm install @ngrx/store-devtools --save

インストールが完了したらapp.module.tsにてStoreDectoolsModuleをインポートする。

@NgModule({
  imports: [
    StoreModule.forRoot(reducers),
    StoreDevtoolsModule.instrument({
      maxAge: 25, // Retains last 25 states
      logOnly: environment.production, // Restrict extension to log-only mode
      autoPause: true, // Pauses recording actions and state changes when the extension window is not open
    }),
  ],
})
export class AppModule {}

Redux DevToolsでは、状態の変化をDispatchごとに追跡することができる。