redux-sagaの理解:アクションジェネレーターからサガまで





redux開発者は、アプリケーション開発の最も難しい部分の1つが非同期呼び出し-reduxアクションとリデューサーを複雑にすることなくリクエスト、タイムアウト、その他のコールバックを処理する方法であることを教えてくれます。



この記事では、redux-thunkのような単純なアプローチからredux-sagaのようなより高度なライブラリに至るまで、アプリケーションで非同期を管理するためのいくつかの異なるアプローチについて説明します。



ReactとReduxを使用するので、それらがどのように機能するかについて少なくともある程度の考えがあると仮定します。



アクションクリエーター



APIとの相互作用は、アプリケーションでかなり一般的な要件です。 ボタンをクリックすると、犬のランダムな写真を表示する必要があると想像してください。







Dog CEO APIと、アクションクリエーター内でfetchを呼び出すような非常に単純なものを使用できます。



const {Provider, connect} = ReactRedux; const createStore = Redux.createStore // Reducer const initialState = { url: '', loading: false, error: false, }; const reducer = (state = initialState, action) => { switch (action.type) { case 'REQUESTED_DOG': return { url: '', loading: true, error: false, }; case 'REQUESTED_DOG_SUCCEEDED': return { url: action.url, loading: false, error: false, }; case 'REQUESTED_DOG_FAILED': return { url: '', loading: false, error: true, }; default: return state; } }; // Action Creators const requestDog = () => { return { type: 'REQUESTED_DOG' } }; const requestDogSuccess = (data) => { return { type: 'REQUESTED_DOG_SUCCEEDED', url: data.message } }; const requestDogaError = () => { return { type: 'REQUESTED_DOG_FAILED' } }; const fetchDog = (dispatch) => { dispatch(requestDog()); return fetch('https://dog.ceo/api/breeds/image/random') .then(res => res.json()) .then( data => dispatch(requestDogSuccess(data)), err => dispatch(requestDogError()) ); }; // Component class App extends React.Component { render () { return ( <div> <button onClick={() => fetchDog(this.props.dispatch)}>Show Dog</button> {this.props.loading ? <p>Loading...</p> : this.props.error ? <p>Error, try again</p> : <p><img src={this.props.url}/></p>} </div> ) } } // Store const store = createStore(reducer); const ConnectedApp = connect((state) => { console.log(state); return state; })(App); // Container component ReactDOM.render( <Provider store={store}> <ConnectedApp /> </Provider>, document.getElementById('root') );
      
      





jsfiddle.net/eh3rrera/utwt4dr8



このアプローチには何の問題もありません。 他の条件が同じであれば、常によりシンプルなアプローチを使用することをお勧めします。



ただし、Reduxのみを使用しても十分な柔軟性は得られません。 Reduxカーネルは、同期データストリームのみをサポートする状態コンテナです。



各アクションについて、何が起こったのかを説明するオブジェクトがストアに送信され、レデューサーが呼び出され、状態がすぐに更新されます。



ただし、非同期呼び出しの場合は、最初に応答を待つ必要があり、その後エラーがなければ状態を更新します。 しかし、アプリケーションに何らかの複雑なロジック/ワークフローがある場合はどうでしょうか?



これを行うために、Reduxはミドルウェアを使用します。 中間層は、アクションが送信された後、リデューサーが呼び出される前に実行されるコードです。

中間層は、アクション(アクション)のさまざまな処理の呼び出しのチェーンで接続できますが、出力は単純なオブジェクト(アクション)でなければなりません



非同期操作の場合、Reduxはredux-thunkミドルウェアの使用を推奨します。



Reduxサンク



Reduxサンクは、Reduxで非同期操作を実行する標準的な方法です。

この目的のために、redux-thunkは必要に応じて遅延実行を提供する関数であるサンクの概念を導入します。



redux-thunkドキュメントの例をご覧ください



 let x = 1 + 2;
      
      





値3はすぐに変数xに割り当てられます。



ただし、次のような式がある場合

 let foo = () => 1 + 2;
      
      





その加算はすぐには実行されませんが、foo()関数が呼び出されたときにのみ実行されます。 これにより、foo関数がサンクになります。



Redux-thunkを使用すると、アクション作成者はオブジェクトに加えて関数を送信できるため、アクションジェネレーターをコンバーターに変換できます。



