ngrxでpolling

ngrxでpollingをどう実装しようか悩み、slackで相談しつつ実装したので、考えたことや実装したコードを共有する。

## version
- Angular 7.0.4
- ngrx 6.1.2
- rxjs 6.3.3

## 実装方針
実装方針は２通りあって

1. component側でデータ更新のactionを定期的にdispatchする。
2. effect側でintervalを組み込んでpollingする。

1の方が実装はシンプル。２はpollingをキャンセルできるようにstore側を作り込む必要があって、若干複雑になる。
ただ２の方がcomponentがシンプルになってテストもしやすいので、２を採用。

2で実装する場合pollingのstart/stopをどのように表現するかだけど

- データupdateのシンプルなactionを実装する
- pollingをstart/stopするactionを定義し、componentはそれを呼び出す。
    - effectの中でupdate actionを呼び続ける。
    - stateにisPollingを定義し、その状態を変更するとpollingが停止するようにする。

という感じで実装した。

## 実装
エラー処理などはいくらか省いてる。
### state
```typescript
import { Todo } from '../../model/todo';

export interface TodoState {
  todoList: Todo[];
  isPolling: boolean;
}

export const initialTodoState: TodoState = {
  todoList: [],
  isPolling: false
};
```
### action
```typescript
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を停止させる。

```typescript
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からは以下のように使える。

```typescript
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の単体テストが放置されている。