RxJSを用いた実装パターンの実例まとめ(後編)
小飼です。
前回に引き続き、RxJSのコード実例を紹介します。
ドラッグストリームの作成
onmousedown
・onmouseup
・onmousemove
イベントから作ったストリームをより合わせて、 『マウスドラッグ』というストリームを作成します。
こういった『既存の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)--|
、c
とf
を観測できれば充分なことも多いです。
特にマーブル記法でテストを書いていると、同期的なイベント全てをテストしているとテストが冗長になって可読性が下がってきますので、できれば必要なイベントだけを発行するようにフィルタできるとうれしいです。
そんな時は、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を状態管理に用いたアプリケーションで実際に遭遇したストリーム操作をまとめてみました。
参考になればうれしいです。