2018/07/30
Angularで状態管理する方法をざっくり把握するためのチュートリアルです。@ngrx/storeベースの簡単なアプリ(数をカウントするアプリ)を作成します。作るだけなら10分程度で出来上がるので、とりあえず手を動かしてngrxを最低限を把握したい人向けです。ソースコードもGitHubに置いているので参考にしてください。
ngrxを使うとボイラープレートが非常に多くなりますが、今回のチュートリアルでは@ngrx/schematics を使い、ボイラープレートを自動生成することで極力手間を省いています。
段階を踏んで、ステップごとに動作確認しながら作成していきます。
各ステップ終了時点のソースコードはGitHubに用意しています。参考にしてください。
大部分はSchematicsを使ってngコマンドでボイラープレートを自動生成し、メイン部分のみ実装という感じです。
$ npm i -g @angular/cli
$ npm i -g @ngrx/schematics
$ ng new ngrx-tutorial
$ cd ngrx-tutorial
$ ng serve -o
カウント処理の資産は全てsrc/app/counter
フォルダ配下に作成します。
まずはコマンドラインからボイラープレートを作成し、その後カウント処理を実装します。
--module
オプションを指定します。$ ng g module counter --module=app.module.ts
--module
オプションを指定します。--export
オプションを指定します。$ ng g component counter --module=counter/counter.module.ts --export
app.component.html
修正し、作成したカウント処理用のコンポーネントを呼び出すようにします。<app-counter></app-counter>
$ ng serve -o
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-counter',
templateUrl: './counter.component.html',
styleUrls: ['./counter.component.css']
})
export class CounterComponent implements OnInit {
count = 0;
constructor() { }
ngOnInit() {
}
increment() {
this.count = this.count + 1;
}
decrement() {
this.count = this.count - 1;
}
}
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<div>Count: {{count}}</div>
$ ng serve -o
+
,-
ボタンをクリックすると数字が増えたり減ったりした、開発者ツールでもエラーがなければ成功です。@ngrx/storeをアプリに導入し、初期設定をします。
@ngrx/schematics
@ngrx/store
@ngrx/store-devtools
$ npm i -D @ngrx/schematics
$ npm i -s @ngrx/store
$ npm i -s @ngrx/store-devtools
*@ngrx/schematics
をデフォルトのSchematicsに追加します(コマンドラインでngrxのボイラープレート生成時に@ngrx/schematics
の指定を省略できるようにするためです。)
$ ng config cli.defaultCollection @ngrx/schematics
angular.json
にこのような設定が追加されます。 "defaultProject": "ngrx-tutorial",
"cli": {
"defaultCollection": "@ngrx/schematics"
}
src/app/state
配下に生成したいので--statePath
オプションを指定します。--module
オプションを指定します。$ ng g store state --statePath state --root --module app.module.ts
src/app/app.module.ts
でenvironment
のimport文のパスでエラーが出ている場合は修正してください。- import { environment } from '../../environments/environment';
+ import { environment } from '../environments/environment';
$ ng serve -o
ここからは実際にStore、Reducer、Actionを作成し、カウント処理の値をStoreに移行します。
ここで作成する資産はカウンター処理に閉じたものなので、src/app/counter/state
配下に作成します。
また@ngrx/schemetics
のデフォルトではReducer、Actionなどの資産が、役割ごとにフォルダ分けされてしまいますが、1フォルダに集約したほうがソースが修正しやすいので、今回は全てsrc/app/counter/state
の直下に作成します。
src/app/counter/state
直下に作成するため--statePath
オプションを指定します。--module
オプションを指定します。$ ng g store counter/counter --statePath state --module counter.module.ts
--reducers
オプションを指定します。$ ng g reducer counter/state/counter --reducers index.ts
src/app/counter/state
直下に作成するため--flat
オプションを登録します。$ ng g action counter/state/counter --flat
※この時点ではコンパイルエラーがでますので、動作確認はできません。そのまま次に進みます。
依存関係の都合でボイラープレートとは逆順で実装していきます。
ボイラープレート生成時から下記のように修正します。
※コメントはコードの説明なので無視して実装してください。
import { Action } from '@ngrx/store';
export enum CounterActionTypes {
// Actionごとに型を定義します。
- LoadCounters = '[Counter] Load Counters'
+ CountIncrement = '[Counter] Increment Count',
+ CountDecrement = '[Counter] Decrement Count'
}
// Actionごとに@ngrx.storeのActionをインプリしたクラスを作成します。
// 複雑な処理をする場合はコンストラクタ引数をとりますが、
// 本チュートリアルでは簡単のため引数なしにしています。
- export class Counter implements Action {
- readonly type = CounterActionTypes.LoadCounters;
- }
+ export class CountIncrement implements Action {
+ readonly type = CounterActionTypes.CountIncrement;
+ public constructor() {}
+ }
+
+ export class CountDecrement implements Action {
+ readonly type = CounterActionTypes.CountDecrement;
+ public constructor() {}
+ }
// 上記で定義したActionクラスを集約した型を定義します。Reducerで使うためです。
- export type CounterActions = LoadCounters;
+ export type CounterActions = CountIncrement | CountDecrement;
import { Action } from '@ngrx/store';
+ import { CounterActionTypes } from './counter.actions';
export interface State {
// カウンター処理に置けるStateを定義します。
+ count: number;
}
export const initialState: State = {
// カウンター処理に置けるStateの初期値を定義します。
+ count: 0
};
export function reducer(state = initialState, action: Action): State {
switch (action.type) {
// 引数として受け取ったActionの型に応じて処理を振り分けます
// ここではカウンター処理に関連するアクションのみ拾って、他はStateをそのまま返します。
+ case CounterActionTypes.CountIncrement:
// Stateを変更する場合は、Stateがイミュータブルになるように元のStateには変更を加えず
// Object.assingで新規オブジェクトを作るようにします。
+ return Object.assign({}, { ...state, count : state.count + 1 });
+ case CounterActionTypes.CountDecrement:
+ return Object.assign({}, { ...state, count : state.count - 1 });
default:
return state;
}
}
// コンポーネントでStateのCountを取得するための関数を定義します。
// Storeの方にも定義しますが、ここでは本ファイルで定義している
// Stateのプロパティに関連する処理のみ定義します。
+ export const getCount = (state: State) => state.count;
import {
ActionReducerMap,
createFeatureSelector,
createSelector,
MetaReducer
} from '@ngrx/store';
// ng gコマンド生成時は相対パスがずれている可能性があるため
// その場合は修正する
- import { environment } from '../../environments/environment';
+ 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 metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];
// コンポーネントでStateのプロパティを取得するための関数を定義します。
// 複数コンポーネントで使う度に定義するのは冗長なのでココで共通的に定義します。
+ export const getCounterFeatureState = createFeatureSelector<State>('counter');
+ export const getCounter = createSelector(getCounterFeatureState, s => s.counter);
+ export const getCount = createSelector(getCounter, fromCounter.getCount);
import { Component, OnInit } from '@angular/core';
+ import { Observable } from 'rxjs';
+ import { Store } from '@ngrx/store';
+ import * as CounterReducer from './state/counter.reducer';
+ import * as CounterActions from './state/counter.actions';
+ import { getCount } from './state';
@Component({
selector: 'app-counter',
templateUrl: './counter.component.html',
styleUrls: ['./counter.component.css']
})
export class CounterComponent implements OnInit {
// Storeでの値変更を順次受け付けれるように型をObservableに変更します
- count = 0;
+ count$: Observable<number>;
// Storeをインジェクションします
- constructor() { }
+ constructor(private store: Store<CounterReducer.State>) {
// Storeからカウンタを取得します
+ this.count$ = store.select(getCount);
+ }
ngOnInit() {
}
increment() {
// インクリメントの実処理はカウンタのReducerに任せるので
// ここではActionをdispatchするだけです。
- this.count = this.count + 1;
+ this.store.dispatch(new CounterActions.CountIncrement());
}
decrement() {
- this.count = this.count - 1;
+ this.store.dispatch(new CounterActions.CountDecrement());
}
}
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<!-- 変数名と型が変わったのでHTMLも若干修正します -->
- <div>Count: {{count }}</div>
+ <div>Count: {{count$ | async }}</div>
$ ng serve -o
@ngrx/store
で管理されるようになっています。ストアとストア登録処理はボイラープレートで生成するのでココで改めて説明します。
まずはルートのストアです。
ストアはsrc/app/state/index.ts
に作成されます。
中身を見るとわかりますが、実態はReducerを集約したActionReducerMapです。
Reducerを新しく作成した時は、このマップにどんどん追加していきます。
import {
ActionReducer,
ActionReducerMap,
createFeatureSelector,
createSelector,
MetaReducer
} from '@ngrx/store';
import { environment } from '../../environments/environment';
export interface State {
}
export const reducers: ActionReducerMap<State> = {
// ココにReducerが追加されていきます。
// 今回のチュートリアルではルートのストアに1つもReducerを定義していないので空っぽです。
};
export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];
ストアをモジュールに登録するには下記のようにStoreModule.forRoot
を使います(ボイラープレートでやってくれます)
@NgModule({
// ・・・
imports: [
// ・・・
StoreModule.forRoot(reducers, { metaReducers }),
!environment.production ? StoreDevtoolsModule.instrument() : []
// ・・・
],
// ・・・
})
export class AppModule { }
次にカウンタのストアに関してです。
こちらもルートの場合とほぼ同じです。
// ・・・
export const reducers: ActionReducerMap<State> = {
// カウンタのReducerをマップに登録しています。
counter: fromCounter.reducer,
};
export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];
// ・・・
ただ登録はStoreModule.forFeature
を使います。
このメソッドは、機能毎に状態管理する時に使うもので、ルートのストアに指定した名前で登録されます。使う時になったら遅延ロードしてくれる機能を持っています。
// ・・・
import * as fromCounter from './state';
// ・・・
@NgModule({
imports: [
// ・・・
// アプリ全体のストアにcounterという名前で登録します
StoreModule.forFeature('counter', fromCounter.reducers, { metaReducers: fromCounter.metaReducers })
// ・・・
],
// ・・・
})
export class CounterModule { }
以上で@ngrx/schematics
を使った@ngrx/store
のチュートリアルは終了です。
ngrx
ライブラリは他にも@ngrx/router-store
、@ngrx/entity
、@ngrx/effect
があるので、
今回のアプリをベースに拡張し、理解を深めてみるのも良いかもしれません。
AngularはVue.jsなどと比較するとボイラープレートが多くなってしまいます。
しかし、ソースコード自動生成機能が充実しているので、けっこう便利なフレームワークです!
あまり周りでAngular使ってる人がいなくて寂しいのですが、、、、皆さん是非Angular使いましょう!
@ngrx/schematics
の使い方が網羅されており参考になりました。