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


488

現在、reduxタウンの最新の子供であるredux-saga / redux-sagaについての話はたくさんあります。アクションをリッスン/ディスパッチするためにジェネレーター関数を使用します。

redux-sagaredux-thunk抱える前に、async / awaitで使用している以下のアプローチの代わりに、使用の賛否両論を知りたいと思います。

コンポーネントは次のようになります。通常どおりアクションをディスパッチします。

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

6
ここでredux-thunkとredux-sagaを比較する私の答えも参照してください:stackoverflow.com/a/34623840/82609
Sebastien Lorber

22
あなたの::前に何をしますthis.onClickか?
ダウンヒル

37
@ZhenyangHuaこれは、関数をオブジェクト(this)にバインドするための略称this.onClick = this.onClick.bind(this)です。省略形はすべてのレンダーで再バインドされるため、通常はコンストラクタで長い形式を使用することをお勧めします。
hampusohlsson 2016

7
そうですか。ありがとう!関数bind()に渡すのthisに多くの人が使用しているのを見かけますが、今は使い始めて() => method()います。
ダウンヒルスキー

2
@Hosar私はしばらくの間、本番環境でreduxとredux-sagaを使用しましたが、オーバーヘッドが少ないため、実際には数か月後にMobXに移行しました
hampusohlsson

回答:


461

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 });
  }
}

最初に気づくのは、フォームを使用してAPI関数を呼び出していることですyield call(func, ...args)callエフェクトを実行せず、のようなプレーンオブジェクトを作成するだけ{type: 'CALL', func, args}です。実行はredux-sagaミドルウェアに委任されます。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呼び出しの結果をモックしていることに注意してください。データのモックは、関数のモックよりもはるかに簡単です。

次に気づくのは、への呼び出しyield take(ACTION)です。サンクは、新しいアクション(例:)が発生するたびにアクション作成者によって呼び出されLOGIN_REQUESTます。つまり、アクションは継続的にサンクにプッシュされ、サンクはそれらのアクションの処理をいつ停止するかを制御できません。

redux-sagaでは、ジェネレーターが次のアクションを引き出します。つまり、彼らはいつアクションを聞くか、いつ行わないかを制御できます。上記の例では、フロー命令はwhile(true)ループ内に配置されているため、サンクプッシュ動作をいくらか模倣する各着信アクションをリッスンします。

プルアプローチでは、複雑な制御フローを実装できます。たとえば、次の要件を追加するとします。

  • ログアウトユーザーアクションの処理

  • 最初のログインが成功すると、サーバーは、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) )
    }
  }
}

上記の例では、を使用して同時実行性の要件を表現していますracetake(LOGOUT)レースに勝った場合(つまり、ユーザーがログアウトボタンをクリックした場合)。レースはauthAndRefreshTokenOnExpiryバックグラウンドタスクを自動的にキャンセルします。通話authAndRefreshTokenOnExpiry中にブロックされた場合call(authorize, {token})もキャンセルされます。キャンセルは自動的に下向きに伝播します。

上記のフローの実行可能なデモを見つけることができます


@yassine delay関数はどこから来ていますか?:ああ、それは見github.com/yelouafi/redux-saga/blob/...
philk

122
redux-thunkコードは非常に読みやすいと自己説明です。しかしredux-sagas:1は、主にこれらの動詞のような機能のうち、本当に読めないcallforktakeput...
SYG

11
@ syg、call、fork、take、putは、意味的に友好的である可能性があることに同意します。ただし、すべての副作用をテスト可能にするのは、動詞のような関数です。
ダウンヒルスキー

3
@sygは、これらの奇妙な動詞関数を含む関数は、ディーププロミスチェーンを含む関数よりも読みやすい
Yasser Sinjab

3
これらの「奇妙な」動詞は、佐賀とreduxから出てくるメッセージとの関係を概念化するのにも役立ちます。あなたはできる取る次の反復をトリガするために、多くの場合、あなたがすることができます- Reduxの外のメッセージタイプを置くあなたの副作用の結果を放送するために後ろに新しいメッセージを。
2018年

