LoginSignup
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-04-25

ちきさんです。
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の型定義をしましょう。

store/models/index.ts
export interface AppState {
  timestamp: TimestampState;
}

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

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

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

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を作りましょう。空です。

store/reducers/timestamp.ts
// nothing

app.module.ts でimportする reducerinitialState を作りましょう。

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 に少し書き足します。

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を作ります。

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で非同期処理がどれだけ簡単に書けるか見ていきましょう。

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() の詳細は こちら にあります。


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がインストールされていればモニタリングできます。

非同期処理のテストを書く例 もあります。案外簡単に書けますのでよろしければどうぞ。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2