【Angular】NgRxの概念を世界一わかりやすく解説!【NGRX】

ANGULAR

みなさんこんにちは、現役フリーランスエンジニアのサメハックです

未経験からWebエンジニアに転職し、
現在正社員として5年働いたのちフリーランスとして独立しました。

Angularの解説シリーズです。

今回はNgRxについて学んでいきましょう!

駆け出しエンジニアや未経験の方、
また新入社員を指導する先輩社員にとっても
わかりやすいように解説していきます!

この記事を読むと・・・
  • NgRxが何かがなんとなくわかる
  • STOREの操作ができる
  • サンプルコードがダウンロードできる

※PCにnpm、nodeがインストールされている前提で記述します。
 yarn等をお使いの方は読み替えてください。

ちなみに今回の内容は難易度がかなり高いので、
何度も読み直してね!

作りたいもの

このようにボタン操作でSTOREのSTATE状態を更新し、
画面を自動的に書き換えるアプリケーションを作成します。

これが今回の完成形だよ!

NgRxとは

NgRxとは、Reduxをベースに作られた状態(STATE)管理をするためのシステムです。

このようなライフサイクルが公式で公表されていますが、
初見では非常に難解なので、

今回はこのライフサイクルを紐解いていきましょう。

結論

まず、このライフサイクルを理解できないと
NGRXの理解が非常に難しいのでいきなり結論から説明します。

ライフサイクル(データ処理)はこのようになっています。

一般的にNGRXは一方通行のデータ処理を行う
と言われていますが、

データ処理は一方通行ではなく、

  • データ更新処理
  • データ取得処理

の2種類が存在する、と解釈してください。

上記の2処理を設定することで、
データ更新→自動的にデータ取得
という一方通行の流れが”最終的に”生まれます。

2種類の処理を設定することで最終的に一方通行となるよ!

COMPONENT

COMPONENTというのは、いわゆる画面描画&操作に関わる部分です。

例えば

  • ボタンをクリックする
  • DOMのを書き換える

というような処理を司る部分です。

STORE

STOREというのは、STATE(状態)を管理している空間で
メモリ上に存在する仮想DBのようなイメージです。

STOREが保持しているSTATEが更新されると、
データ連携しているCOMPONENTが自動的に書き換わります。


このデータの流れがまさにNGRXのライフサイクルです。

画面とSTOREを紐付けることで、
例えば画面操作によってSTATEが更新された際に、
特別な処理をしなくてもDOMが更新されるよ!

データ取得処理:SELECTOR

NgRxでは、SELECTORというファイルにSTOREのデータ取得および整形処理
定義されており、COMPONENTと紐付けることで
STATEが書き換わったタイミングで、自動的に画面を書き換えることができます。

少しややこしいので言い換えると
「STOREのデータを取得(自動連携)して!」
とCOMPONENTから依頼することで、
STOREデータ→コンポーネントの単方向データバインディングが行われるようになります。

感覚的にはgetter()に近いよ!

データ更新処理:REDUCER , ACTION

NgRxではREDUCERというファイルにSTOREのデータ更新処理が定義されます。
「え?ACTIONじゃなくて?」と思った方も多いと重いますが、
ACTIONはどこでどんな処理が行われているかを定義しているだけで、
実際のデータ更新処理は定義されていません。

後述しますが、専用のデバッグツールでのログを出すことと
引数を渡すのがActionの主な用途です。

これはsetter()に近いよ!

EFFECT

今回は割愛しますが、外部APIを叩いてデータを取得する場合などには
EFFECTを使用します。

実際に作ってみよう!

ファイル作成・インストール

今回はファイルが多いので、
インストール作業・ファイル作成作業は始めに済ませておきます。

初期設定

ng new ngrx-user
cd ngrx-user

ユーザコンポーネントの作成

ng generate component user

ACTIONファイルの作成

touch src/app/user/user.action.ts

REDUCERファイルの作成

touch src/app/user/user.reducer.ts

SELECTORファイルの作成

touch src/app/user/user.selector.ts

モジュールの作成

ng generate module --flat user/user

Storeのインストール

npm install @ngrx/store --save

デバッグツールのインストール

npm install @ngrx/store-devtools --save

データ更新処理の作成

user.action.ts

■ACTION作成構文

import { createAction } from '@ngrx/store';

