RxJSを用いた実装パターンの実例まとめ(後編)

MMM Corporation
mmmuser

小飼です。
前回に引き続き、RxJSのコード実例を紹介します。

ドラッグストリームの作成

onmousedownonmouseuponmousemoveイベントから作ったストリームをより合わせて、 『マウスドラッグ』というストリームを作成します。
こういった『既存のDOMイベントを混ぜ合わせて新しいストリームを作る』ようなことは、Rxの最も得意とする領域だと思います。

const { merge, combineLatest } = Observable;

const ACTION_ON_MOUSE_DOWN = "ACTION_ON_MOUSE_DOWN";
const ACTION_ON_MOUSE_UP = "ACTION_ON_MOUSE_UP";
const ACTION_ON_MOUSE_MOVE = "ACTION_ON_MOUSE_MOVE";

interface Point {
  x: number;
  y: number;
}

interface RootState {
  point: Point;
}

const subject = new Subject<Action<any>>();

const onMouseDown = (event: React.MouseEvent<HTMLDivElement>): void => subject.next({
  type: ACTION_ON_MOUSE_DOWN,
});

const onMouseUp = (event: React.MouseEvent<HTMLDivElement>): void => subject.next({
  type: ACTION_ON_MOUSE_UP,
});

const onMouseMove = (event: React.MouseEvent<HTMLDivElement>): void => {
  const nativeEvent = event.nativeEvent as MouseEvent;
  subject.next({
    type: ACTION_ON_MOUSE_MOVE,
    payload: {
      x: nativeEvent.offsetX,
      y: nativeEvent.offsetY,
    },
  });
};

const root$ = (): Observable<RootState> => {
  const onMouseDown$ = subject.ofType<void>(ACTION_ON_MOUSE_DOWN);
  const onMouseUp$ = subject.ofType<void>(ACTION_ON_MOUSE_UP);
  const onMouseMove$ = subject.ofType<Point>(ACTION_ON_MOUSE_MOVE);

  const isDragging$ = merge(onMouseDown$, onMouseUp$)
    .scan<boolean>(isDragging => !isDragging, false);

  const drag$ = combineLatest(
      isDragging$, onMouseMove$,
      (isDragging, nextPoint) => ({ isDragging, nextPoint })
    )
    .filter(({ isDragging }) => isDragging)
    .map(({ nextPoint }) => nextPoint);

  const initialState = { point: { x: 0, y: 0 } };

  return drag$
    .map(point => ({ point }))
    .startWith(initialState)
    ;
};


export class SampleApp extends Component<void, RootState> {
  componentWillMount() {
    root$().subscribe(root => this.setState(root));
  }

  render() {
    const { point } = this.state;
    return (
      <div>
        <div
          onMouseDown={ onMouseDown }
          onMouseMove={ onMouseMove }
          onMouseUp={ onMouseUp }
        >ドラッグできる要素</div>
        <span>X: { point.x }</span>
        <span>Y: { point.y }</span>
      </div>
    );
  }
}

同期的なイベントのフィルタ

複数のストリームをcombineLatestなどで合成した時、同期的に(同じフレームに)イベントが発行されてくるケースがあります。
例えばこのようなストリームの場合--(abc)--(def)--|cfを観測できれば充分なことも多いです。

特にマーブル記法でテストを書いていると、同期的なイベント全てをテストしているとテストが冗長になって可読性が下がってきますので、できれば必要なイベントだけを発行するようにフィルタできるとうれしいです。
そんな時は、auditTimeオペレータに極小の時間を渡してあげることで、同期的なイベントだけをフィルタしたストリームが作れます。

const { of, merge } = Observable;

const foo$ = of("foo");
const bar$ = of("bar");

const combined$ = merge(foo$, bar$)
  .auditTime(5);

この時、テストのためにスケジューラを注入するために

import { TestCheduler } from "rxjs"
const createCombined$ = (shceduler: TestScheduler = null) => merge(foo$, bar$)
  .auditTime(5, shceduler);

のように、nullをデフォルト値に渡しているサンプルコードを見かける時があります。(4.xを使った時のコード)

Reactive Programming with RxJSのテストの章にも同様の記述が載ってしまっているのですが、本来『デフォルト引数が与えられていない』ことを表現するためにはnullでなくundefinedを使うことが期待されています。

TypeScriptに移行した(ことでデフォルトパラメータの扱いがES標準に準拠した)RxJS 5.xでは、このような場合undefinedを渡さないと例外を投げられることになりますので少しだけ注意が必要です。

import { TestCheduler } from "rxjs"
const createCombined$ = (shceduler: TestScheduler = undefined) => merge(foo$, bar$)
  .auditTime(5, shceduler);

ストリームの配列を配列のストリームに変換する

ちょっとややこしいですが、Observable<SomeType>[]Observable<SomeType[]>にするような操作のことを指しています。
例えばラジオボタンのような、『複数のソースストリームがあるけど出力したいイベントは一つ』みたいなストリームを作る時に使います。

const input1$ = of(1);
const input2$ = of(2);
const input3$ = of(3);
const input4$ = of(4);
const input5$ = of(5);

const input$List: Observable<number>[] = [input1$, input2$, input3$, input4$, input5$];
const inputs$ = combineLatest(...input$List);

// input$Listに格納せずに
// const inputs$ = combineLatest(input1$, input2$, input3$, input4$, input5$);
// とも書ける

まとめ

以上、RxJSを状態管理に用いたアプリケーションで実際に遭遇したストリーム操作をまとめてみました。
参考になればうれしいです。

AUTHOR
デロイト トーマツ ウェブサービス株式会社(DWS)
デロイト トーマツ ウェブサービス株式会社(DWS)
デロイト トーマツ ウェブサービス株式会社はアマゾン ウェブ サービス(AWS)に 専門性や実績を認定された公式パートナーです。
記事URLをコピーしました