web-dev-qa-db-ja.com

ES6ジェネレーターでredux-sagaを使用することES2017でredux-thunkを使用することの長所/短所async/await

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...
417
hampusohlsson

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バックグラウンドタスクをキャンセルします。そしてauthAndRefreshTokenOnExpirycall(authorize, {token})呼び出しの途中でブロックされていたら、それもキャンセルされます。キャンセルは自動的に下方に伝播します。

あなたは 上記の流れの実行可能なデモを見つけることができます

415
Yassine Elouafi

私は、図書館作家のやや徹底的な答えに加えて、制作システムでのsagaの使用経験を追加します。

Pro(sagaを使用)

  • テスト容易性call()が純粋なオブジェクトを返すので、sagasをテストするのはとても簡単です。さんくをテストするには、通常、テストにmockStoreを含める必要があります。

  • redux-sagaには、タスクに関する便利なヘルパー関数がたくさん付属しています。私には、sagaの概念はあなたのアプリのためのある種のバックグラウンドワーカー/スレッドを作成することであるように思えます。

  • Sagasはすべての副作用に対処するための独立した場所を提供しています。私の経験では、サンクアクションよりも変更や管理が簡単です。

Con:

  • ジェネレータの構文.

  • 学ぶべきたくさんの概念。

  • APIの安定性redux-sagaはまだ機能(チャンネルなど)を追加しているようで、コミュニティはそれほど大きくありません。いつかライブラリが後方互換性のない更新をするのであれば心配です。

83
yjcxy12

私の個人的な経験からのコメントをいくつか追加したいと思います(サガとサンクの両方を使用して)。

Sagasはテストに最適です。

  • あなたは効果で包まれた関数をモックする必要はありません
  • したがって、テストはきれいで、読みやすく、そして書くのが簡単です。
  • サガを使うとき、アクションクリエータはほとんどプレーンなオブジェクトリテラルを返します。サンクの約束と違って、テストして主張するのも簡単です。

