RxConnect-ReactがRxJSに遭遇したとき

この翻訳は、私自身が書いたドキュメンテーションのロシア語版ですので、質問することをheしないでください。







はじめに



ユーザー入力の処理は、思ったほど簡単ではない場合があります。 ユーザーがまだリクエストを入力している間にサーバーにリクエストを送信したくないのですか? そしてもちろん、ユーザーは常に最後に送信したリクエストの結果を見るはずです。







Reactアプリケーションのインタラクティブなイベントに応答する方法はいくつかありますが、私の意見では、リアクティブアプローチ(RxJSやBaconのようなライブラリのおかげ)は最高の方法の1つです。 RxJSとReactを同時に使用するには、Reactコンポーネントのライフサイクルに対処したり、ストリームサブスクリプションを手動で管理したりする必要があります。 朗報は、これらすべてがRxConnectを使用して自動的に実行できることです。RxConnect-ZeroTurnaroundのAngularからReactへの移行中に開発されたライブラリです。









やる気



最初はReactでした。 そしてそれは良かった。







...しかし、人々はAPIリクエストを作成し、アプリケーションの状態をさまざまなコンポーネントに分散させることは良くないことに気付きました。 そして、Fluxアーキテクチャがありました。 そして、良くなりました。







...しかし、人々は、多くのデータストアを所有する代わりに、データストアが存在する可能性があることに気付きました。 そして、Reduxが登場しました。 そして、それは良くなり、一元化されました。







しかし、別の問題が発生しました-単純なことを行うことが難しくなり、各くしゃみ(ログインフィールドなど)はアクションクリエーター、レデューサーを通過し、グローバルな状態で保存される必要があります。 そして、誰もがReactコンポーネントがローカル状態になっている可能性があることを思い出しました! ダンはどの程度気づいたか







Reactコンポーネントの状態は、アプリケーションのグローバルな状態にとって重要ではない場合、および複雑な変換によって(ローカル状態)が変化しない場合に使用します。 たとえば、チェックボックスの状態、フォームフィールド。

グローバル状態または複雑な変換によって変化する状態の状態リポジトリとしてReduxを使用します。 たとえば、ユーザーのキャッシュ、またはユーザーが入力したドラフト記事。

言い換えれば、最も奇妙な(容認できない)と思われることを行います。

RxJSは、このローカル状態を管理するのにこれまで以上に優れています。







例を考えてみましょう:







RxJSまたは他のライブラリなしで、経過した秒数を示す最も単純なタイマーを作成します。







class Timer extends React.Component { state = { counter: 0 } componentWillMount() { setInterval( () => this.setState(state => ({ counter: state.counter + 1 })), 1000 ) } render() { return <div>{ this.state.counter }</div> } }
      
      





シンプルでしょ? それは不運です-シーンからこのコンポーネントを削除するとどうなりますか? リモートコンポーネントでsetState()



を呼び出すことはできないため、引き続きsetState()



を呼び出して例外をスローします。







そのため、コンポーネントが削除される前に、間隔を解除する必要があります。







 class Timer extends React.Component { state = { value: 0 } intervalRef = undefined; componentWillMount() { this.intervalRef = setInterval( () => this.setState(state => ({ value: state.value + 1 })), 1000 ) } componentWillUnmount() { clearInterval(this.intervalRef); } render() { return <div>{ this.state.value }</div> } }
      
      





この問題は非常に一般的であるため、このライブラリもあります: https : //github.com/reactjs/react-timer-mixin







ここで、Promise、interval、およびその他の非同期ごとに、独自の署名および登録解除ハンドラー、個別のライブラリを作成する必要があることを想像してください。 RxJSがあることは良いことです。RxJSは、こうしたことをリアクティブに処理できる抽象化です。







同じ例ですが、RxJSのみを使用すると、次のようになります。







 class Timer extends React.Component { state = { value: 0 } subscription = undefined; componentWillMount() { this.subscription = Rx.Observable.timer(0, 1000).timestamp().subscribe(::this.setState); } componentWillUnmount() { this.subscription.dispose(); } render() { return <div>{ this.state.value }</div> } }
      
      





