React DevelopmentでRxJSを使用してアプリケーションの状態を管理する

本書の著者である翻訳者は、ここで翻訳を公開しているが、ここでは、RxJSを使用する単純なReactアプリケーションの開発プロセスを示したいと述べています。 彼によると、彼はRxJSの専門家ではありません。彼はこのライブラリの研究に従事しており、知識のある人々の助けを拒否しないからです。 その目標は、Reactアプリケーションを作成する別の方法に聴衆の注意を引き、読者に独立した調査を行うように促すことです。 この資料をRxJSの紹介と呼ぶことはできません。 React開発でこのライブラリを使用する多くの方法の1つをここに示します。



画像



それがすべて始まった方法



最近、 私のクライアント RxJSを使用してReactアプリケーションの状態を管理することについて学びたいと思いました。 このクライアントのアプリケーションコードを監査したとき、彼はアプリケーションの開発方法に関する私の意見を知りたいと思っていました。 このプロジェクトは、Reactのみに依存するのは不合理な点に達しました。 まず、アプリケーションの状態を管理するためのより良い手段としてReduxまたはMobXを使用することについて説明しました。 私のクライアントは、これらの各テクノロジーのプロトタイプを作成しました。 しかし、彼はこれらのテクノロジーに留まらず、RxJSを使用するReactアプリケーションのプロトタイプを作成しました。 その瞬間から、私たちの会話はもっと面白くなりました。



問題のアプリケーションは、暗号通貨の取引プラットフォームでした。 データがリアルタイムで更新される多くのウィジェットがありました。 このアプリケーションの開発者は、とりわけ、次の困難なタスクを解決する必要がありました。





その結果、開発者が直面した主な困難はReactライブラリ自体とは関係がなく、さらに、この分野で彼らを助けることができました。 主な問題は、システムの内部メカニズム、つまり、暗号通貨データとReactツールで作成されたインターフェイス要素をリンクするメカニズムを正しく機能させることでした。 RxJSの機能が非常に有用であることが判明したのはこの分野であり、彼らが私に見せたプロトタイプは非常に有望に見えました。



ReactでのRxJSの使用



ローカルアクションを実行した後、 サードパーティAPIにリクエストを送信するアプリケーションがあるとします 。 記事で検索できます。 リクエストを実行する前に、このリクエストを形成するために使用されるテキストを取得する必要があります。 特に、このテキストを使用して、APIにアクセスするためのURLを作成します。 この機能を実装するReactコンポーネントのコードは次のとおりです



import React from 'react'; const App = ({ query, onChangeQuery }) => (  <div>    <h1>React with RxJS</h1>    <input      type="text"      value={query}      onChange={event => onChangeQuery(event.target.value)}    />    <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p>  </div> ); export default App;
      
      





このコンポーネントには、状態管理システムがありません。 query



プロパティの状態はどこにも保存されませんonChangeQuery



関数も状態を更新しません。 従来のアプローチでは、このようなコンポーネントにはローカル状態管理システムが装備されています。 次のようになります。



 class App extends React.Component { constructor(props) {   super(props);   this.state = {     query: '',   }; } onChangeQuery = query => {   this.setState({ query }); }; render() {   return (     <div>       <h1>React with RxJS</h1>       <input         type="text"         value={this.state.query}         onChange={event =>           this.onChangeQuery(event.target.value)         }       />       <p>{`http://hn.algolia.com/api/v1/search?query=${         this.state.query       }`}</p>     </div>   ); } } export default App;
      
      





ただし、これはここで説明するアプローチではありません。 代わりに、RxJSを使用してアプリケーション状態管理システムを確立する必要があります。 高次コンポーネント(HOC)を使用してこれを行う方法を見てみましょう。



必要に応じて、 App



コンポーネントに同様のロジックを実装できますが、おそらくアプリケーションの作業のある時点で、再利用に適したHOCの形式でそのようなコンポーネントを設計することを決定します。



ReactおよびRxJSトップオーダーコンポーネント