createAction(`[呼び出されるコンポーネント名]関数名`, props等のオプション)
/* ____________________
ACTION
  各アクションがどこで呼ばれて、どのような処理をするのかを記述
  引数が必要であれば、ここで定義する。

  基本的にデバッグツールのログ確認用途で役に立ちます。

  createAction(`[呼び出されるコンポーネント名]関数名`, props等のオプション)
____________________ */

import { createAction } from '@ngrx/store';

export const countUpAction = createAction(`[User Component] countUpAction`);
export const countDownAction = createAction(`[User Component] countDownAction`);
export const changeNameAction = createAction(
  `[User Component] changeNameAction`
);
createActionの引数はデバッグツールで表示されるものだよ!
ただのテキストなので、編集しても処理に影響はないよ!

user.reducer.ts

■REDUCER作成構文

import { createReducer, on } from '@ngrx/store';

createReducer(
  initialState,
  on(アクション名, (state) => ({
    ...state,
    STATEの更新処理,
  }))

import { createReducer, on } from '@ngrx/store';
import { __asyncGenerator } from 'tslib';
import {
  countUpAction,
  countDownAction,
  changeNameAction,
} from './user.action';

// デバッグツールでの表示名
export const featureName = 'user';

// 型の定義
export interface MyUserState {
  name: string;
  age: number;
}

/**初期値の設定 ※これをSTOREで状態管理する */
export const initialState: MyUserState = {
  name: 'サメハック',
  age: 100,
};

/**実際にSTOREのSTATEを操作する処理

createReducer(
  initialState,
  on(アクション名, (state) => ({
    ...state,
    STATEの更新処理,
  })),
 */
export const userReducer = createReducer(
  initialState,
  // countUpActionが呼び出されたら、STOREのstateを受け取る
  // 受け取ったstateを展開し、ageプロパティの値を+1する
  on(countUpAction, (state) => ({
    ...state,
    age: state.age + 1,
  })),
  on(countDownAction, (state) => ({
    ...state,
    age: state.age - 1,
  })),
  on(changeNameAction, (state) => ({
    ...state,
    name: 'いぬハック',
  }))
);
ここで設定したfeatureNameは後述するデバッグツールの表示オブジェクト名になるよ!

ちなみに、Angularではスプレッド構文が多様されます。
自信がない方は以下の記事を参考にしてください。

【JavaScript&TypeScript】スプレッド構文を使おう!【...Object】
ここで各アクションが呼び出されたときの実際の処理を定義しているよ!

データ取得(自動連携)処理の作成

user.selector.ts

■SELECTOR作成構文

/* ____________________
SELECTOR
  ・STOREのデータを取得(連携)するための処理
    イメージ的には、getter()のようなもの。
  ・STOREデータ → コンポーネント
   の単方向データバインディングが行われる

createSelector(
  State,(全てのstate情報) => 欲しいデータを取り出す)
____________________ */
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { MyUserState, featureName } from './user.reducer';

export const selectUserState = createFeatureSelector(featureName);

/**ユーザの全情報を取得するセレクター */
export const selectUserAllInfo = createSelector(
  selectUserState,
  (state) => state
);

/**ユーザの年齢情報を取得するセレクター */
export const selectUserAge = createSelector(
  selectUserState,
  (state) => state.age
);
ここでコンポーネントに渡すデータを定義するよ!

コンポーネント

app.component.html

<p>user works!</p>

<h2>ユーザ情報をすべて取得</h2>
<div *ngIf="user$ | async as user">
  name: {{ user.name }}
  <button (click)="changeName()">名前を変更</button>
  <br />
  age: {{ user.age }}
  <button (click)="countUp()">+</button>
  <button (click)="countDown()">ー</button>
</div>

<h2>ユーザの年齢のみ取得</h2>

<div *ngIf="userAge$ | async as userAge">
  age: {{ userAge }}
  <button (click)="countUp()">+</button>
  <button (click)="countDown()">ー</button>
</div>

特に特殊な記述はないですが、

user$ | async as user

の部分だけがAngularの特殊な記述かと思います。

変数名$ | async as 変数別名

これはasync pipeといい、上記のように記述することで
TypeScript側へ記述を最小限にして、STOREのデータを取得することができます。

*ngIfと併用することでSTOREからのデータ取得完了後に
はじめてコンポーネントが描画されます。
※この記述をしないとデータ収録前にコンポーネントが描画されてしまいエラーが発生します。
詳しく知りたい方はこちらの記事を参考にしてください

【Angular】asyncパイプを使おう!【サンプルあり!】

async pipeを使うことでsubscribe()とunsubscribe()が自動的に実行されるよ!

user.component.ts

■select・・・STOREのデータを受け取るための記述

変数名 = this.store.select(SELECTORで作成した変数);

■dispatch・・・STOREのデータを更新するための記述

this.store.dispatch(アクション名());
import { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { MyUserState } from './user.reducer';
import { selectUserAllInfo, selectUserAge } from './user.selector';
import {
  changeNameAction,
  countUpAction,
  countDownAction,
} from './user.action';

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

  /**ユーザの全情報を取得 */
  user$ = this.store.select(selectUserAllInfo);
  /**ユーザの年齢を取得 */
  userAge$ = this.store.select(selectUserAge);

  constructor(private store: Store) {}


  // STOREデータの更新
  countUp() {
    this.store.dispatch(countUpAction());
  }

  // STOREデータの更新
  countDown() {
    this.store.dispatch(countDownAction());
  }

  // STOREデータの更新
  changeName() {
    this.store.dispatch(changeNameAction());
  }

  ngOnInit(): void {}
}

ちなみにですが、この時点ではuser$の値は取得されません
html側でasync pipeを実装することで初めてuser$の値を取得する処理が実行されます。

SELECTORもACTIONも呼び出すのはこのファイルだよ!

モジュール関連

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserComponent } from './user/user.component';
import { UserModule } from './user/user.module';

