ちきさんです。
Simplr という@ngrx/storeをラップするライブラリを作ったのですが、これを使うと非同期処理がすごく簡単に書けるのでその話をします。
Simplr - 第1回 @ngrx/storeをラップしてActionとReducer書かずにRedux使う を読んでくれていると理解が早いかと思います。
@ngrx/storeで非同期処理を書くなら ngrx/effects に乗っかってしまうのがベストプラクティスだと思います。redux-observableがわかる方向けの説明としては、あれとほぼ同じようなものです。
でも非同期処理が増える毎にActionが3つ増える(Request, Fulfilled, Failed)ってかなり苦痛じゃないですか?
プロジェクトがメンテナンスフェーズに入っているならともかく、開発初期段階でのあれは精神的によろしくありません。同じ事を思ってる人はたくさんいると思います。
そこで Simplr です。
では実際のコーディングを見ていきましょう。
プロジェクトはAngular CLIの ng new
コマンドで作成していることを前提とします。
GitHubリポジトリは ovrmrw/simplr-timestamp です。
1. npm install
npm i -S @ngrx/core @ngrx/store ngrx-store-simplr
2. Store を作る
src/app/
の下に store
というディレクトリを作り、Storeに関するものはここに書くようにします。
まずStoreで扱うStateの型定義をしましょう。
export interface AppState {
timestamp: TimestampState;
}
export interface TimestampState {
local: number;
server: number;
}
これで AppState
は { timestamp: { local: number, server: number } }
という型になります。
timestampキー用のActionを作りましょう。
import { Resolver } from 'ngrx-store-simplr';
import { AppState } from '../models';
export type Resolver = Resolver<AppState, 'timestamp'>;
この Resolver
は用意する必要はありませんが、後のServiceでガチガチに型ガードを効かせてくれるのであると便利なやつです。
いわゆるActionは一つも書いていませんね。問題ありません。
次にtimestampキー用の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({
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
に少し書き足します。
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 情報通信研究機構 から非同期でタイムスタンプを取得するServiceを作ります。
@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で非同期処理がどれだけ簡単に書けるか見ていきましょう。
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)
... 第二引数。この部分はややこしいので順番に解説します。
-
this.nict.requestServerTimestamp()
... この関数の戻り値の型はObservable<number>
で、値はNICTから取得したタイムスタンプです。 -
.map(serverTimestampResolver)
...map()
の中のコールバックを具体的に展開すると次のようになります。 -
.map((newTimestamp) => (state) => ({ ...state, server: newTimestamp }))
... そして(state) => ({ ...state, server: newTimestamp })
というコールバックが戻り値になります。 -
.map()
の戻り値の型はObservable<(state: TimestampState) => state: TimestampState>
となります。
dispatch()
の第二引数はコールバックあるいは直値をObservableやPromiseでも受けることができるので型にしっかりマッチしていますね。
これにより local
プロパティの値はそのままになり、 server
プロパティの値が更新されるということになります。
ReduxでよくあるRequest, Fulfilled, Failedで言うところのRequestに相当するActionは発行していませんが、必要であれば simplr.dispatch()
の前でやればいいかなという感じです。
dispatch()
の詳細は こちら にあります。
6. Component を作る
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に動作デモがありますので実際に動かしてみてください。
ChromeにRedux Dev Toolsがインストールされていればモニタリングできます。
非同期処理のテストを書く例 もあります。案外簡単に書けますのでよろしければどうぞ。