104

ライブラリの作者のかなり完全な答えに加えて、プロダクションシステムでの佐賀の使用経験を追加します。

プロ(佐賀を使用):

  • テスト容易性。call()が純粋なオブジェクトを返すため、sagasをテストするのは非常に簡単です。サンクのテストでは、通常、テスト内にmockStoreを含める必要があります。

  • redux-sagaには、タスクに関する便利なヘルパー関数が多数用意されています。サガのコンセプトは、アプリのバックグラウンドワーカー/スレッドのようなものを作成することです。reactreduxアーキテクチャで欠落している部分として機能します(actionCreatorsとreducerは純粋な関数である必要があります)。これが次のポイントにつながります。

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

短所:

  • ジェネレータ構文。

  • 学ぶべき多くの概念。

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


9
少しコメントしたいのですが、アクションクリエーターは純粋な機能である必要はありません。これは、ダン自身が何度も主張しています。
Marson Mao

14
現在、使用法とコミュニティが拡大しているため、redux-sagaが非常に推奨されています。また、APIはより成熟しています。API stability現在の状況を反映する更新として、Conを削除することを検討してください。
Denialos 2017

1
サガはサンクより開始しており、その最後のコミットすぎサンクの後にある
amorenew

2
はい、FWIW redux-sagaには12k個のスターがあり、redux-thunkには8k個あります
Brian Burns

3
サガの別の課題を追加します。サガは、デフォルトでアクションおよびアクション作成者から完全に切り離されています。サンクはアクションクリエーターと副作用を直接結び付けますが、サガはアクションクリエーターを、それらをリッスンするサガから完全に切り離します。これには技術的な利点がありますが、コードの追跡がはるかに困難になる可能性があり、単方向の概念の一部が不明瞭になる可能性があります。
theaceofthespade

33

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

