Simplr - 第1回 @ngrx/storeをラップしてActionとReducer書かずにRedux使う

ちきさんです。
@ngrx/storeを使ってみたらかなり良かったので、いつも通りActionとReducerを書かなくても使えるラッパーライブラリを作ってみました。

[Simplr (ovrmrw/ngrx-store-simplr)](https://github.com/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](https://github.com/ovrmrw/simplr-counter) です。

---

## 1. npm install

```
npm i -S @ngrx/core @ngrx/store ngrx-store-simplr
```

## 2. Store を作る

`src/app/` の下に `store` というディレクトリを作り、Storeに関するものはここに書くようにします。

まずStoreで扱うStateの型定義をしましょう。

```ts:store/models/index.ts
export interface AppState {
  counter: number;
}
```

これで `AppState` は `{ counter: number }` という型になります。

counterキー用のActionを作りましょう。空です。

```ts:store/actions/counter.ts
// nothing
```

counterキー用のReducerを作りましょう。空です。

```ts:store/reducers/counter.ts
// nothing
```

`app.module.ts` でimportする `reducer` と `initialState` を作りましょう。

```ts:store/reducer.ts
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` に少し書き足します。

```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も書かなくて良いので、本来そっちに書くべきことをここに書くようになります。

```ts:services/counter.ts
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()` の詳細は [こちら](https://github.com/ovrmrw/ngrx-store-simplr#details) にあります。

---


## 5. Component を作る

公式サイトを見ると@ngrx/storeからimportするのは `Store` クラスが定番なのですが、こいつはdispatchも出来てしまう危なっかしいやつなので、ComponentではStateの取得しかできない `State` クラスを使いたいところです。

```ts:containers/counter.ts
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に動作デモがあります](https://ovrmrw.github.io/simplr-counter/)ので実際に動かしてみてください。

ChromeにRedux Dev Toolsがインストールされていればモニタリングできます。

続きもあります。↓
[Simplr - 第2回 @ngrx/storeをラップして@ngrx/effects使わずに非同期処理を書く](http://qiita.com/ovrmrw/items/b01f0c36d18624a08aa5)