この目的のために高次のコンポーネントを使用して、RxJSを使用してReactアプリケーションの状態を管理する方法を見つけます。 代わりに、「小道具のレンダリング」 テンプレートを実装できます。 そのため、この目的のために高次コンポーネントを自分で作成したくない場合は、 mapPropsStream()



およびcomponentFromStream()



観察可能な高次コンポーネントmapPropsStream()



を使用できます。 ただし、このガイドでは、すべてを自分で行います。



 import React from 'react'; const withObservableStream = (...) => Component => { return class extends React.Component {   componentDidMount() {}   componentWillUnmount() {}   render() {     return (       <Component {...this.props} {...this.state} />     );   } }; }; const App = ({ query, onChangeQuery }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); export default withObservableStream(...)(App);
      
      





一方、高次コンポーネントRxJSはアクションを実行しません。 独自の状態とプロパティのみを入力コンポーネントに転送します。入力コンポーネントは、その助けを借りて拡張される予定です。 ご覧のとおり、最終的には、React状態の管理に高次のコンポーネントが関与します。 ただし、この状態は、観測されたフローから取得されます。 HOCの実装とApp



コンポーネントでの使用を開始する前に、RxJSをインストールする必要があります。



 npm install rxjs --save
      
      





それでは、高次コンポーネントの使用を開始して、そのロジックを実装しましょう。



 import React from 'react'; import { BehaviorSubject } from 'rxjs'; ... const App = ({ query, onChangeQuery }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> ); const query$ = new BehaviorSubject({ query: 'react' }); export default withObservableStream( query$, {   onChangeQuery: value => query$.next({ query: value }), } )(App);
      
      





App



コンポーネント自体は変更されません。 2つの引数を高次コンポーネントに渡しました。 それらについて説明します。





監視対象オブジェクトを作成してサブスクライブすると、リクエストのフローが機能するはずです。 ただし、これまでは、高次コンポーネント自体はブラックボックスのように見えます。 私たちはそれを実現します:



 const withObservableStream = (observable, triggers) => Component => { return class extends React.Component {   componentDidMount() {     this.subscription = observable.subscribe(newState =>       this.setState({ ...newState }),     );   }   componentWillUnmount() {     this.subscription.unsubscribe();   }   render() {     return (       <Component {...this.props} {...this.state} {...triggers} />     );   } }; };
      
      





高次コンポーネントは、オブザーバブルオブジェクトとトリガーを持つオブジェクト(関数を含むこのオブジェクトは、RxJSレキシコンからより成功した用語と呼ばれる可能性があります)を受け取ります。



トリガーは、HOCを介して入力コンポーネントにのみ渡されます。 そのため、 App



コンポーネントはonChangeQuery()



関数を直接受け取ります。この関数は、 onChangeQuery()



対象オブジェクトと直接連携して、新しい値を渡します。



監視対象オブジェクトは、 componentDidMount()



ライフサイクルメソッドを使用して署名し、 componentDidMount()



メソッドを使用して登録解除します。 メモリリークを防ぐには、登録解除が必要です。 オブザーバブルオブジェクトのサブスクリプションでは、関数はthis.setState()



コマンドを使用して、ストリームからのすべての着信データをローカルのReact状態ストアに送信するだけです。



App



コンポーネントに小さな変更を加えましょう。これにより、高次コンポーネントがquery



プロパティの初期値を設定しない場合に発生する問題を取り除きます。 これが行われない場合、作業の開始時に、 query



プロパティはundefined



ます。 この変更により、このプロパティはデフォルト値を取得します。



 const App = ({ query = '', onChangeQuery }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <p>{`http://hn.algolia.com/api/v1/search?query=${query}`}</p> </div> );
      
      





この問題に対処する別の方法は、高次コンポーネントでquery



の初期状態を設定することです。



 const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component {   constructor(props) {     super(props);     this.state = {       ...initialState,     };   }   componentDidMount() {     this.subscription = observable.subscribe(newState =>       this.setState({ ...newState }),     );   }   componentWillUnmount() {     this.subscription.unsubscribe();   }   render() {     return (       <Component {...this.props} {...this.state} {...triggers} />     );   } }; }; const App = ({ query, onChangeQuery }) => ( ... ); export default withObservableStream( query$, {   onChangeQuery: value => query$.next({ query: value }), }, {   query: '', } )(App);
      
      