Sagasはテストに最適です。

  • エフェクトでラップされた関数をモックする必要はありません
  • したがって、テストはクリーンで読みやすく、簡単に記述できます
  • sagasを使用する場合、アクション作成者は通常、プレーンオブジェクトリテラルを返します。サンクの約束とは異なり、テストとアサートも簡単です。

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

  • 1つまたは複数のアクションがディスパッチされるのを待ちます(take
  • 既存のルーチンをキャンセル(canceltakeLatestrace
  • 複数のルーチンが同じアクションを聴くことができます(taketakeEvery、...)

Sagasは、いくつかの一般的なアプリケーションパターンを一般化する他の便利な機能も提供します。

  • channels 外部イベントソース(WebSocketなど)をリッスンする
  • フォークモデル(forkspawn
  • スロットル
  • ...

Sagasはすばらしい強力なツールです。しかし、力には責任が伴います。アプリケーションが大きくなると、アクションがディスパッチされるのを待っている人や、アクションがディスパッチされているときに何が起こるかを把握することで、簡単に迷子になる可能性があります。一方、サンクのほうがシンプルであり、推論も容易です。どちらを選択するかは、プロジェクトのタイプやサイズ、プロジェクトが処理しなければならない副作用のタイプや開発チームの好みなど、多くの側面に依存します。いずれにせよ、アプリケーションをシンプルで予測可能な状態に保つだけです。


8

いくつかの個人的な経験:

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

  2. 特にTypescriptでredux-sagaを使用する場合は、さらに多くのボイラープレートコードを記述する必要があります。たとえば、fetch async関数を実装する場合、データとエラーの処理は、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の「呼び出し」テストではテストで実際のAPI呼び出しが実行されないため、API呼び出しの後にそれを使用する可能性のあるステップのサンプル結果を指定する必要があることです。したがって、redux-sagaを作成する前に、sagaとそれに対応するsagas.spec.tsを詳細に計画することをお勧めします。

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

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

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


5

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

はい、最初は少し奇妙ですが、ほとんどの開発者は1日で十分に理解できます。私はいつも何yieldから始めればいいのか心配しないように、そしてあなたが2、3のテストを書いたらそれがあなたに来るだろうといつも言っています。

サンクがMVCパッテンのコントローラーであるかのように扱われているいくつかのプロジェクトを見たことがありますが、これはすぐに保守不能な混乱になります。

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


2

サンク対サガ

Redux-Thunk そして Redux-Saga、いくつかの重要な点で異なる、両方のReduxのためのミドルウェアライブラリ(Reduxのミドルウェアが傍受アクションがディスパッチ()メソッドを介して、店舗に入ってくることをコードである)です。

アクションは文字通り何でもかまいませんが、ベストプラクティスに従っている場合、アクションはタイプフィールドとオプションのペイロード、メタ、エラーフィールドを持つプレーンな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
        };

つまり、a thunkは(オプションで)パラメータを取り、別の関数を返す関数です。内部関数はa dispatch functionと関数を取り、getState両方ともRedux-Thunkミドルウェアによって提供されます。

Redux-Saga

Redux-Sagaミドルウェアを使用すると、複雑なアプリケーションロジックをsagasと呼ばれる純粋な関数として表現できます。純粋な関数は予測可能で繰り返し可能なため、テストの観点からは比較的簡単です。

Sagasは、ジェネレータ関数と呼ばれる特別な関数によって実装されます。これらはの新機能ですES6 JavaScript。基本的に、yieldステートメントが表示されているすべての場所で、実行はジェネレーターに飛び入ります。yieldステートメントは、ジェネレーターを一時停止させ、生成された値を返すものと考えてください。後で、呼び出し元は、次のステートメントでジェネレータを再開できます。yield

ジェネレータ関数はこのように定義されたものです。functionキーワードの後のアスタリスクに注意してください。

function* mySaga() {
    // ...
}

一度ログインサガはに登録されていますRedux-Saga。しかしyield、最初の行のテイクは、タイプのアクション'LOGIN_REQUEST'がストアにディスパッチされるまで、サガを一時停止します。それが発生すると、実行が続行されます。

詳細については、この記事を参照してください


1

1つの簡単なメモ。ジェネレータはキャンセル可能で、非同期/待機です—できません。したがって、質問の例としては、何を選択するかは実際には意味がありません。しかし、より複雑なフローでは、ジェネレーターを使用するよりも優れたソリューションがない場合があります。

したがって、別のアイデアは、redux-thunkでジェネレーターを使用することですが、私にとっては、四角いホイールを備えた自転車を発明しようとしているようです。

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


0

ここに両方の​​最高の部分(長所)を組み合わせたプロジェクトが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)
    })
  }
}

1
then()Reactコンポーネント内での使用はパラダイムに反します。componentDidUpdatepromiseが解決されるのを待つのではなく、変更された状態を処理する必要があります。

3
@ Maxincredible52サーバーサイドレンダリングには当てはまりません。
Diego Haz 2017年

私の経験では、Maxのポイントはサーバー側のレンダリングにも当てはまります。これはおそらくルーティング層のどこかで処理されるべきです。
ThinkingInBits

3
@ Maxincredible52パラダイムに反するのはなぜですか、どこで読んだことがありますか?私は通常@Diego Hazに似ていますが、componentDidMountで行います(Reactのドキュメントに従って、ネットワークコールはそこで行う必要があります)componentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421

0

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

文書から

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

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

アイデアは、特定のファイルにアクションを含めることです。「保留中」、「完了」、「拒否」のリデューサー関数を使用して、サーバー呼び出しをファイル内に配置します。これにより、約束の処理が非常に簡単になります。

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


2
別のソリューションは、あまりにも考慮しなければならないので、私は1もそれの無関係な答えをした
amorenew

12
彼はプロジェクトの作者であることを彼が明らかにしなかったので、そこにあると思います
jreptak
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.