以下では、redux-thunkを使用して前の例を書き換えます



 const {Provider, connect} = ReactRedux; const {createStore, applyMiddleware} = Redux; const thunk = ReduxThunk.default; // Reducer const initialState = { url: '', loading: false, error: false, }; const reducer = (state = initialState, action) => { switch (action.type) { case 'REQUESTED_DOG': return { url: '', loading: true, error: false, }; case 'REQUESTED_DOG_SUCCEEDED': return { url: action.url, loading: false, error: false, }; case 'REQUESTED_DOG_FAILED': return { url: '', loading: false, error: true, }; default: return state; } }; // Action Creators const requestDog = () => { return { type: 'REQUESTED_DOG' } }; const requestDogSuccess = (data) => { return { type: 'REQUESTED_DOG_SUCCEEDED', url: data.message } }; const requestDogError = () => { return { type: 'REQUESTED_DOG_FAILED' } }; const fetchDog = () => { return (dispatch) => { dispatch(requestDog()); fetch('https://dog.ceo/api/breeds/image/random') .then(res => res.json()) .then( data => dispatch(requestDogSuccess(data)), err => dispatch(requestDogError()) ); } }; // Component class App extends React.Component { render () { return ( <div> <button onClick={() => this.props.dispatch(fetchDog())}>Show Dog</button> {this.props.loading ? <p>Loading...</p> : this.props.error ? <p>Error, try again</p> : <p><img src={this.props.url}/></p>} </div> ) } } // Store const store = createStore( reducer, applyMiddleware(thunk) ); const ConnectedApp = connect((state) => { console.log(state); return state; })(App); // Container component ReactDOM.render( <Provider store={store}> <ConnectedApp /> </Provider>, document.getElementById('root') );
      
      





jsfiddle.net/eh3rrera/0s7b54n4



一見、以前のバージョンと大差ありません。



redux-thunkなし







redux-thunkを使用







redux-thunkを使用する利点は、非同期アクションが実行されていることをコンポーネントが認識しないことです。



なぜなら 中間層は、ディスパッチ関数をアクションジェネレーターが返す関数に自動的に渡します。その後、コンポーネントの外部では、同期アクションと非同期アクションの呼び出しに違いはありません(コンポーネントはこれについて心配する必要がなくなります)



したがって、中間層のメカニズムを使用して、暗黙層(間接層)を追加しました。これにより、柔軟性が向上しました。



redux-thunkは、返された関数にパラメーターとしてストアからdispatchおよびgetStateメソッドを渡すため、他のアクションを送信し、状態を使用して追加のロジックとワークフローを実装できます。



しかし、反応コンポーネントを変更せずに、サンクを使用して表現するより複雑なものがある場合はどうでしょう。 この場合、別のミドルウェアライブラリを使用して、より多くの制御を取得することができます。



redux-thunkをライブラリに置き換える方法を見てみましょう。これにより、より多くの制御が可能になります-redux-saga。



Redux-saga



Redux-sagaは、sagaを操作することで副作用をより簡単に改善することを目的としたライブラリです。



Sagasは、分散トランザクションの世界から来た設計パターンであり、サガはトランザクションの方法で実行する必要があるプロセスを管理し、実行状態を維持し、失敗したプロセスを補正します。



サガの詳細については、Sagaパターンの Caitie McCaffrey Applicationを 参照することから始めることができますが、意欲がある場合は、分散システムに関するサガについて最初に説明する記事参照してください



Reduxのコンテキストでは、サガは中間層として実装され(リデューサーは純粋な関数でなければならないため、リデューサーを使用できません)、非同期アクション(副作用)を調整および誘発します。



Redux-sagaはES6ジェネレーターでこれを行います







ジェネレーターは、単一のパスですべての式を実行する代わりに、停止および継続できる関数です。



ジェネレーター関数を呼び出すと、イテレーターオブジェクトが返されます。 そして、next()イテレータメソッドを呼び出すたびに、ジェネレータ関数の本体は次のyield式まで実行され、その後停止します。







これにより、非同期コードの記述と理解が容易になります。

たとえば、次の式の代わりに:







ジェネレーターでは、次のように記述します。







redux-sagaに戻ると、一般的に言えば、ディスパッチされたアクションを監視することが目的のサガがあります。







サガ内で実装するロジックを調整するために、takeEveryヘルパー関数を使用して、操作を実行する新しいサガを作成できます。







複数のリクエストがある場合、takeEveryはworker sagaのいくつかのインスタンスを開始します。 つまり、並行性を実装します。



ウォッチャーの物語は、複雑なロジックを実装するための柔軟性を提供する別の間接層であることに注意する必要があります(ただし、これは単純なアプリケーションでは不要な場合があります)。



これでfetchDogAsync()関数を実装できます(dispatchメソッドへのアクセス権があると仮定します)







しかし、redux-sagaを使用すると、操作自体の結果ではなく、操作を実行する意図を宣言するオブジェクトを取得できます。 つまり、上の例はredux-sagaで次のように実装されています。







(メモ翻訳者:著者は最初のディスパッチ呼び出しを置き換えるのを忘れていました)

非同期メソッドを直接呼び出す代わりに、callメソッドはこの操作を記述するオブジェクトのみを返し、redux-sagaは呼び出しを処理してジェネレーター関数に結果を返します。



