LoginSignup
5
1

More than 5 years have passed since last update.

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

version

  • Angular 7.0.4
  • ngrx 6.1.2
  • rxjs 6.3.3

実装方針

実装方針は2通りあって

  1. component側でデータ更新のactionを定期的にdispatchする。
  2. 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の単体テストが放置されている。

5
1
0

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
5
1