
それがすべて始まった方法
最近、 私のクライアントは、 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つの引数を高次コンポーネントに渡しました。 それらについて説明します。
- 観測されたオブジェクト。
query
引数は、初期値を持つ観測可能なオブジェクトですが、時間の経過とともに新しい値を返します(これはBehaviorSubject
)。 誰でもこの監視可能オブジェクトにサブスクライブできます。BehaviorSubject
型のオブジェクトについてRxJSのドキュメントが述べているのは、「Subject
オブジェクトのオプションの1つは、「現在の値」の概念を使用するBehaviorSubject
です。 サブスクライバに渡された最後の値を保存し、新しいオブザーバがサブスクライブすると、すぐにBehaviorSubject
からこの「現在の値」を受け取ります。 このようなオブジェクトは、新しい部分が時間とともに表示されるデータの表示に適しています。」 - 監視対象オブジェクトに新しい値を発行するためのシステム(トリガー)。 HOCを介して
App
コンポーネントに渡されるonChangeQuery()
関数は、onChangeQuery()
対象オブジェクトに次の値を渡す通常の関数です。 この関数は、観測されたオブジェクトで特定のアクションを実行するいくつかの関数を高次コンポーネントに転送する必要がある場合があるため、オブジェクトで転送されます。
監視対象オブジェクトを作成してサブスクライブすると、リクエストのフローが機能するはずです。 ただし、これまでは、高次コンポーネント自体はブラックボックスのように見えます。 私たちはそれを実現します:
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を使用していますか?