同じことがputメソッドにも当てはまります。 ジェネレーター関数内でアクションをディスパッチする代わりに、putはミドルウェアの指示を含むオブジェクトを返し、アクションを送信します。



これらの戻りオブジェクトは、エフェクトと呼ばれます。 以下は、呼び出しメソッドによって返される効果の例です。







エフェクトを使用する場合、redux-sagaはsagasを命令型よりも宣言 型にします。



宣言型プログラミングは、プログラムの実行方法を記述するのではなくプログラムが実行すべきことを記述することにより、副作用を最小限に抑えるか、排除しようとするプログラミングスタイルです。



これがもたらす利点、およびほとんどの人が話すことは、単純なオブジェクトを返す関数は、非同期呼び出しを行う関数よりもテストがはるかに簡単であることです。 テストでは、実際のIPAを使用したり、偽物を作ったり、濡れたりする必要はありません。



テストでは、アサートを行って結果の値を比較することにより、ジェネレーター関数を繰り返します。





別の追加の利点は、さまざまな効果を簡単に組み合わせて複雑なワークフローを作成できることです。



takeEverycallput 、redux-sagaに加えて、 遅延 現在の状態の取得、 並列タスクの開始タスクのキャンセル を行うエフェクト(エフェクトクリエーター)作成するための多くのメソッドを提供します。 ほんのいくつかの可能性。



簡単な例に戻り、以下はredux-sagaの完全な実装です。



 const {Provider, connect} = ReactRedux; const {createStore, applyMiddleware} = Redux; const createSagaMiddleware = ReduxSaga.default; const {takeEvery} = ReduxSaga; const {put, call} = ReduxSaga.effects; // Reducer const initialState = { url: '', loading: false, error: false, }; const reducer = (state = initialState, action) => { switch (action.type) { case 'REQUESTED_DOG': return { url: '', loading: true, error: false, }; case 'REQUESTED_DOG_SUCCEEDED': return { url: action.url, loading: false, error: false, }; case 'REQUESTED_DOG_FAILED': return { url: '', loading: false, error: true, }; default: return state; } }; // Action Creators const requestDog = () => { return { type: 'REQUESTED_DOG' } }; const requestDogSuccess = (data) => { return { type: 'REQUESTED_DOG_SUCCEEDED', url: data.message } }; const requestDogError = () => { return { type: 'REQUESTED_DOG_FAILED' } }; const fetchDog = () => { return { type: 'FETCHED_DOG' } }; // Sagas function* watchFetchDog() { yield takeEvery('FETCHED_DOG', fetchDogAsync); } function* fetchDogAsync() { try { yield put(requestDog()); const data = yield call(() => { return fetch('https://dog.ceo/api/breeds/image/random') .then(res => res.json()) } ); yield put(requestDogSuccess(data)); } catch (error) { yield put(requestDogError()); } } // Component class App extends React.Component { render () { return ( <div> <button onClick={() => this.props.dispatch(fetchDog())}>Show Dog</button> {this.props.loading ? <p>Loading...</p> : this.props.error ? <p>Error, try again</p> : <p><img src={this.props.url}/></p>} </div> ) } } // Store const sagaMiddleware = createSagaMiddleware(); const store = createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(watchFetchDog); const ConnectedApp = connect((state) => { console.log(state); return state; })(App); // Container component ReactDOM.render( <Provider store={store}> <ConnectedApp /> </Provider>, document.getElementById('root') );
      
      





jsfiddle.net/eh3rrera/qu42h5ee



ボタンをクリックすると、次のようになります。



1.アクションFETCHED_DOGが送信されます

2.ウォッチサーガwatchFetchDogはこのアクションを受け取り、ワーカーサーガfetchDogAsyncを呼び出します。

3.ロードインジケータを表示するアクションが送信されます。

4.メソッドAPI呼び出しが行われます。

5.ステータス更新アクションが送信されます(成功または失敗)



いくつかの暗黙的なレイヤーと少し余分な作業が価値があると思う場合、redux-sagaは機能的な方法で副作用を処理するためのより多くの制御を提供できます。



おわりに



この記事では、アクション作成者、サンク、およびサガを使用してReduxに非同期操作を実装する方法を示し、単純なアプローチからより複雑なアプローチに移行しました。



Reduxは、副作用を処理するためのソリューションを規定していません。 どのアプローチに従うかを決定するときは、アプリケーションの複雑さを考慮する必要があります。 私の推奨事項は、簡単なソリューションから始めることです。



試してみる価値のあるredux-sagaの代替もあります。 最も人気のある2つは、 redux-observableRxJSに基づく)とredux-logic (RxJSオブザーバーにも基づくが、 他のスタイルでロジックを自由に記述できる)です。



All Articles