このアプリケーションを今すぐ試してみると、入力ボックスは期待どおりに動作するはずです。 App



コンポーネントは、プロパティの形式でHOCからquery



状態と状態を変更するためのonChangeQuery



関数のみをonChangeQuery



ます。



内部React状態ストアが高次コンポーネント内で使用されるという事実にもかかわらず、状態の取得と変更はRxJS監視可能オブジェクトを介して行われます。 監視対象オブジェクトのサブスクリプションから拡張コンポーネント( App



)のプロパティに直接データをストリーミングする問題の明らかな解決策が見つかりませんでした。 そのため、Reactのローカル状態を中間層の形で使用する必要がありました。これは、さらに、再レンダリングの原因となる点で便利です。 同じ目標を達成する別の方法を知っている場合は、コメントで共有できます。



Reactで観測されたオブジェクトを結合する



query



プロパティのように、 App



コンポーネントで使用できる値の2番目のストリームを作成しましょう。 後で両方の値を使用し、別の監視可能なオブジェクトを使用してそれらを操作します。



 const SUBJECT = { POPULARITY: 'search', DATE: 'search_by_date', }; const App = ({ query = '', subject, onChangeQuery, onSelectSubject, }) => ( <div>   <h1>React with RxJS</h1>   <input     type="text"     value={query}     onChange={event => onChangeQuery(event.target.value)}   />   <div>     {Object.values(SUBJECT).map(value => (       <button         key={value}         onClick={() => onSelectSubject(value)}         type="button"       >         {value}       </button>     ))}   </div>   <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p> </div> );
      
      





ご覧のとおり、APIにアクセスするために使用されるURLを作成するときに、 subject



パラメーターを使用して要求を絞り込むことができます。 すなわち、資料は、その人気または発行日に基づいて検索できます。 次に、 subject



パラメーターの変更に使用できる別の監視可能なオブジェクトを作成します。 このオブザーバブルを使用して、 App



コンポーネントをより高次のコンポーネントに関連付けることができます。 そうしないと、 App



コンポーネントに渡されたプロパティが機能しません。



 import React from 'react'; import { BehaviorSubject, combineLatest } from 'rxjs/index'; ... const query$ = new BehaviorSubject({ query: 'react' }); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({   subject,   query, })), {   onChangeQuery: value => query$.next({ query: value }),   onSelectSubject: subject => subject$.next(subject), }, )(App);
      
      





onSelectSubject()



トリガーは新しいものではありません。 ボタンを介して、2つのsubject



状態を切り替えるために使用できます。 しかし、観測されたオブジェクトは、高次のコンポーネントに渡され、新しいものです。 combineLatest()



関数を使用して、2つ(またはそれ以上)の監視スレッドから最後に返された値を結合します。 監視対象オブジェクトをサブスクライブした後、値( query



またはsubject



)のいずれかが変更されると、サブスクライバーは両方の値を受け取ります。



combineLatest()



関数によって実装されるメカニズムを補完するものが最後の引数です。 ここで、監視対象オブジェクトによって生成された値の戻り順序を設定できます。 この場合、オブジェクトとして表現する必要があります。 これにより、以前と同様に、それらを高次のコンポーネントで分解し、Reactのローカル状態に書き込むことができます。 必要な構造が既にあるので、観測されたquery



オブジェクトのオブジェクトをラップするステップを省略できます。



 ... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); export default withObservableStream( combineLatest(subject$, query$, (subject, query) => ({   subject,   query, })), {   onChangeQuery: value => query$.next(value),   onSelectSubject: subject => subject$.next(subject), }, )(App);
      
      





ソースオブジェクト{ query: '', subject: 'search' }



、監視対象オブジェクトの結合ストリームによって返される他のすべてのオブジェクトは、それらを高次コンポーネントで分解し、対応する値をローカルのReact状態に書き込むのに適しています。 前と同様に状態を更新した後、レンダリングが実行されます。 更新されたアプリケーションを起動すると、入力フィールドとボタンを使用して両方の値を変更できるはずです。 変更された値は、APIへのアクセスに使用されるURLに影響します。 これらの値の1つのみが変更された場合でも、 combineLatest()