サガはより強力です。 1サンクのアクションクリエーターでできることはすべて1サガでもできますが、その逆はできません(または少なくとも簡単ではありません)。例えば:

  • アクションがディスパッチされるのを待ちます(take
  • 既存のルーチンを取り消します(canceltakeLatestrace
  • 複数のルーチンが同じアクションをリッスンできます(taketakeEvery、...)。

Sagasは他の便利な機能も提供しています。これは一般的なアプリケーションパターンを一般化したものです。

  • 外部のイベントソース(例えばWebSocket)をリッスンするためのchannels
  • フォークモデル(forkspawn
  • throttle
  • ...

サガは素晴らしいと強力なツールです。しかし力には責任があります。アプリケーションが大きくなると、誰がアクションがディスパッチされるのを待っているのか、または何らかのアクションがディスパッチされているときに何が起こるのかを把握することによって、簡単に迷うことができます。その一方で、さんくの方が簡単で推論が簡単です。どちらを選択するかは、プロジェクトの種類やサイズ、プロジェクトがどのような種類の副作用を処理する必要があるか、チームの好みなどのさまざまな側面によって異なります。いずれにせよあなたのアプリケーションを単純で予測可能なものにしてください。

22
madox2

より簡単な方法は、 redux-auto を使用することです。

文書から

redux-autoは、promiseを返す「アクション」関数を作成できるようにすることで、この非同期の問題を修正しました。 「デフォルト」機能アクションロジックに付随する。

  1. 他のRedux非同期ミドルウェアは必要ありません。例えばサンク、約束ミドルウェア、サガ
  2. 約束をreduxに簡単に渡し、それを管理してもらうことができます
  3. 外部サービスコールを変換先と同じ場所に配置できます
  4. ファイルに「init.js」という名前を付けると、アプリの起動時に1回呼び出されます。これは、起動時にサーバーからデータをロードするのに適しています

アイデアは、各 特定のファイル内のアクション を持つことです。 「保留」、「実現」、「拒否」のレデューサー関数を使用して、ファイル内のサーバー呼び出しを同じ場所に配置します。これにより、約束の処理が非常に簡単になります。

また、状態のプロトタイプに helperオブジェクト(「非同期」と呼ばれる) を自動的に付加し、UI、要求された遷移を追跡できるようにします。

0
codemeasandwich

サンクス対サガス

Redux-ThunkRedux-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'のアクションがストアにディスパッチされるまで、サガを一時停止します。それが発生すると、実行が続行されます。

詳細については、この記事を参照

0
Mselmi Ali

いくつかの個人的な経験

  1. コーディングスタイルと読みやすさのために、過去にredux-sagaを使用することの最も重要な利点の1つは、redux-thunkでコールバック地獄を回避することです - もう多くのネストを使用する必要はありません。しかし、今ではasync/await thunkの人気が高まっているので、redux-thunkを使うときには非同期コードを同期スタイルで書くこともできます。これはredux-thinkの改善と見なすことができます。

  2. 特にTypeScriptで、redux-sagaを使うときはもっと定型的なコードを書く必要があるかもしれません。たとえば、フェッチ非同期関数を実装したい場合は、1つの単一FETCHアクションを使用して、action.js内の1サンク単位でデータとエラー処理を直接実行できます。しかしredux-sagaでは、FETCH_START、FETCH_SUCCESS、およびFETCH_FAILUREアクション、およびそれらに関連するすべての型チェックを定義する必要があります。これは、redux-sagaの機能の1つが、エフェクトの作成と指示にこの種の豊富な「トークン」メカニズムを使用することです。簡単なテストのためのreduxストアもちろん、これらのアクションを使わずにサガを書くこともできますが、それはさんくに似ています。

  3. ファイル構造の点では、redux-sagaは多くの場合より明示的なようです。すべてのsagas.tsで非同期関連のコードを簡単に見つけることができますが、redux-thunkでは、実際にそれを見る必要があります。

  4. 簡単なテストはredux-sagaのもう1つの機能です。これは本当に便利です。しかし、明確にする必要があることの1つは、redux-sagaの "call"テストはテストで実際のAPI呼び出しを実行しないことです。したがって、API呼び出しの後にそれを使用するステップのサンプル結果を指定する必要があります。したがってredux-sagaで書く前に、sagaとそれに対応するsagas.spec.tsを詳細に計画することをお勧めします。

  5. Redux-sagaはタスクの並列実行、takeLatest/takeEvery、fork/spawnのような並行処理ヘルパーなどの多くの高度な機能も提供します。これらはサンクよりはるかに強力です。

結論として、個人的には、私が言いたいのは、通常の多くの場合や中小規模のアプリでは、非同期/待機スタイルのredux-thunkを使用することです。それはあなたに多くの定型コード/アクション/ typedefを節約するでしょう、そしてあなたは多くの異なるsagas.tsを取り替えそして特定のsagasツリーを維持する必要はないでしょう。しかし、非常に複雑な非同期ロジックと並行性/並列パターンのような機能を必要とする大規模なアプリケーションを開発している場合、またはテストとメンテナンスの需要が高い場合は(特にテスト駆動開発で) 。

とにかく、redux-sagaはredux自体よりも難しく複雑ではありません。また、コア概念とAPIが十分に制限されているため、いわゆる急な学習曲線はありません。ちょっとした時間を費やしてredux-sagaを学ぶことは、将来いつか自分自身に利益をもたらすかもしれません。

0
Jonathan

ちょっとしたメモ。ジェネレータはキャンセル可能で、非同期/待機 - できません。したがって、質問の例では、何を選ぶべきかについては実際には意味がありません。しかし、より複雑なフローでは、ジェネレータを使用するよりも優れた解決策がない場合があります。

それで、もう一つの考えはredux-thunkで発電機を使うことであるかもしれません、しかし私にとって、それは正方形の車輪で自転車を発明しようとしているようです。

そしてもちろん、ジェネレータはテストが簡単です。

0
Dmitriy

これはredux-sagaredux-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)
    })
  }
}
0
Diego Haz

私の経験の中でいくつかの異なる大規模なReact/ReduxプロジェクトをレビューしてきたSagasは開発者にテストするのがはるかに簡単で、間違いを起こしにくいコードを書くためのもっと構造化された方法を提供します。

はい、最初は少し厄介ですが、ほとんどの開発者は1日で十分に理解することができます。私はいつもyieldが何をするのか心配しないようにと人々に言います。

私はサンクがMVC pattenのコントローラーであるかのように扱われていたいくつかのプロジェクトを見ましたが、これはすぐに厄介な混乱になります。

私のアドバイスは、Aが単一のイベントに関連するBタイプのものをトリガーする必要があるところでSagasを使用することです。いくつかのアクションにまたがる可能性があるものについては、顧客のミドルウェアを作成し、それをトリガーするためにFSAアクションのメタプロパティを使用する方が簡単です。

0
David Bradshaw