redux-saga/redux-saga 今、レックスの町で最も最近の子供についての話がたくさんあります。それはアクションを監視する/ディスパッチするためにジェネレータ関数を使います。
私が頭を包む前に、redux-saga
をasync/awaitで使っている以下のアプローチの代わりにredux-thunk
を使うことの賛否両論を知りたいです。
コンポーネントはこのようになり、ディスパッチアクションは通常のようになります。
import { login } from 'redux/auth';
class LoginForm extends Component {
onClick(e) {
e.preventDefault();
const { user, pass } = this.refs;
this.props.dispatch(login(user.value, pass.value));
}
render() {
return (<div>
<input type="text" ref="user" />
<input type="password" ref="pass" />
<button onClick={::this.onClick}>Sign In</button>
</div>);
}
}
export default connect((state) => ({}))(LoginForm);
それから私の行動はこのようになります:
// auth.js
import request from 'axios';
import { loadUserData } from './user';
// define constants
// define initial state
// export default reducer
export const login = (user, pass) => async (dispatch) => {
try {
dispatch({ type: LOGIN_REQUEST });
let { data } = await request.post('/login', { user, pass });
await dispatch(loadUserData(data.uid));
dispatch({ type: LOGIN_SUCCESS, data });
} catch(error) {
dispatch({ type: LOGIN_ERROR, error });
}
}
// more actions...
// user.js
import request from 'axios';
// define constants
// define initial state
// export default reducer
export const loadUserData = (uid) => async (dispatch) => {
try {
dispatch({ type: USERDATA_REQUEST });
let { data } = await request.get(`/users/${uid}`);
dispatch({ type: USERDATA_SUCCESS, data });
} catch(error) {
dispatch({ type: USERDATA_ERROR, error });
}
}
// more actions...
Redux-sagaでは、上記の例と等価になります。
export function* loginSaga() {
while(true) {
const { user, pass } = yield take(LOGIN_REQUEST)
try {
let { data } = yield call(request.post, '/login', { user, pass });
yield fork(loadUserData, data.uid);
yield put({ type: LOGIN_SUCCESS, data });
} catch(error) {
yield put({ type: LOGIN_ERROR, error });
}
}
}
export function* loadUserData(uid) {
try {
yield put({ type: USERDATA_REQUEST });
let { data } = yield call(request.get, `/users/${uid}`);
yield put({ type: USERDATA_SUCCESS, data });
} catch(error) {
yield put({ type: USERDATA_ERROR, error });
}
}
最初に注意することは、yield call(func, ...args)
という形式を使用してapi関数を呼び出していることです。 call
は効果を実行するのではなく、単に{type: 'CALL', func, args}
のようなプレーンなオブジェクトを作成します。実行は関数を実行し、その結果でジェネレータを再開することを引き受けるredux-sagaミドルウェアに委任されます。
主な利点は、単純な等価検査を使用してReduxの外部でジェネレータをテストできることです。
const iterator = loginSaga()
assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))
// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
iterator.next(mockAction).value,
call(request.post, '/login', mockAction)
)
// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
iterator.throw(mockError).value,
put({ type: LOGIN_ERROR, error: mockError })
)
我々は単純化されたデータをイテレータのnext
メソッドに注入することによってAPI呼び出し結果を偽造していることに注意してください。データのモックは関数のモックよりもずっと簡単です。
2番目に注意することはyield take(ACTION)
の呼び出しです。さんくはそれぞれの新しいアクション(例えばLOGIN_REQUEST
)でアクション作成者によって呼ばれます。つまり、アクションはサンクに対して継続的にpushされ、サンクはいつそれらのアクションの処理を停止するかについて制御できません。
Redux-sagaでは、ジェネレータpull次のアクション。すなわち、彼らはいつ行動を聴くべきで、いつ聴くべきでないかを制御します。上記の例では、フロー命令はwhile(true)
ループの中に置かれているので、入ってくる各アクションを待ち受けます。
プルアプローチでは、複雑な制御フローを実装できます。たとえば、次の要件を追加したいとします。
LOGOUTユーザー操作の処理
最初のログイン成功時に、サーバーはexpires_in
フィールドに格納されているある程度の期限内に期限切れになるトークンを返します。 expires_in
ミリ秒ごとにバックグラウンドで承認を更新する必要があります。
API呼び出しの結果(初期ログインまたは更新)を待つときに、ユーザーはその間にログアウトする可能性があることを考慮に入れてください。
どうやってそれをサンクで実装しますか。また、フロー全体に対して完全なテストカバレッジを提供しますか?これがSagasの外観です。
function* authorize(credentials) {
const token = yield call(api.authorize, credentials)
yield put( login.success(token) )
return token
}
function* authAndRefreshTokenOnExpiry(name, password) {
let token = yield call(authorize, {name, password})
while(true) {
yield call(delay, token.expires_in)
token = yield call(authorize, {token})
}
}
function* watchAuth() {
while(true) {
try {
const {name, password} = yield take(LOGIN_REQUEST)
yield race([
take(LOGOUT),
call(authAndRefreshTokenOnExpiry, name, password)
])
// user logged out, next while iteration will wait for the
// next LOGIN_REQUEST action
} catch(error) {
yield put( login.error(error) )
}
}
}
上記の例では、race
を使用して並行性の要件を表現しています。 take(LOGOUT)
がレースに勝利した場合(すなわち、ユーザーがログアウトボタンをクリックした場合)。レースは自動的にauthAndRefreshTokenOnExpiry
バックグラウンドタスクをキャンセルします。そしてauthAndRefreshTokenOnExpiry
がcall(authorize, {token})
呼び出しの途中でブロックされていたら、それもキャンセルされます。キャンセルは自動的に下方に伝播します。
私は、図書館作家のやや徹底的な答えに加えて、制作システムでのsagaの使用経験を追加します。
Pro(sagaを使用)
テスト容易性call()が純粋なオブジェクトを返すので、sagasをテストするのはとても簡単です。さんくをテストするには、通常、テストにmockStoreを含める必要があります。
redux-sagaには、タスクに関する便利なヘルパー関数がたくさん付属しています。私には、sagaの概念はあなたのアプリのためのある種のバックグラウンドワーカー/スレッドを作成することであるように思えます。
Sagasはすべての副作用に対処するための独立した場所を提供しています。私の経験では、サンクアクションよりも変更や管理が簡単です。
Con:
ジェネレータの構文.
学ぶべきたくさんの概念。
APIの安定性redux-sagaはまだ機能(チャンネルなど)を追加しているようで、コミュニティはそれほど大きくありません。いつかライブラリが後方互換性のない更新をするのであれば心配です。
私の個人的な経験からのコメントをいくつか追加したいと思います(サガとサンクの両方を使用して)。
Sagasはテストに最適です。
サガはより強力です。 1サンクのアクションクリエーターでできることはすべて1サガでもできますが、その逆はできません(または少なくとも簡単ではありません)。例えば:
take
)cancel
、takeLatest
、race
)take
、takeEvery
、...)。Sagasは他の便利な機能も提供しています。これは一般的なアプリケーションパターンを一般化したものです。
channels
fork
、spawn
)サガは素晴らしいと強力なツールです。しかし力には責任があります。アプリケーションが大きくなると、誰がアクションがディスパッチされるのを待っているのか、または何らかのアクションがディスパッチされているときに何が起こるのかを把握することによって、簡単に迷うことができます。その一方で、さんくの方が簡単で推論が簡単です。どちらを選択するかは、プロジェクトの種類やサイズ、プロジェクトがどのような種類の副作用を処理する必要があるか、チームの好みなどのさまざまな側面によって異なります。いずれにせよあなたのアプリケーションを単純で予測可能なものにしてください。
より簡単な方法は、 redux-auto を使用することです。
文書から
redux-autoは、promiseを返す「アクション」関数を作成できるようにすることで、この非同期の問題を修正しました。 「デフォルト」機能アクションロジックに付随する。
アイデアは、各 特定のファイル内のアクション を持つことです。 「保留」、「実現」、「拒否」のレデューサー関数を使用して、ファイル内のサーバー呼び出しを同じ場所に配置します。これにより、約束の処理が非常に簡単になります。
また、状態のプロトタイプに helperオブジェクト(「非同期」と呼ばれる) を自動的に付加し、UI、要求された遷移を追跡できるようにします。
サンクス対サガス
Redux-Thunk
とRedux-Saga
はいくつかの重要な点で異なります。どちらもReduxのミドルウェアライブラリです(Reduxミドルウェアは、dispatch()メソッドを介してストアに着信するアクションをインターセプトするコードです)。
アクションは文字通り何でもかまいませんが、ベストプラクティスに従っている場合、アクションはタイプフィールドとオプションのペイロード、メタ、エラーフィールドを持つプレーンなJavaScriptオブジェクトです。例えば.
const loginRequest = {
type: 'LOGIN_REQUEST',
payload: {
name: 'admin',
password: '123',
}, };
Redux-Thunk
標準アクションのディスパッチに加えて、Redux-Thunk
ミドルウェアを使用すると、thunks
と呼ばれる特別な機能をディスパッチできます。
(Reduxの)サンクは一般に次の構造を持ちます。
export const thunkName =
parameters =>
(dispatch, getState) => {
// Your application logic goes here
};
つまり、thunk
は(オプションで)いくつかのパラメーターを取り、別の関数を返す関数です。内部関数はdispatch function
関数とgetState
関数を取ります。どちらもRedux-Thunk
ミドルウェアによって提供されます。
Redux-Saga
Redux-Saga
ミドルウェアを使用すると、複雑なアプリケーションロジックをsagasと呼ばれる純粋な関数として表現できます。純粋な関数は、予測可能で再現性があり、テストが比較的容易になるため、テストの観点から望ましいです。
Sagasは、ジェネレーター関数と呼ばれる特別な関数によって実装されます。これらはES6 JavaScript
の新機能です。基本的に、yieldステートメントが表示されるすべての場所で実行がジェネレーターにジャンプインおよびアウトアウトします。 yield
ステートメントは、ジェネレーターを一時停止し、生成された値を返すと考えてください。後で、呼び出し元はyield
に続くステートメントでジェネレーターを再開できます。
ジェネレーター関数は、このように定義された関数です。 functionキーワードの後のアスタリスクに注意してください。
function* mySaga() {
// ...
}
ログインサガがRedux-Saga
に登録されたら。ただし、最初の行のyield
テイクは、タイプ'LOGIN_REQUEST'
のアクションがストアにディスパッチされるまで、サガを一時停止します。それが発生すると、実行が続行されます。
いくつかの個人的な経験
コーディングスタイルと読みやすさのために、過去にredux-sagaを使用することの最も重要な利点の1つは、redux-thunkでコールバック地獄を回避することです - もう多くのネストを使用する必要はありません。しかし、今ではasync/await thunkの人気が高まっているので、redux-thunkを使うときには非同期コードを同期スタイルで書くこともできます。これはredux-thinkの改善と見なすことができます。
特にTypeScriptで、redux-sagaを使うときはもっと定型的なコードを書く必要があるかもしれません。たとえば、フェッチ非同期関数を実装したい場合は、1つの単一FETCHアクションを使用して、action.js内の1サンク単位でデータとエラー処理を直接実行できます。しかしredux-sagaでは、FETCH_START、FETCH_SUCCESS、およびFETCH_FAILUREアクション、およびそれらに関連するすべての型チェックを定義する必要があります。これは、redux-sagaの機能の1つが、エフェクトの作成と指示にこの種の豊富な「トークン」メカニズムを使用することです。簡単なテストのためのreduxストアもちろん、これらのアクションを使わずにサガを書くこともできますが、それはさんくに似ています。
ファイル構造の点では、redux-sagaは多くの場合より明示的なようです。すべてのsagas.tsで非同期関連のコードを簡単に見つけることができますが、redux-thunkでは、実際にそれを見る必要があります。
簡単なテストはredux-sagaのもう1つの機能です。これは本当に便利です。しかし、明確にする必要があることの1つは、redux-sagaの "call"テストはテストで実際のAPI呼び出しを実行しないことです。したがって、API呼び出しの後にそれを使用するステップのサンプル結果を指定する必要があります。したがってredux-sagaで書く前に、sagaとそれに対応するsagas.spec.tsを詳細に計画することをお勧めします。
Redux-sagaはタスクの並列実行、takeLatest/takeEvery、fork/spawnのような並行処理ヘルパーなどの多くの高度な機能も提供します。これらはサンクよりはるかに強力です。
結論として、個人的には、私が言いたいのは、通常の多くの場合や中小規模のアプリでは、非同期/待機スタイルのredux-thunkを使用することです。それはあなたに多くの定型コード/アクション/ typedefを節約するでしょう、そしてあなたは多くの異なるsagas.tsを取り替えそして特定のsagasツリーを維持する必要はないでしょう。しかし、非常に複雑な非同期ロジックと並行性/並列パターンのような機能を必要とする大規模なアプリケーションを開発している場合、またはテストとメンテナンスの需要が高い場合は(特にテスト駆動開発で) 。
とにかく、redux-sagaはredux自体よりも難しく複雑ではありません。また、コア概念とAPIが十分に制限されているため、いわゆる急な学習曲線はありません。ちょっとした時間を費やしてredux-sagaを学ぶことは、将来いつか自分自身に利益をもたらすかもしれません。
ちょっとしたメモ。ジェネレータはキャンセル可能で、非同期/待機 - できません。したがって、質問の例では、何を選ぶべきかについては実際には意味がありません。しかし、より複雑なフローでは、ジェネレータを使用するよりも優れた解決策がない場合があります。
それで、もう一つの考えはredux-thunkで発電機を使うことであるかもしれません、しかし私にとって、それは正方形の車輪で自転車を発明しようとしているようです。
そしてもちろん、ジェネレータはテストが簡単です。
これはredux-saga
とredux-thunk
の両方の良い部分(長所)を組み合わせたプロジェクトです:dispatching
に対応するアクションで約束をしながら、あなたはサガのすべての副作用を処理することができます: https://github.com/ diegohaz/redux-saga-thunk
class MyComponent extends React.Component {
componentWillMount() {
// `doSomething` dispatches an action which is handled by some saga
this.props.doSomething().then((detail) => {
console.log('Yaay!', detail)
}).catch((error) => {
console.log('Oops!', error)
})
}
}
私の経験の中でいくつかの異なる大規模なReact/ReduxプロジェクトをレビューしてきたSagasは開発者にテストするのがはるかに簡単で、間違いを起こしにくいコードを書くためのもっと構造化された方法を提供します。
はい、最初は少し厄介ですが、ほとんどの開発者は1日で十分に理解することができます。私はいつもyield
が何をするのか心配しないようにと人々に言います。
私はサンクがMVC pattenのコントローラーであるかのように扱われていたいくつかのプロジェクトを見ましたが、これはすぐに厄介な混乱になります。
私のアドバイスは、Aが単一のイベントに関連するBタイプのものをトリガーする必要があるところでSagasを使用することです。いくつかのアクションにまたがる可能性があるものについては、顧客のミドルウェアを作成し、それをトリガーするためにFSAアクションのメタプロパティを使用する方が簡単です。