Schematics で始める @ngrx/store

公開日:2018-12-02
最終更新:2018-12-31
※この記事は外部サイト(http://tercel-s.hatenablog.jp/entry/2018/0...)からのクロス投稿です

こんにちは、たーせるです。

今日は Angular で Redux 的なことをやるにはどうするんだっけ、という話。 手順やお作法が多すぎてよく忘れる。

この記事の対象読者は主に自分であり、前提となる説明やら手順やらもろもろ省いている。よく分かっていないところは逃げている。

そのため、何かの間違いでこの記事に辿り着いてしまった自分以外の誰かにとっては何一つとして得るものはない。

前置き

いろいろ雑だが、とりあえず関係ありそうなキーワードについて復習する。

Redux とは

シングルページアプリケーションの状態を管理するためのライブラリである。

Action だとか Store だとか Reducer だとか、意味不明な登場人物がたくさん出てくる。

偏見に満ち溢れた補足

時は世紀末、 React という UI ライブラリでシングルページアプリケーションを開発する際に、どうしても状態管理のコードがぐちゃーになる問題が深刻化していた。

この問題をエレガントに解決したのが Flux というデザインパターンであり、Redux はその Flux を実装したライブラリの中でも最もポピュラーと言われている。

ちなみに今回の題材である Angular は、ライブラリではなくフレームワークなので、無理やり Redux を適用しなくても React よりは状態管理の基盤が整っている(気がする)。

Schematics とは

Angular でソースコードの雛形を自動生成するツールである。

Redux は、登場人物の役割がいちいち回りくどい上、プログラミングの作法もかなり面倒くさい。 正直、毎回リファレンスを見ないと一から書けない。

しかし、Schematics を導入すれば、この問題が多少マシになる。

コマンドを一発叩くだけでそれっぽいソースファイルが出来上がるのだ。基本的にはその雛形のソースの断片を見ながら「あー、そういえばこんな書き方だったなぁ」と感慨に浸りつつ、継ぎ足したり書き換えたりするだけなので、お手軽度は上がる。

自分用チュートリアル

環境

  • node 8.11.3
  • npm 5.6.0
  • @angular/cli 6.1.2

プロジェクトの作成

コマンドプロンプトを起動して、以下のコマンドを入力する(@angular/cli がインストールされていないと「そんなコマンド無いよ」と叱られるので注意)。

$ng new NgRxSample  

必要なライブラリのインストール

引き続き、コマンドプロンプト上での作業。 プロジェクトのフォルダに cd して、以下のコマンドを入力する。

$cd NgRxSample  
$npm i @ngrx/core @ngrx/store @ngrx/store-devtools  

@npm i  @ngrx/schematics -D  

はじめの一歩

@ngrx/schematics を利用する前に以下のコマンドを入力する。

$ng config cli.defaultCollection @ngrx/schematics  

このコマンドを実行すると、angular.json に以下の設定が追加される。

"cli": {  
  "defaultCollection": "@ngrx/schematics"  
}  

この手順を踏むことで、Schematics を利用する際にいちいち @ngrx/schamatics を指定する必要がなくなる。

Store の生成

次に、Store を生成する。 ここで生成されるのは、すべての Reducer を束ねる ActionReducersMap オブジェクト、および State インタフェースである。

$ng g @ngrx/schematics:store State --root --module app.module.ts  

ng g State --root --module app.module.ts でも可らしいが、試していない)

このコマンドによって、src/app/reducers/index.ts ファイルが自動生成される。これこそが Schematics の威力である。

src/app/reducers/index.ts

import {  
  ActionReducer,  
  ActionReducerMap,  
  createFeatureSelector,  
  createSelector,  
  MetaReducer  
} from '@ngrx/store';  
import { environment } from '../../environments/environment';  

export interface State {  
}  

export const reducers: ActionReducerMap<State> = {  
};  

export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];  

こんなものを毎回手で書いていたら発狂してしまう。

ここまでは、どのプロジェクトでもほぼ共通の手順となる。たぶん。

Action のコード生成と実装

今回は、非常に簡素なカウンタを作成したい。よく Web や雑誌のチュートリアルでよく見かけるおなじみのアレである。 何番煎じだろうか。

どこから手を付けたらよいだろうか。

とりあえず、カウンタ値を1増やす imcrement、カウンタ値を1減らす decrement, カウンタ値を0クリアする reset という3つの Type を持った Counter Action から作り始めよう。

コマンドの構文は、ng g action ActionName [options] である。options--group を指定すると、actions ディレクトリの下にファイルを作ってくれる(省略した場合はその場にファイルが生成される)。

$ng g action Counter --group  

生成されたファイルの中身を以下のように改造する(予め一つ、参考の LoadCounters なる雛形サンプルが用意されているので、それを流用するだけである)。

src/app/actions/counter.action.ts

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

export enum CounterActionTypes {  
  Increment = '[Counter] Increment Counter',  
  Decrement = '[Counter] Decrement Counter',  
  Reset     = '[Counter] Reset Counter'  
}  