関数は常に観測されたフローから発行された最新の値を結合するため、他の値はその最後の状態を保持します。



ReactでのAxiosとRxJS



システムでは、APIにアクセスするためのURLは、他の2つのオブザーバブルオブジェクトを含む結合オブザーバブルオブジェクトからの2つの値に基づいて構築されます。 このセクションでは、URLを使用してAPIからデータをロードします。 Reactデータ読み込みシステムの使用は得意かもしれませんが、RxJSオブザーバブルを使用する場合は、アプリケーションに別のオブザーバブルストリームを追加する必要があります。



次の監視対象オブジェクトの作業に取りかかる前に、 axiosをインストールします。 これは、ストリームからプログラムにデータをロードするために使用するライブラリです。



 npm install axios --save
      
      





ここで、 App



コンポーネントが出力する記事の配列があると想像してください。 ここでは、デフォルトで対応するパラメーターの値として、空の配列を使用します。これは、他のパラメーターで既に行ったのと同じことです。



 ... const App = ({ query = '', subject, stories = [], onChangeQuery, onSelectSubject, }) => ( <div>   ...   <p>{`http://hn.algolia.com/api/v1/${subject}?query=${query}`}</p>   <ul>     {stories.map(story => (       <li key={story.objectID}>         <a href={story.url || story.story_url}>           {story.title || story.story_title}         </a>       </li>     ))}   </ul> </div> );
      
      





