ちきさんです。
@ngrx/storeを使ってみたらかなり良かったので、いつも通りActionとReducerを書かなくても使えるラッパーライブラリを作ってみました。
Simplr (ovrmrw/ngrx-store-simplr)
発音は sɪ́mpəlɚ です。
@ngrx/store とは
RxJS powered state management for Angular applications, inspired by Redux
です。
いわゆる reactjs/redux ではありませんが、Reduxの概念を実装したライブラリなので考え方はほぼ同じです。ただしRxベースなのでとても使いやすくなっています。
ただ僕は開発初期段階におけるActionとReducerの記述がとてもめんどくさいと思っているので、それを省略できる書き方をしたくなります。しかしそのままではオレオレ脱線Reduxになってしまうので、いつでも本来の@ngrx/store(Redux)の書き方に簡単に移行できるようになっていることが重要です。
Simplr はそういう目的で作りました。
個人的には本来の書き方に移行するまでの繋ぎで使うつもりで作っていますが、Simplrだけで書いても普通に動くものは出来上がります。むしろComponent, Service, Action, Reducer等のファイル切り替えの手間が少なくなるので開発初期段階ではコーディングの速度が全然違うと思います。
それでは実際のコーディングを見ていきましょう。
Angular CLIの ng new
コマンドでプロジェクトを作成していることを前提とします。
GitHubリポジトリは ovrmrw/simplr-counter です。
1. npm install
npm i -S @ngrx/core @ngrx/store ngrx-store-simplr
2. Store を作る
src/app/
の下に store
というディレクトリを作り、Storeに関するものはここに書くようにします。
まずStoreで扱うStateの型定義をしましょう。
export interface AppState {
counter: number;
}
これで AppState
は { counter: number }
という型になります。
counterキー用のActionを作りましょう。空です。
// nothing
counterキー用のReducerを作りましょう。空です。
// nothing
app.module.ts
でimportする reducer
と initialState
を作りましょう。
import { combineReducers } from '@ngrx/store';
import { Wrapper } from 'ngrx-store-simplr';
import { AppState } from './models';
const wrapper = new Wrapper<AppState>();
const wrappedReducers = wrapper.mergeReducersIntoWrappedReducers({
counter: null // counterキー用のReducerがある場合はnullの代わりに配置する。
});
const rootReducer = combineReducers(wrappedReducers);
export function reducer(state, action) { // AoTコンパイルのために必要。
return rootReducer(state, action);
}
export const initialState: AppState = {
counter: 0
};
最低限これだけ書けばstoreディレクトリの作業は終わりです。
ActionとReducerを書いていませんね。安心してください、全く問題ありません。
ちなみに wrapper.mergeReducersIntoWrappedReducers()
の中身はオブジェクトですが、型ガードによって AppState
インターフェースに存在しないキーはエラーになりますし、キーが足りなくてもエラーになります。
3. app.module.ts に書き足す
app.module.ts
に少し書き足します。
import { StoreModule } from '@ngrx/store';
import { SimplrModule } from 'ngrx-store-simplr';
import { reducer, initialState } from './store/reducer';
@NgModule({
imports: [
...,
StoreModule.provideStore(reducer, initialState), // <== Add
SimplrModule.forRoot(), // <== Add
],
})
export class AppModule { }
imports
-
StoreModule.provideStore(reducer, initialState)
... @ngrx/storeを使うときの決まり文句みたいなものです。 -
SimplrModule.forRoot()
... Simplrを使うために必要です。テスト用に.forTesting()
というメソッドも生えています。
4. Service を作る
Storeへのdispatchを担当するのはComponentではなくServiceの役割です。(in my opinion)
Simplrを使う場合のdispatchは@ngrx/storeと作法が違います。
そもそもSimplrではActionもReducerも書かなくて良いので、本来そっちに書くべきことをここに書くようになります。
import { Simplr } from 'ngrx-store-simplr';
import { AppState } from '../store/models';
import { initialState } from '../store/reducer';
@Injectable()
export class CounterService {
constructor(
private simplr: Simplr<AppState>,
) { }
increment() {
this.simplr
.dispatch('counter', (state) => state + 1);
}
reset() {
this.simplr
.dispatch('counter', initialState.counter);
}
}
increment 関数
this.simplr.dispatch()
は引数を2つとります。
-
'counter'
... 第一引数。Stateのキーです。この指定が無いとどのキーを更新していいのかわからないからです。 -
(state) => state + 1
... 第二引数。Store内部で実行されるコールバックです。実行結果はStateにマージされます。変数state
にはその時点の最新のStateが代入されます。
increment()
が実行される度にStateの counter
キーの値を 1 増やします。 結果は下記のようになります。
{ counter: 1 }
==> { counter: 2 }
{ counter: 2 }
==> { counter: 3 }
reset 関数
this.simplr.dispatch()
はやはり引数を2つとっています。
-
'counter'
... 第一引数。Stateのキーです。 -
initialState.counter
... 第二引数。コールバックでない場合はその値がそのままStateにマージされます。
initialState.counter
が 0
の場合は毎回下記のようになります。
{ counter: 3 }
==> { counter: 0 }
{ counter: 4 }
==> { counter: 0 }
ちなみに this.simplr.dispatch('counter', ...
の 'counter'
はただの文字列のように見えますが型ガードが効いているのでStateに存在しないキーはエラーになります。
また、 dispatch()
の第二引数はPromiseとObservable(非同期処理)も受け入れるので下記のように書いてもOKです。
this.simplr.dispatch('counter', Promise.resolve(foo))
this.simplr.dispatch('counter', Observable.of(bar))
dispatch()
の詳細は こちら にあります。
5. Component を作る
公式サイトを見ると@ngrx/storeからimportするのは Store
クラスが定番なのですが、こいつはdispatchも出来てしまう危なっかしいやつなので、ComponentではStateの取得しかできない State
クラスを使いたいところです。
import { State } from '@ngrx/store';
import { AppState } from '../store/models';
import { CounterService } from '../services/counter';
@Component({
selector: 'app-counter-container',
template: `
<button (click)="increment()">increment</button>
<button (click)="reset()">reset</button>
<pre>{{ state$ | async | json }}</pre>
`
})
export class CounterContainerComponent {
constructor(
public state$: State<AppState>,
private service: CounterService,
) { }
increment() {
this.service.increment();
}
reset() {
this.service.reset();
}
}
State<AppState>
をインジェクトした this.state$
は型が Observable<AppState>
なのでAsync Pipeを付けてViewに突っ込むことができます。
(僕はあまりAsync Pipeは使わないですが)
あとはServiceの関数を呼び出したりしているだけで特別なことは何もしていないですね。
Demo
GitHub Pagesに動作デモがありますので実際に動かしてみてください。
ChromeにRedux Dev Toolsがインストールされていればモニタリングできます。
続きもあります。↓
Simplr - 第2回 @ngrx/storeをラップして@ngrx/effects使わずに非同期処理を書く