ngrxでpollingをどう実装しようか悩み、slackで相談しつつ実装したので、考えたことや実装したコードを共有する。
version
- Angular 7.0.4
- ngrx 6.1.2
- rxjs 6.3.3
実装方針
実装方針は2通りあって
- component側でデータ更新のactionを定期的にdispatchする。
- effect側でintervalを組み込んでpollingする。
1の方が実装はシンプル。2はpollingをキャンセルできるようにstore側を作り込む必要があって、若干複雑になる。
ただ2の方がcomponentがシンプルになってテストもしやすいので、2を採用。
2で実装する場合pollingのstart/stopをどのように表現するかだけど
- データupdateのシンプルなactionを実装する
- pollingをstart/stopするactionを定義し、componentはそれを呼び出す。
- effectの中でupdate actionを呼び続ける。
- stateにisPollingを定義し、その状態を変更するとpollingが停止するようにする。
という感じで実装した。
実装
エラー処理などはいくらか省いてる。
state
import { Todo } from '../../model/todo';
export interface TodoState {
todoList: Todo[];
isPolling: boolean;
}
export const initialTodoState: TodoState = {
todoList: [],
isPolling: false
};
action
import { Action } from '@ngrx/store';
import { Todo } from '../../model/todo';
export enum ActionTypes {
LIST_REQUEST = '[Todo] List Request',
LIST_UPDATE = '[Todo] List Update',
LIST_START_POLLING = '[Todo] List Start Polling',
LIST_STOP_POLLING = '[Todo] List Stop Polling',
}
// データの更新をrequestするシンプルなaction
export class ListRequestAction implements Action {
readonly type = ActionTypes.LIST_REQUEST;
}
// データ更新用
export class ListUpdateAction implements Action {
readonly type = ActionTypes.LIST_UPDATE;
constructor(public payload: { todos: Todo[] }) {}
}
// pollingの開始
export class ListStartPollingAction implements Action {
readonly type = ActionTypes.LIST_START_POLLING;
}
// pollingの停止。reducerでstateのisPollingをfalseにするだけ。
export class ListStopPollingAction implements Action {
readonly type = ActionTypes.LIST_STOP_POLLING;
}
export type Actions =
| ListRequestAction
| ListUpdateAction
| ListStartPollingAction
| ListStopPollingAction;
effect
キモの部分。PollingStartのアクションでUpdateReqesutアクションを定期実行しつつ、stateのisPollingがfalseになるとpollingを停止させる。
import { Injectable } from '@angular/core';
import { TodoService } from '../../services/todo.service';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { interval, Observable, of as observableOf } from 'rxjs';
import { Action, select, Store } from '@ngrx/store';
import * as todoActions from './actions';
import { catchError, map, mergeMap, startWith, switchMap, takeWhile } from 'rxjs/operators';
import { Todo } from '../../model/todo';
import { TodoState } from './state';
import * as TodoStoreSelectors from './selectors';
@Injectable()
export class TodoStoreEffects {
constructor(private actions$: Actions, private store$: Store<TodoState>, private todoService: TodoService) {}
// APIを叩いてデータを更新するだけ
@Effect()
listRequestEffect$: Observable<Action> = this.actions$.pipe(
ofType<todoActions.ListRequestAction>(todoActions.ActionTypes.LIST_REQUEST),
mergeMap(action =>
this.todoService.index().pipe(
map((todos: Todo[]) => {
return new todoActions.ListUpdateAction({ todos });
}),
catchError(error => observableOf(new todoActions.RequestFailureAction({ error: error.error.message })))
)
)
);
// polling用。キャンセルはstateを変更するだけ。
@Effect()
listStartPooling$: Observable<Action> = this.actions$.pipe(
ofType<todoActions.ListStartPollingAction>(todoActions.ActionTypes.LIST_START_POLLING),
switchMap(() => {
return interval(30000).pipe( // interval
startWith(0),
// selectorを使ってstoreのisPollingを監視する。
switchMap(() => this.store$.pipe(select(TodoStoreSelectors.selectPolling))),
// isPollingがfalseならキャンセルする
takeWhile(isPolling => isPolling),
switchMap(() => {
return observableOf(new todoActions.ListRequestAction());
})
);
})
);
}
component
componentからは以下のように使える。
import { Component, OnDestroy, OnInit } from '@angular/core';
import { RootStoreState, TodoStoreSelectors, TodoStoreActions } from '../../../store';
import { select, Store } from '@ngrx/store';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit, OnDestroy {
todos = this.store$.pipe(select(TodoStoreSelectors.selectList));
constructor(private store$: Store<RootStoreState.State>) {}
ngOnInit() {
this.store$.dispatch(new TodoStoreActions.ListStartPollingAction());
}
ngOnDestroy() {
this.store$.dispatch(new TodoStoreActions.ListStopPollingAction());
}
}
課題
現状effectの単体テストが放置されている。