@NgModule({
  declarations: [AppComponent, UserComponent],
  // ユーザモジュールのインポート
  imports: [BrowserModule, AppRoutingModule, UserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

user.module.ts

■STOREを使用できるようにする

import { StoreModule } from '@ngrx/store';

StoreModule.forRoot({ [featureName]: リデューサー名 }),
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { featureName, userReducer } from './user.reducer';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    // Storeを使用できるようにする設定
    StoreModule.forRoot({ [featureName]: userReducer }),
    // デバッグツールのインポート
    StoreDevtoolsModule.instrument(),
  ],
})
export class UserModule {}
この記述を忘れるとSTOREが使用できないよ!

画面表示

app.component.html

<app-user></app-user>
これでようやくアプリケーションが動くようになったね!
NgRxは難しい概念なので、何度も読み返してゆっくり理解しよう!

デバッグツールのインストール

このままでは、STOREのSTATEの状態を確認することが出来ないので、
専用のデバッグツールをインストールします。

https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd/related?hl=ja

これをインストールすると、このようにSTATEの値や、
ACTION実行時のログ、及び差分を表示することができるようになります。

できることが多いので色々試してみよう!

サンプルコードダウンロード

今回使用したサンプルコードはGitHubに置いておきました。
NgRxが即使用できるシンプルなサンプルコードはなかなか見つからないので、
これを元に改版していただくと良いかと思います。

https://github.com/same-hack/NGRX-user.git

サンプルコードダウンロード&実行コマンド

git clone https://github.com/same-hack/NgRx-user.git
npm install 
npm start

実行環境

Angularのバージョンが古いと動かないことがあるので、
うまく動かなければアップデートしてね!

まとめ

今回のSTORE操作の処理をわかりやすく図解しました。

筆者も理解するのに時間がかかった概念なので、
世界一わかりやすくまとめたつもりです。

NgRxを使用すると、ファイルがかなり多くなってしまい
混乱すると思いますが、こうして見てみると
やっていることは非常に理解しやすいと思います。

  • NgRx・・・Angularで状態(STATE)管理をするためのシステム
  • STORE・・・STATEを管理している空間。メモリ上の仮想DBのようなイメージ
  • COMPONENT・・・画面描画&操作に関わる部分
  • SELECTOR・・・データ取得(自動連携)の定義。getter()のようなもの
  • ACTION・・・どこでなんの関数を使用しているかを記述
  • REDUCER・・・STOREのSTATEを更新する。setter()のようなもの

満足いただけたら、1クリックなのでSNSフォローしてもらえると嬉しいです🦈

タイトルとURLをコピーしました