Simplr - 第2回 @ngrx/storeをラップして@ngrx/effects使わずに非同期処理を書く

ちきさんです。
**Simplr** という@ngrx/storeをラップするライブラリを作ったのですが、これを使うと非同期処理がすごく簡単に書けるのでその話をします。

[Simplr - 第1回 @ngrx/storeをラップしてActionとReducer書かずにRedux使う](http://qiita.com/ovrmrw/items/e65ffd6e439d2df5615e) を読んでくれていると理解が早いかと思います。

@ngrx/storeで非同期処理を書くなら [ngrx/effects](https://github.com/ngrx/effects) に乗っかってしまうのがベストプラクティスだと思います。redux-observableがわかる方向けの説明としては、あれとほぼ同じようなものです。
でも非同期処理が増える毎にActionが3つ増える(Request, Fulfilled, Failed)ってかなり苦痛じゃないですか？
プロジェクトがメンテナンスフェーズに入っているならともかく、開発初期段階でのあれは精神的によろしくありません。同じ事を思ってる人はたくさんいると思います。
そこで **Simplr** です。

では実際のコーディングを見ていきましょう。
プロジェクトはAngular CLIの `ng new` コマンドで作成していることを前提とします。

GitHubリポジトリは [ovrmrw/simplr-timestamp](https://github.com/ovrmrw/simplr-timestamp) です。

---


## 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 {
  timestamp: TimestampState;
}

export interface TimestampState {
  local: number;
  server: number;
}
```

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

timestampキー用のActionを作りましょう。

```ts:store/actions/timestamp.ts
import { Resolver } from 'ngrx-store-simplr';
import { AppState } from '../models';

export type Resolver = Resolver<AppState, 'timestamp'>;
```

この `Resolver` は用意する必要はありませんが、後のServiceでガチガチに型ガードを効かせてくれるのであると便利なやつです。
いわゆるActionは一つも書いていませんね。問題ありません。

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

```ts:store/reducers/timestamp.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({
  timestamp: null, // timestampキー用のReducerがある場合はnullの代わりに配置する。
});

const rootReducer = combineReducers(wrappedReducers);

export function reducer(state, action) { // AoTコンパイルのために必要。
  return rootReducer(state, action);
}

export const initialState: AppState = {
  timestamp: {
    local: 0,
    server: 0,
  }
};
```

今回のstoreディレクトリの作業はこれだけです。
`reducer.ts` の中身は第1回とほとんど変わりません。

---

## 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 { }
```

第1回と全く同じです。

---

## 4. Service を作る (NictService)

[NICT 情報通信研究機構](https://www.nict.go.jp/) から非同期でタイムスタンプを取得するServiceを作ります。

```ts:services/nict.ts
@Injectable()
export class NictService {
  constructor(
    private http: Http,
  ) { }

  requestServerTimestamp(): Observable<number> {
    return this.http.get('https://ntp-a1.nict.go.jp/cgi-bin/json')
      .map(res => res.json() as { st: number })
      .map(data => data.st * 1000);
  }
}
```

- `requestServerTimestamp()` はHTTPリクエストをしてタイムスタンプをJSONで受け取る非同期処理です。

## 5. Service を作る (TimestampService)

今回の記事の目玉です。Simplrで非同期処理がどれだけ簡単に書けるか見ていきましょう。

```ts:services/timestamp.ts
import { Simplr, Action } from 'ngrx-store-simplr';
import { AppState } from '../store/models';
import { NictService } from './nict';
import * as timestamp from '../store/actions/timestamp';

const serverTimestampResolver: (effect: number) => timestamp.Resolver =
  (newTimestamp) => (state) => {
    const server = newTimestamp;
    return { ...state, server };
  };

@Injectable()
export class TimestampService {
  constructor(
    private simplr: Simplr<AppState>,
    private nict: NictService,
  ) { }

  getLocalTimestamp() {
    this.simplr
      .dispatch('timestamp', 
        ({ local: new Date().getTime() })
      );
  }

  getServerTimestamp() {
    this.simplr
      .dispatch('timestamp', 
        this.nict
          .requestServerTimestamp()
          .map(serverTimestampResolver)
      )
  }
}
```

### getLocalTimestamp 関数

`this.simplr.dispatch()` は引数を2つとります。

- `'timestamp'` ... 第一引数。Stateのキーです。
- `({ local: new Date().getTime() })` ... 第二引数。コールバックではないのでこの値がそのままStateにマージされます。

`local` プロパティの値が実行する度に上書きされることがわかればOKです。

### getServerTimestamp 関数

`this.simplr.dispatch()` はやはり引数を2つとっています。

- `'timestamp'` ... 第一引数。Stateのキーです。
- `this.nict.requestServerTimestamp().map(serverTimestampResolver)` ... 第二引数。この部分はややこしいので順番に解説します。

1. `this.nict.requestServerTimestamp()` ... この関数の戻り値の型は `Observable<number>` で、値はNICTから取得したタイムスタンプです。
2. `.map(serverTimestampResolver)` ... `map()` の中のコールバックを具体的に展開すると次のようになります。
3. `.map((newTimestamp) => (state) => ({ ...state, server: newTimestamp }))` ... そして `(state) => ({ ...state, server: newTimestamp })` というコールバックが戻り値になります。
4. `.map()` の戻り値の型は `Observable<(state: TimestampState) => state: TimestampState>` となります。

`dispatch()` の第二引数はコールバックあるいは直値をObservableやPromiseでも受けることができるので型にしっかりマッチしていますね。

これにより `local` プロパティの値はそのままになり、 `server` プロパティの値が更新されるということになります。

ReduxでよくあるRequest, Fulfilled, Failedで言うところのRequestに相当するActionは発行していませんが、必要であれば `simplr.dispatch()` の前でやればいいかなという感じです。

`dispatch()` の詳細は [こちら](https://github.com/ovrmrw/ngrx-store-simplr#details) にあります。

---

## 6. Component を作る

```ts
import { State } from '@ngrx/store';
import { AppState } from '../store/models';
import { TimestampService } from '../services/timestamp';

@Component({
  selector: 'app-timestamp-container',
  template: `
    <button (click)="localTimestamp()">local timestamp</button>
    <button (click)="serverTimestamp()">server timestamp</button>
    <pre>{{ state$ | async | json }}</pre>
  `
})
export class TimestampContainerComponent {
  constructor(
    public state$: State<AppState>,
    private service: TimestampService,
  ) { }

  localTimestamp() {
    this.service.getLocalTimestamp();
  }

  serverTimestamp() {
    this.service.getServerTimestamp();
  }
}
```

第1回とほぼ同じです。
`this.state$` にAsync Pipeを付けてViewに突っ込んでいます。

---

## Demo

[GitHub Pagesに動作デモがあります](https://ovrmrw.github.io/simplr-timestamp/)ので実際に動かしてみてください。

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

[非同期処理のテストを書く例](https://github.com/ovrmrw/simplr-timestamp/blob/master/src/app/services/timestamp.spec.ts) もあります。案外簡単に書けますのでよろしければどうぞ。