リスト内の各記事では、アクセスしているAPIが異種であるという事実のために、代替値の使用が提供されています。 現在、最も興味深いのは、新しいオブザーバブルオブジェクトの実装です。このオブジェクトは、データを視覚化するReactアプリケーションにデータをロードする役割を果たします。



 import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { flatMap, map } from 'rxjs/operators'; ... const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY); const fetch$ = combineLatest(subject$, query$).pipe( flatMap(([subject, query]) =>   axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ...
      
      





新しいオブザーバブルオブジェクトは、 subject



query



オブザーバブルの組み合わせです。なぜなら、APIを使用してデータを読み込むURLを作成するには、両方の値が必要だからです。 監視対象オブジェクトのpipe()



メソッドでは、値を使用して特定のアクションを実行するために、いわゆる「RxJS演算子」を使用できます。 この場合、クエリに配置される2つの値をマッピングし、axiosが結果を取得するために使用します。 ここでは、 flatMap()



ではなくflatMap()



演算子を使用して、返されたpromise自体ではなく、正常に解決されたpromiseの結果にアクセスします。 その結果、この新しいオブザーバブルオブジェクトにサブスクライブした後、他のオブザーバブルオブジェクトから新しいsubject



またはquery



値がシステムに入力されるたびに、新しいquery



が実行され、結果がサブスクリプション関数になります。



これで、再び、高次コンポーネントに新しいオブザーバブルオブジェクトを提供できます。 私たちはcombineLatest()



関数の最後の引数を持っています。これにより、これをstories



というプロパティに直接マッピングすることができます。 最終的に、これは、このデータがApp



コンポーネントで既にどのように使用されているかを表しています。



 export default withObservableStream( combineLatest(   subject$,   query$,   fetch$,   (subject, query, stories) => ({     subject,     query,     stories,   }), ), {   onChangeQuery: value => query$.next(value),   onSelectSubject: subject => subject$.next(subject), }, )(App);
      
      





監視対象オブジェクトは、他の2つの監視フローによって間接的にアクティブ化されるため、ここではトリガーはありません。 入力フィールド( query



)の値が変更されるか、ボタン( subject



)がクリックされるたびに、これは両方のストリームからの最新の値を含むfetch



対象fetch



オブジェクトに影響します。



ただし、入力フィールドの値が変わるたびに、 fetch



対象のfetch



オブジェクトに影響を与える必要はないでしょう。 さらに、値が空の文字列で表される場合、 fetch



に影響を与えたくないでしょう。 そのため、 debounce



ステートメントを使用して、 debounce



可能なquery



オブジェクトを拡張できquery



。これにより、クエリの頻繁な変更が不要になります。 つまり、このメカニズムのおかげで、新しいイベントは、前のイベントから所定の時間が経過した後にのみ受け入れられます。 さらに、ここでfilter



演算子を使用しfilter



。これは、 query



文字列が空の場合にストリームイベントをフィルターで除外します。



 import React from 'react'; import axios from 'axios'; import { BehaviorSubject, combineLatest, timer } from 'rxjs'; import { flatMap, map, debounce, filter } from 'rxjs/operators'; ... const queryForFetch$ = query$.pipe( debounce(() => timer(1000)), filter(query => query !== ''), ); const fetch$ = combineLatest(subject$, queryForFetch$).pipe( flatMap(([subject, query]) =>   axios(`http://hn.algolia.com/api/v1/${subject}?query=${query}`), ), map(result => result.data.hits), ); ...
      
      





debounce



ステートメントは、フィールドにデータを入力するジョブを実行します。 ただし、ボタンがsubject



値をクリックするsubject



、リクエストはすぐに実行されます。



これで、 App



コンポーネントを初めて表示したときに表示されるquery



およびsubject



の初期値は、監視対象オブジェクトの初期値から取得したものと同じではありません。



 const query$ = new BehaviorSubject('react'); const subject$ = new BehaviorSubject(SUBJECT.POPULARITY);
      
      





subject



subject



記述され、 query



は空の文字列が含まれquery



。 これは、署名内のApp



コンポーネントの機能を分解するためのデフォルトのパラメーターとして提供したのはこれらの値であったためです。 これは、 fetch



対象のfetch



オブジェクトによって実行される最初の要求を待つ必要があるためです。 ローカル状態に書き込むために、高次コンポーネントのオブザーバブルquery



およびsubject



オブジェクトから値をすぐに取得する方法が正確にはわからないため、高次コンポーネントの初期状態を再度設定することにしました。



 const withObservableStream = ( observable, triggers, initialState, ) => Component => { return class extends React.Component {   constructor(props) {     super(props);     this.state = {       ...initialState,     };   }   componentDidMount() {     this.subscription = observable.subscribe(newState =>       this.setState({ ...newState }),     );   }   componentWillUnmount() {     this.subscription.unsubscribe();   }   render() {     return (       <Component {...this.props} {...this.state} {...triggers} />     );   } }; };
      
      





これで、初期状態を3番目の引数として高次コンポーネントに提供できます。 将来的には、 App



コンポーネントのデフォルト設定を削除できます。



 ... const App = ({ query, subject, stories, onChangeQuery, onSelectSubject, }) => ( ... ); export default withObservableStream( combineLatest(   subject$,   query$,   fetch$,   (subject, query, stories) => ({     subject,     query,     stories,   }), ), {   onSelectSubject: subject => subject$.next(subject),   onChangeQuery: value => query$.next(value), }, {   query: 'react',   subject: SUBJECT.POPULARITY,   stories: [], }, )(App);
      
      





私が今心配しているのは、監視対象オブジェクトquery$



およびsubject$



の宣言にも初期状態が設定されていることです。 観測されたオブジェクトの初期化と高次コンポーネントの初期状態は同じ値を共有するため、このアプローチはエラーを起こしやすいです。 代わりに、初期状態を設定するために、高次のコンポーネントで観測されたオブジェクトから初期値を取得した方がよかったでしょう。 おそらく、この資料の読者の1人が、これを行う方法に関するコメントでアドバイスを共有できるでしょう。



ここで行ったソフトウェアプロジェクトのコードは、ここにあります



まとめ



この記事の主な目標は、RxJSを使用してReactアプリケーションを開発するための代替アプローチを示すことです。 彼があなたに食べ物を与えてくれることを願っています。 ReduxとMobXは必要ない場合もありますが、そのような状況では、RxJSが特定のプロジェクトに適したものになるでしょう。



親愛なる読者! Reactアプリケーションを開発するときにRxJSを使用していますか?






All Articles