しかし、このような単純なタスクには少なすぎるコードではありませんか? 開発者がサブスクリプションでdisposeを呼び出すのを忘れた場合はどうでしょうか? そして、すでにRx.Observable.timer



の形の状態があるRx.Observable.timer



、なぜコンポーネントのローカル状態の形でそれを複製する必要があるのですか?







これは、RxConnectが私たちを助ける場所です:







 import { rxConnect } from "rx-connect"; @rxConnect( Rx.Observable.timer(0, 1000).timestamp() ) class Timer extends React.PureComponent { render() { return <div>{ this.props.value }</div> } }
      
      





(例として、 http://codepen.io/bsideup/pen/wzvGAEで遊んでください







RxConnectは高次コンポーネントとして実装され、サブスクリプション管理ルーチン全体を処理します。これにより、コードがより安全になり、読みやすくなります。 また、コンポーネントにはプロパティの機能があります。 「純粋」。内部状態がないため、テストが大幅に簡素化されます。







React 0.14以降、関数をステートレスのReactコンポーネントとして使用できるようになりました。そのため、コードを1行に変換できます。







 const Timer = rxConnect(Rx.Observable.timer(0, 1000).timestamp())(({ value }) => <div>{value}</div>)
      
      





確かに、クラスのオプションはずっと読みやすいと思います。







人生の例



タイマーは優れていますが、頻繁に対処する必要があります つまらない APIとさまざまなサービスですので、より現実的な例を見てみましょう-ウィキペディアで記事を検索してください。







成分



コンポーネント自体から始めましょう:







 class MyView extends React.PureComponent { render() { const { articles, search } = this.props; return ( <div> <label> Wiki search: <input type="text" onChange={ e => search(e.target.value) } /> </label> { articles && ( <ul> { articles.map(({ title, url }) => ( <li><a href={url}>{title}</a></li> ) ) } </ul> ) } </div> ); } }
      
      





お気づきかもしれませんが、次の2つのプロパティが必要です。









コンポーネントはクリーンでステートレスです。 コードを変更することはもうないので、覚えておいてください!







注:RxConnectは、変更せずに既存のReactコンポーネントと連携します







反応性成分



コンポーネントを外部に接続するときが来ました:







 import { rxConnect } from "rx-connect"; @rxConnect(Rx.Observable.of({ articles: [ { title: "Pure (programming Language)", url: "https://en.wikipedia.org/wiki/Pure_(programming_language)" }, { title: "Reactive programming", url: "https://en.wikipedia.org/wiki/Reactive_programming" }, ] })) class MyView extends React.PureComponent { // ... }
      
      





遊び回るhttp : //codepen.io/bsideup/pen/VKwKGv







ここでは、2つの要素の静的配列を囲むことによってデータをシミュレートし、コンポーネントが表示されることを確認しました! やった!







**注: rxConnect



メソッドに渡される関数は、コンポーネントのObservable



プロパティを返す必要があります。







リアクティブインタラクティブコンポーネント



すべてが確かにクールですが、...検索? ユーザーはまだコンポーネントと対話できません。 要件は次のとおりです。









RxJSのおかげで、これを簡単に実装できます。







 import { rxConnect, ofActions } from "rx-connect"; function searchWikipedia(search) { return Rx.DOM .jsonpRequest(`https://en.wikipedia.org/w/api.php?action=opensearch&search=${search}&format=json&callback=JSONPCallback`) .pluck("response") // Wikipedia      o_O .map(([,titles,,urls]) => titles.map((title, i) => ({ title, url: urls[i] }))) } @rxConnect(() => { const actions = { search$: new Rx.Subject() } const articles$ = actions.search$ .pluck(0) //      .flatMapLatest(searchWikipedia) return Rx.Observable.merge( Rx.Observable::ofActions(actions), articles$.map(articles => ({ articles })) ) }) class MyView extends React.PureComponent { // ... }
      
      





(再生するには: http : //codepen.io/bsideup/pen/rrNrEo 注意!あまり早く入力しないでください。そうしないと、リクエスト数に関するAPIの制限が発生します (以下を参照)







うまくいきました! 印刷して結果を確認します。







コードをステップごとに見ていきましょう。







 const actions = { search$: new Rx.Subject() }
      
      





ここでは、ユーザーアクションからオブジェクトを作成します。 それらは被験者です。 必要な数のエンティティを宣言できます。







アクション名の最後に$記号がありますか? これは、データフローを識別するためのRxJSの特別な表記法です。 RxConnectはそれを省略し、コンポーネントはそれをsearch



プロパティとして受け取ります。







しかし、アクション自体は何もしません。反応の助けを借りてそれらに応答しなければなりません







 const articles$ = actions.search$ .pluck(0) // select first passed argument .flatMapLatest(searchWikipedia)
      
      





これで、検索する反応は1つだけになりましたが、多くの反応が存在する可能性があるため、すべての反応のフローを1つにまとめました。







 return Rx.Observable.merge( Rx.Observable::ofActions(actions), articles$.map(articles => ({ articles })) )
      
      





記事のストリームは、コンポーネントのarticles



プロパティに変換されます。







インタラクティブAPIレスポンシブインタラクティブコンポーネント



現在の実装では、ユーザーが入力フィールドに新しい文字を入力するたびにAPIをリクエストします。 これは、ユーザーが頻繁に入力する場合(たとえば、1秒あたり10文字)、1秒あたり10リクエストを送信することを意味します。 しかし、ユーザーは、入力を停止したときの最後のリクエストの結果のみを見たいと考えています。 そして、そのような状況は、RxJSを選択する理由の良い例です。なぜなら、それはそのような状況を処理するように設計されているからです!







反応を少し変更します。







 actions.search$ .debounce(500) // <-- RxJS ! .pluck(0) .flatMapLatest(searchWikipedia)
      
      





(遊び回る: http : //codepen.io/bsideup/pen/gwOLdK (できるだけ早く入力することを恐れないでください







500ミリ秒間ユーザーからの入力がない場合にのみリクエストを送信するため、ユーザーは任意の速度で入力できます。つまり、サーバーは1秒あたり最大2つのリクエストを受信します。







注:RxJSを学ぶ、それは素晴らしいです:)







詳細に注意を払ったAPIレスポンシブインタラクティブコンポーネント



入力フィールドに何かを入力します。 結果を確認したら、別のものを入力します。 サーバーから新しい要求に対する応答を受け取るまで、古い結果は画面に残ります。 あまりきれいではありませんが、簡単に修正できます。







データストリームを組み合わせる、つまりコンポーネントがリアクティブであることを言ったことを思い出してください。 これにより、リクエストをサーバーに送信すると同時に、その結​​果の前に空のオブジェクトを送信するよりも、前の結果をクリアするのは難しくありません。







 actions.search$ .debounce(500) .pluck(0) .flatMapLatest(search => searchWikipedia(search) .startWith(undefined) // <--     undefined,  ,   ,     )
      
      





結果: http : //codepen.io/bsideup/pen/mAbaom







Redux



記事のサイズを小さくするために、Reduxのトピックについては説明しません。RxConnectはReduxで正常に機能し、 @connect



代わりにコンポーネントをリアクティブにリンクできるとしか言えません。 例:







 @rxConnect((props$, state$, dispatch) => { const actions = { logout: () => dispatch(logout()), }; const user$ = state$.pluck("user").distinctUntilChanged(); return Rx.Observable.merge( Rx.Observable::ofActions(actions), user$.map(user => ({ user })), ); }) export default class MainView extends React.PureComponent { // ... }
      
      





例: https : //github.com/bsideup/rx-connect/tree/master/examples/blog/

デモ: https : //bsideup.github.io/rx-connect/examples/blog/dist/







おわりに



リアクティブプログラミングは思ったより簡単かもしれません。 ほとんどのコンポーネントをRxJSに移行した後、他の方法は見当たりません。 また、RxConnectにより、不要なコードや潜在的なサブスクリプション管理エラーを回避できました。







参照資料






All Articles