Edited at

recomposeとRxJSでpropsをstream化する方法

長年recomposeでのObservable利用というのが理解出来なかったが、一部唐突に理解できたので忘れる前にメモ。


前提の考え:propsだけをstreamとして考える

とにかくRxを考える上で自分の理解を妨げていたのが「Everything is stream」という考え方だった。

redux-observableを覚えた際は、streamとなるのがReduxのActionだけであると限定出来たから理解できたと考えている。

そのため今回は mapPropsStream の使い方のみフォーカスする。そのためstreamになるのはコンポーネントに渡され続けるpropsだけだ。

(それ以外のものは未だに使い方がいまいちわかってない所がある。特に componentFromStreamはいまいち用途を想像出来てない。)


例:文字数カウンター

例えばこんな文字数カウンターを考える

    export class TextArea extends React.Component {

constructor(props) {
super(props)
this.state = {
text: ""
}
}
render() {
return (
<div>
<textarea
value={this.state.text}
onChange={(e) => {
this.setState({ text: e.target.value })
}}
/>
<TextCounter text={this.state.text} />
</div>
)
}
}

TextCounterの実装は一番愚直に考えればこんな感じだろう

    const TextCounter = ({ text }) => {

const count = text.count
return (
<div>
<div>
{count}
文字
</div>
<div>{text}</div>
</div>
)
}

ここで文字入力の度に更新が走ることを危惧した場合、class化してメモ化するなど様々手段は考えられるが、今回ここでpropsをObservableにする方法で解決してみたい。


まずはObservable使わずmapPropsだけで解決する

ひとまずcounterの計算処理をComponentから剥がしてみよう。

それだけであればObservableは不要で mapProps で十分だ。

    const withTextCounter = mapProps(({ text }) => {

return {
text,
count: text.length
}
})
const TextCounterInner = ({ text, count }) => {
// const count = text.count
 // カウンターはもう外からもらっているので計算不要
return (
<div>
<div>
{count}
文字
</div>
<div>{text}</div>
</div>
)
}
const TextCounter = withTextCounter(TextCounterInner)


更新回数をmapPropsStreamを利用して間引く。

ここで例えば更新回数が多すぎるということでObservableで更新を間引きたい。

今回はRxJSを使う。

不幸にもRxJS@6で上手く動いてないバグを踏み抜いてしまったので、rxjsObservableConfig は今回使わずにセットアップする

    import { tap, auditTime, map } from "rxjs/operators"

import { mapPropsStreamWithConfig } from "recompose"
import { from } from "rxjs"

const rxjsConfig = {
fromESObservable: from,
toESObservable: (stream) => stream
}
// configを与えてmapPropsStreamを作成
const mapPropsStream = mapPropsStreamWithConfig(rxjsConfig)

これで mapPropsStream の準備が出来たので、今回のcomponent用のstreamを作成する。

これはredux-observableのEpicととても良く似ている記述になる。今回は audiTimeを利用して200msに一度だけ更新されるようにする

    const withTextCounter = mapPropsStream((props$) =>

props$.pipe(
// tap(props => console.log("before props", props)),
// -> before props, {text: "aaa"}

auditTime(200),
// 200msの入力をまとめる。throttleやdebounceと似てるが最後にemitするので一番都合が良い

map(({ text }) => {
return {
text,
count: text.length
}
}),
// textとcountに分岐する。ここは先程のmapPropsと一緒

// tap(props => console.log("after props", props)),
// -> after props, { text: "aaa", count: 3 }
)
)

const TextCounter = withTextCounter(TextCounterInner)

カウンターは先程までのものと同じのが利用できるだろう