export class IncrementCounter implements Action {  
  readonly type = CounterActionTypes.Increment;  
  constructor() { };  
}  

export class DecrementCounter implements Action {  
  readonly type = CounterActionTypes.Decrement;  
  constructor() { };  
}  

export class ResetCounter implements Action {  
  readonly type = CounterActionTypes.Reset;  
  constructor() { };  
}  

export type CounterActions = IncrementCounter  
  | DecrementCounter  
  | ResetCounter;  

Reducer のコード生成と実装

次に、Reducer のコード生成を行う。

コマンドの構文は ng generate reducer ReducerName [options] である。

$ng g reducer Counter --group --reducer reducers/index.ts  

--group オプションをつけると、reducers フォルダの配下に Reducer が生成される。

また、--reducer reducers/index.ts を付けると、生成した Reducer が Store (src/app/reducers/reducers/index.ts) に自動的に登録される。

続いて、生成した src/app/reducers/counter.reducer.ts を改造する。 Reducer は、引き渡されてきた action と現在の state に基づき、次状態を返す。

state 変数を直接更新することは道徳的に許されず、常に新たなオブジェクトを作って返す必要がある。

src/app/reducers/counter.reducer.ts

import { Action } from '@ngrx/store';  
import { CounterActionTypes } from '../actions/counter.actions';  

export interface State {  
  counter: number;  
}  

export const initialState: State = {  
  counter: 0  
};  

export function reducer(state = initialState, action: Action): State {  
  switch (action.type) {  
    case CounterActionTypes.Increment:  
      return Object.assign({}, { ...state, counter: state.counter + 1 });  
    case CounterActionTypes.Decrement:  
      return Object.assign({}, { ...state, counter: state.counter - 1 });  
    case CounterActionTypes.Reset:  
      return Object.assign({}, { ...state, counter: 0 });  
    default:  
      return state;  
  }  
}  

さらに、src/app/reducers/index.ts を修正する。

とはいえ、Schematics によってほとんどのコードが生成済みであり、追加するのは getCounterFeatureState および getCounter の宣言くらいである。これらは、Angular の Component から state の中身にアクセスできるようにするための getter である。

src/app/reducers/index.ts

import {  
  ActionReducer,  
  ActionReducerMap,  
  createFeatureSelector,  
  createSelector,  
  MetaReducer  
} from '@ngrx/store';  
import { environment } from '../../environments/environment';  
import * as fromCounter from './counter.reducer';  

export interface State {  
  counter: fromCounter.State;  
}  

export const reducers: ActionReducerMap<State> = {  
  counter: fromCounter.reducer,  
};  

export const getCounterFeatureState = createFeatureSelector<State, fromCounter.State>('counter');  
export const getCounter = createSelector(getCounterFeatureState, state => state.counter);  

export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];  

createFeatureSelectorcreateSelector はぶっちゃけ正確なところをよく分かっていない。

子 State にアクセスできるようにするためのものだということくらいは解る。引数に指定している文字列 ('counter')は何でもよいというわけではなく、Store で State インタフェースに存在するメンバ変数名と一致していないと実行時に怒られる(一致させるのは、白い下線の箇所)。

画面と連携させる

最後に、画面にボタンを追加して IncrementAction, DecrementAction, ResetAction の各 Action を発行してみる。

  • Store を DI するため、コンストラクタの引数に Store<State> を指定する。
  • Store の状態にアクセスするには、this.store.select() を利用する。戻り値は Observable<T> 型になるので、テンプレート側では async パイプが必要である。
  • Action を発行するには this.store.dispatch() を利用する。引数には Action クラスを作って渡す。

src/app/app.component.ts

import { Component } from '@angular/core';  
import { Store } from '@ngrx/store';  
import { Observable } from 'rxjs';  

import * as AppStore from './reducers';  
import * as CounterActions from './actions/counter.actions';  

@Component({  
  selector: 'app-root',  
  template: `  
    <div>  
      <button (click)="increment()">+</button>  
      <button (click)="decrement()">-</button>  
      <button (click)="reset()">!</button>  
    </div>  
    <div>Counter: {{counter$ | async}}</div>  
  `  
})  
export class AppComponent {  
  counter$: Observable<number>;  

  constructor(private store: Store<AppStore.State>) {  
    this.counter$ = this.store.select(AppStore.getCounter)  
  }  

  increment() {  
    this.store.dispatch(new CounterActions.IncrementCounter());  
  }  

  decrement() {  
    this.store.dispatch(new CounterActions.DecrementCounter());  
  }  

  reset() {  
    this.store.dispatch(new CounterActions.ResetCounter());  
  }  
}  

余談だが、メンバ変数の counter$ の末尾の「$」はRx における一種の慣例によるものである。jQuery とは関係ない。

なんかここまで書いて力尽きた。つづく。

記事が少しでもいいなと思ったらクラップを送ってみよう!
0
+1
@tercelの技術ブログ

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする