Reduxの非同期フローにミドルウェアが必要なのはなぜですか?


684

ドキュメントによれば、「ミドルウェアがなければ、Reduxストアは同期データフローのみをサポートします」とのことです。なぜそうなのかわかりません。コンテナコンポーネントが非同期APIを呼び出してdispatchからアクションを呼び出せないのはなぜですか?

たとえば、フィールドとボタンという単純なUIを想像してください。ユーザーがボタンを押すと、フィールドにリモートサーバーからのデータが入力されます。

フィールドとボタン

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

エクスポートされたコンポーネントがレンダリングされたら、ボタンをクリックすると、入力が正しく更新されます。

呼び出しのupdate関数に注意してくださいconnect。アプリに更新中であることを通知するアクションをディスパッチし、非同期呼び出しを実行します。呼び出しが終了すると、指定された値が別のアクションのペイロードとしてディスパッチされます。

このアプローチの何が問題になっていますか?ドキュメントが示唆するように、なぜRedux ThunkまたはRedux Promiseを使用したいのですか?

編集:私はReduxリポジトリで手がかりを検索しましたが、Action Creatorは以前は純粋な機能である必要がありました。たとえば、非同期データフローのより良い説明を提供しようとしているユーザーは次のとおりです。

アクション作成者自体はまだ純粋な関数ですが、それが返すサンク関数は必ずしもそうである必要はなく、非同期呼び出しを実行できます

アクションクリエーターはもはや純粋である必要はありません。つまり、過去にはサンク/プロミスミドルウェアが絶対に必要でしたが、これはもはやそうではないようです。


53
アクション作成者は、純粋な関数である必要はありませんでした。これはドキュメントの誤りであり、変更された決定ではありませんでした。
Dan Abramov、2016年

1
@DanAbramovはテスト容易性のために、しかし、それは良い実践的かもしれません。Redux-sagaはこれを許可します:stackoverflow.com/a/34623840/82609
Sebastien Lorber

回答:


699

このアプローチの何が問題になっていますか?ドキュメントが示唆するように、なぜRedux ThunkまたはRedux Promiseを使用したいのですか?

このアプローチには何の問題もありません。同じアクションを実行するさまざまなコンポーネントがあるため、大規模なアプリケーションでは不便です。アクションをデバウンスしたり、IDの自動インクリメントなどのローカル状態をアクション作成者の近くに保持したりすることができます。アクション作成者を個別の機能に抽出するための保守の観点。

詳細なウォークスルーについては、「タイムアウト付きのReduxアクションをディスパッチする方法」に対する私の回答を読んでください。

Redux ThunkやRedux Promiseなどのミドルウェアは、サンクやプロミスをディスパッチするための「シンタックスシュガー」を提供しますが、それ使用する必要はありません。

したがって、ミドルウェアがない場合、アクション作成者は次のようになります。

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

しかし、サンクミドルウェアを使用すると、次のように記述できます。

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

したがって、大きな違いはありません。後者のアプローチについて私が気に入っている点の1つは、コンポーネントがアクションの作成者が非同期であることを気にしないことです。それはdispatch普通に呼び出すだけで、そのmapDispatchToPropsようなアクションクリエーターを短い構文などでバインドするために使用することもできます。コンポーネントはアクションクリエーターがどのように実装されているかを知らず、異なる非同期アプローチ(Redux Thunk、Redux Promise、Redux Saga)を切り替えることができます)コンポーネントを変更せずに。一方、前者の明示的なアプローチでは、コンポーネントは特定の呼び出しが非同期であることを正確dispatchに認識しており、何らかの規則(たとえば、同期パラメーターとして)で渡す必要があります。

また、このコードがどのように変更されるかについても検討してください。2つ目のデータ読み込み関数を用意し、それらを1つのアクションクリエーターに結合するとします。

最初のアプローチでは、どのようなアクションクリエーターを呼び出すかを意識する必要があります。

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

Redux Thunkを使用すると、アクション作成者dispatchは他のアクション作成者の結果を得ることができ、それらが同期であるか非同期であるかを考えることさえありません。

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

このアプローチでは、後でアクションの作成者に現在のRedux状態を調べてもらいたい場合はgetState、呼び出しコードをまったく変更せずに、サンクに渡された2番目の引数を使用できます。

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

同期に変更する必要がある場合は、呼び出しコードを変更せずにこれを行うこともできます。

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

したがって、Redux ThunkやRedux Promiseなどのミドルウェアを使用する利点は、コンポーネントがアクションクリエーターの実装方法、およびコンポーネントがReduxの状態を気にするかどうか、同期か非同期か、および他のアクションクリエーターを呼び出すかどうかを認識しないことです。 。欠点は間接的なものですが、実際のアプリケーションではそれだけの価値があると考えています。

最後に、Redux Thunkとその仲間は、Reduxアプリでの非同期リクエストへの1つの可能なアプローチにすぎません。もう1つの興味深いアプローチはRedux Sagaです。これにより、実行時にアクションを実行し、アクションを出力する前に要求を変換または実行する、長期実行デーモン(「サガ」)を定義できます。これにより、ロジックがアクションクリエーターからサーガに移動します。あなたはそれをチェックアウトし、後であなたに最も合うものを選ぶことを望むかもしれません。

Reduxリポジトリで手掛かりを探したところ、以前はAction Creatorが純粋な機能である必要がありました。

これは誤りです。ドキュメントはこれを言ったが、ドキュメントは間違っていた。
アクション作成者は、純粋な関数である必要はありませんでした。
それを反映するようにドキュメントを修正しました。


57
Danの考えを簡単に言うと、ミドルウェアは一元化されたアプローチです。この方法により、コンポーネントをよりシンプルで一般化し、データフローを1か所で制御できます。大きなアプリを維持している場合は、それをお楽しみいただけます=)
セルゲイラパン2016年

3
@asdfasdfadsなぜ動かないのか分かりません。まったく同じように機能します。アクションのalert後に置きdispatch()ます。
Dan Abramov、2016年

9
最初のコード例の最後から2行目:loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch。なぜ発送に合格する必要があるのですか?慣例により、グローバルストアが1つしかない場合は、それを直接参照するだけで store.dispatch、必要なときにいつでも(たとえば、loadData
セーレンDebois

10
@SørenDeboisアプリがクライアント側のみの場合は機能します。サーバー上でレンダリングされる場合は、store事前に定義できないように、リクエストごとに異なるインスタンスが必要になります。
Dan Abramov、2016年

3
この回答は139行あり、これは14行で構成されるredux-thunkのソースコードの9.92倍であることを指摘したいだけです。github.com
ガイ

447

あなたはしません。

しかし... redux-sagaを使用する必要があります:)

ダン・アブラモフの答えは正しいですredux-thunkが、かなり似ていますがより強力なredux-sagaについてもう少し話をします。

命令型VS宣言型

  • DOM:jQueryは必須/ Reactは宣言型
  • モナド:IOは必須/無料は宣言型
  • Redux効果redux-thunk必須/ redux-saga宣言的

サンクを手に持っている場合、IOモナドやプロミスのように、いったん実行するとどうなるか簡単にわかりません。サンクをテストする唯一の方法は、サンクを実行して、ディスパッチャー(または、他のものとやり取りする場合は外界全体)をモックすることです。

モックを使用している場合は、関数型プログラミングを行っていません。

副作用のレンズを通して見たモックは、コードが不純であることを示すフラグであり、関数型プログラマの目には、何かが間違っていることの証拠です。ライブラリをダウンロードして、氷山が損傷していないことを確認できるようにする代わりに、航海する必要があります。ハードコアTDD / Javaの人から、Clojureでモックを行う方法を尋ねられました。答えは、通常はありません。通常、これはコードをリファクタリングする必要がある兆候と見なされます。

ソース

sagas(で実装されたものredux-saga)は宣言型であり、FreeモナドやReactコンポーネントと同様に、モックなしでテストするのがはるかに簡単です。

この記事も参照してください

最新のFPでは、プログラムを作成するべきではありません。プログラムの説明を作成する必要があります。プログラムの説明は、自由にイントロスペクト、変換、および解釈できます。

(実際、Redux-sagaはハイブリッドのようなものです。フローは必須ですが、効果は宣言的です)

混乱:アクション/イベント/コマンド...

フロントエンドの世界では、CQRS / EventSourcingやFlux / Reduxなどのバックエンドの概念がどのように関連しているのかについて、多くの混乱があります。主に、Fluxでは、命令コード(LOAD_USER)とイベント(USER_LOADED)。イベントソーシングと同様に、イベントをディスパッチするだけでよいと思います。

実際にサガを使用する

ユーザープロファイルへのリンクを持つアプリを想像してみてください。各ミドルウェアでこれを処理する慣用的な方法は次のようになります。

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

このサガは次のように翻訳されます:

ユーザー名がクリックされるたびに、ユーザープロファイルを取得し、読み込まれたプロファイルでイベントをディスパッチします。

ご覧のとおり、にはいくつかの利点がありredux-sagaます。

takeLatest最後にクリックしたユーザー名のデータのみを取得することに関心があることを表現するための許可の使用法(ユーザーが多数のユーザー名を非常に速くクリックした場合の同時実行性の問題を処理します)。この種のものはサンクで難しいです。takeEveryこの動作を望まない場合は、使用することができます。

あなたはアクションクリエーターを純粋に保ちます。将来アクション検証(assertions / flow / typescript)を追加するのに役立つ可能性があるため、(saga putとコンポーネントでdispatch)actionCreatorsを保持することは依然として有用であることに注意してください。

効果が宣言的であるため、コードははるかにテスト可能になります

のようなrpcのような呼び出しをトリガーする必要はもうありませんactions.loadUser()。UIは何が起こったかをディスパッチする必要があるだけです。私達は火のイベントはもう(常に過去形!)ではないアクション。これは、分離された「ダック」またはバインドされたコンテキストを作成できること、およびサガがこれらのモジュール式コンポーネント間のカップリングポイントとして機能できることを意味します。

つまり、発生したことと効果として何が発生するかの間の変換レイヤーを含める必要がなくなるため、ビューの管理がより簡単になります。

たとえば、無限のスクロールビューを想像してください。CONTAINER_SCROLLEDにつながる可能性NEXT_PAGE_LOADEDがありますが、別のページをロードする必要があるかどうかを決定するのは、スクロール可能なコンテナの責任ですか?次に、最後のページが正常にロードされたかどうか、ロードしようとしているページがすでに存在するかどうか、またはロードするアイテムが残っていないかどうかなど、より複雑なことに注意する必要があります。私はそうは思いません:最大の再利用性のために、スクロール可能なコンテナはそれがスクロールされたことを単に記述すべきです。ページの読み込みは、そのスクロールの「ビジネス効果」です

一部の人は、ジェネレーターがローカル変数でreduxストアの外の状態を本質的に隠すことができると主張するかもしれませんが、タイマーなどを開始することによってサンク内の複雑なものを調整し始めると、とにかく同じ問題が発生します。そしてselect、Reduxストアから状態を取得できるようにする効果があります。

Sagasはタイムトラベルが可能で、現在作業中の複雑なフローロギングと開発ツールも使用できます。以下は、すでに実装されているいくつかの単純な非同期フローロギングです。

佐賀流ロギング

デカップリング

Sagasはredux thunkを置き換えるだけではありません。それらはバックエンド/分散システム/イベントソーシングから来ています。

sagasがredux thunkをより優れたテスト容易性に置き換えるためにここにあるのは、よくある誤解です。実際、これはredux-sagaの実装の詳細にすぎません。宣言型効果の使用は、サンクよりもテスト容易性の点で優れていますが、sagaパターンは命令型または宣言型コードの上に実装できます。

そもそも、sagaは、長時間実行されるトランザクション(結果整合性)と、異なる境界コンテキスト(ドメイン駆動設計の専門用語)全体のトランザクションを調整できるソフトウェアです。

フロントエンドの世界でこれを単純化するために、widget1とwidget2があると想像してください。widget1のボタンがクリックされると、widget2に影響を与えるはずです。2つのウィジェットを結合する(つまり、widget1はwidget2をターゲットとするアクションをディスパッチする)代わりに、widget1はそのボタンがクリックされたことのみをディスパッチします。次に、サガはこのボタンのクリックをリッスンし、widget2が認識している新しいイベントをディスパッチしてwidget2を更新します。

これにより、単純なアプリには不要な間接参照のレベルが追加されますが、複雑なアプリケーションのスケーリングがより簡単になります。これで、widget1とwidget2を異なるnpmリポジトリに公開できるため、アクションのグローバルレジストリを共有しなくても、互いを知る必要がなくなります。2つのウィジェットは、個別に存続できる境界コンテキストになりました。それらは互いに整合している必要はなく、他のアプリでも再利用できます。サガは、ビジネスに意味のある方法でウィジェットを調整する2つのウィジェット間の結合点です。

分離の理由でRedux-sagaを使用できるReduxアプリの構成方法に関するすばらしい記事:

具体的なユースケース:通知システム

コンポーネントがアプリ内通知の表示をトリガーできるようにしたいのですが。ただし、独自のビジネスルール(最大3つの通知が同時に表示される、通知キューイング、4秒の表示時間など)を持つ通知システムにコンポーネントを高度に結合したくありません。

JSXコンポーネントで通知の表示/非表示をいつにするか決定したくありません。私はそれに通知を要求する能力を与え、複雑なルールをサガの中に残します。この種のものは、サンクやプロミスで実装するのが非常に困難です。

通知

私はこれがどのようにサガで行われることができるかについてここで説明しました

なぜそれが佐賀と呼ばれるのですか?

サガという用語は、バックエンドの世界から来ています。私は長い議論の中で最初にその用語をYassine(Redux-sagaの作者)に紹介しました。

当初、この用語はで紹介されましたが、サガパターンは分散トランザクションの結果整合性を処理するために使用されるはずでしたが、その使用法はバックエンド開発者によってより広い定義に拡張され、「プロセスマネージャ」もカバーするようになりました。パターン(どういうわけか、元のsagaパターンはプロセスマネージャの特殊な形式です)。

今日、「佐賀」という用語は2つの異なることを説明できるため、混乱しています。これはredux-sagaで使用されるため、分散トランザクションを処理する方法ではなく、アプリ内のアクションを調整する方法について説明しています。redux-sagaと呼ぶこともできたredux-process-manager

以下も参照してください。

代替案

ジェネレーターを使用するアイデアが気に入らないが、サガパターンとそのデカップリングプロパティに興味がある場合は、名前を使用してまったく同じパターンを記述するredux-observableでも同じことを実現できますepicが、RxJSを使用します。Rxに既に慣れている場合は、自宅にいるように感じるでしょう。

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

redux-sagaの役立つリソース

2017アドバイス

  • Redux-sagaを使用するためだけに使いすぎないでください。テスト可能なAPI呼び出しだけでは価値がありません。
  • ほとんどの単純なケースでは、プロジェクトからサンクを削除しないでください。
  • yield put(someActionThunk)それが理にかなっている場合は、サンクを派遣することを躊躇しないでください。

Redux-saga(またはRedux-observable)を使用するのが怖いが、デカップリングパターンだけが必要な場合は、redux-dispatch-subscribeを確認してください。ディスパッチをリッスンし、リスナーで新しいディスパッチをトリガーできます。

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});

64
これは私が再び訪れるたびに良くなっています。ブログ投稿に変換することを検討してください:)。
RainerAtSpirit 2016

4
良い書き込みをありがとう。ただし、特定の側面については同意しません。LOAD_USERはどのように必須ですか?私には、それは宣言的であるだけでなく、読みやすいコードも提供します。例えば 「このボタンを押すと、ADD_ITEMを表示します」。コードを見て、何が起こっているのかを正確に理解できます。代わりに「BUTTON_CLICK」の影響で何かが呼び出された場合は、それを調べる必要があります。
swellet

4
いい答えだ。現在、別の代替手段があります:github.com/blesh/redux-observable
swennemen

4
@swellet遅くなって申し訳ありません。をディスパッチするときADD_ITEMは、ストアに影響を与えることを目的としたアクションをディスパッチするため、これは必須です。アクションが何かを行うことを期待しています。宣言型であることは、イベントソーシングの哲学を受け入れることです。アプリケーションの変更をトリガーするアクションをディスパッチするのではなく、過去のイベントをディスパッチして、アプリケーションで何が起こったかを記述します。イベントのディスパッチは、アプリケーションの状態が変化したことを考慮するのに十分でなければなりません。イベントに反応するReduxストアがあるという事実は、オプションの実装詳細です
Sebastien Lorber

3
誰かが自分のライブラリを売り込むために実際の質問をそらすので、私はこの答えは好きではありません。この回答は、質問の意図ではなかった2つのライブラリの比較を提供します。実際の質問は、ミドルウェアを使用するかどうかを尋ねることです。これは、受け入れられた回答で説明されています。
Abhinav Manchanda 2018年

31

短い答え:非同期問題への完全に合理的なアプローチのようです。いくつかの注意点があります。

私が仕事を始めたばかりの新しいプロジェクトに取り組むとき、私は非常に似たような考えを持っていました。私は、Reactコンポーネントツリーの根本から離れた方法でストアを更新し、コンポーネントを再レンダリングするバニラReduxのエレガントなシステムの大ファンでした。dispatch非同期を処理するためにそのエレガントなメカニズムにフックするのは私には奇妙に思えました。

私は、プロジェクトから除外したライブラリー(react-redux-controllerと呼びます)にあるものと非常によく似たアプローチを採用しました。

いくつかの理由により、私はあなたが上記の正確なアプローチを採用しなくなった:

  1. あなたがそれを書いたように、それらのディスパッチング関数はストアにアクセスできません。ディスパッチ機能が必要とするすべての情報をUIコンポーネントに渡すことで、多少は回避できます。しかし、これにより、これらのUIコンポーネントがディスパッチングロジックに不必要に結合されると主張します。さらに問題なのは、ディスパッチング関数が非同期継続で更新された状態にアクセスする明確な方法がないことです。
  2. ディスパッチ関数はdispatch、字句スコープを介して自身にアクセスします。これは、そのconnectステートメントが手に負えなくなると、リファクタリングのオプションを制限します-そして、その1つのupdate方法だけではかなり扱いにくいように見えます。したがって、これらのディスパッチャー関数を別々のモジュールに分割した場合にそれらを作成できるシステムが必要です。

まとめると、いくつかのシステムを準備dispatchして、イベントのパラメーターとともにストアをディスパッチング関数に注入する必要があります。この依存性注入に対する3つの合理的なアプローチを知っています。

  • redux-thunkはこれを機能的な方法でサンクに渡します(ドームの定義により、サンクがまったくサンクにならないようにします)。私は他のdispatchミドルウェアアプローチを使用していませんが、基本的には同じだと思います。
  • react-redux-controllerはこれをコルーチンで行います。おまけとしてconnect、生の正規化されたストアを直接操作する必要がなく、最初の引数としてに渡した関数である「セレクター」にアクセスできます。
  • またthis、さまざまな可能なメカニズムを通じて、コンテキストにそれらを注入することにより、オブジェクト指向の方法でそれを行うこともできます。

更新

この難問の一部がreact-reduxの制限である私は思います。の最初の引数connectは、状態のスナップショットを取得しますが、ディスパッチしません。2番目の引数はディスパッチを取得しますが、状態は取得しません。どちらの引数も、継続/コールバック時に更新された状態を確認できるようにするため、現在の状態を終了するサンクを取得しません。


22

アブラモフの目標、そして理想的には、複雑さ(および非同期呼び出し)を最も適切な場所カプセル化することです。

標準のReduxデータフローでそれを行うのに最適な場所はどこですか?どうですか:

  • レデューサー?ありえない。副作用のない純粋な関数である必要があります。ストアの更新は、深刻で複雑なビジネスです。汚染しないでください。
  • ダムビューコンポーネント?間違いなくあります。プレゼンテーションとユーザーインタラクションという1つの懸念事項があり、できるだけシンプルにする必要があります。
  • コンテナコンポーネント?可能ですが、最適ではありません。コンテナーは、ビューに関連する複雑さをカプセル化し、ストアと対話する場所ですが、次の点で意味があります。
    • コンテナーは、ダムコンポーネントよりも複雑である必要がありますが、それでも単一の責任があります。それは、ビューと状態/ストアの間のバインディングを提供することです。非同期ロジックはそれとはまったく別の問題です。
    • それをコンテナーに配置することにより、非同期ロジックを単一のビュー/ルートの単一のコンテキストにロックします。悪いアイデア。理想的には、すべて再利用可能で、完全に切り離されています。
  • S 青梅他のサービスモジュールは?悪い考え:ストアへのアクセスを注入する必要があります。これは、保守性/テスト容易性の悪夢です。Reduxの粒度に合わせて、提供されているAPI /モデルのみを使用してストアにアクセスすることをお勧めします。
  • アクションとそれらを解釈するミドルウェア?何故なの?!手始めに、それは私たちが残した唯一の主要なオプションです。:-)より論理的には、アクションシステムは、どこからでも使用できる分離された実行ロジックです。ストアにアクセスでき、さらにアクションをディスパッチできます。それは、アプリケーションの周りの制御とデータの流れを組織化することである単一の責任を持っており、ほとんどの非同期はそれにぴったり合います。
    • アクションクリエーターはどうですか?アクション自体やミドルウェアではなく、そこで単に非同期を実行しないのはなぜですか?
      • まず、最も重要なのは、ミドルウェアのように作成者がストアにアクセスできないことです。つまり、新しい偶発的なアクションをディスパッチしたり、ストアから読み取って非同期を構成したりすることはできません。
      • したがって、必要性が複雑な場所に複雑さを保ち、その他すべてを単純にしてください。作成者は、テストが簡単な、単純で比較的純粋な関数にすることができます。

コンテナコンポーネント -なぜでしょうか?Reactでコンポーネントが果たす役割のため、コンテナーはサービスクラスとして機能し、DI(プロップ)を介して既にストアを取得しています。コンテナーに配置することで、非同期ロジックを単一のビュー/ルートの単一のコンテキストにロックします -どのようにですか?コンポーネントは複数のインスタンスを持つことができます。レンダリングプロップなどを使用して、プレゼンテーションから切り離すことができます。答えは、要点を証明する短い例からさらに恩恵を受けることができると思います。
Estus Flask 2018

私はこの答えが大好きです!
MauricioAvendaño

13

最初に尋ねられる質問に答えるには:

コンテナコンポーネントが非同期APIを呼び出してアクションをディスパッチできないのはなぜですか?

これらのドキュメントはRedux向けであり、ReduxとReactではないことに注意してください。Reactコンポーネントに接続された Reduxストアは、あなたが言うことを正確に実行できますが、ミドルウェアのないPlain Jane Reduxストアは、dispatchプレーンなol 'オブジェクト以外への引数を受け入れません。

ミドルウェアがなくてももちろんできます

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

しかし、それは非同期に包まれている同様のケースの周り Reduxのではなく、扱いによって Reduxの。したがって、ミドルウェアは、に直接渡すことができるものを変更することにより、非同期を可能にしますdispatch


とはいえ、あなたの提案の精神は妥当だと思います。Redux + Reactアプリケーションで非同期を処理できる他の方法は確かにあります。

ミドルウェアを使用する利点の1つは、アクションクリエーターがどのように接続されているかを心配することなく、通常どおりアクションクリエーターを引き続き使用できることです。たとえば、を使用するredux-thunkと、記述したコードは次のようになります。

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

これは、元のものとそれほど異なっていないように見えます—少し混ぜただけです—そして、connectそれupdateThingが非同期である(または非同期である必要がある)ことを知りません。

あなたもサポートしたい場合は、約束観測サガ、または狂気のカスタム高度宣言型アクションクリエイター、そしてReduxのはちょうどあなたがに渡すものを変更することでそれを行うことができますdispatch(別名、あなたはアクションクリエイターから何を返します)。Reactコンポーネント(またはconnect呼び出し)をいじくる必要はありません。


アクションの完了時にさらに別のイベントをディスパッチすることをお勧めします。アクションの完了後にalert()を表示する必要がある場合は機能しません。ただし、Reactコンポーネント内のPromiseは機能します。私は現在、Promisesアプローチを推奨しています。
カタフェタミン

8

OK、のは非常に質問に答えることを、ミドルウェアが最初の作業方法を確認するために始めましょう、これはソースコードであるpplyMiddleWareの Reduxの関数:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = compose.apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

この部分を見て、ディスパッチ関数になる方法を見てください。

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • 各ミドルウェアには、名前付き引数としてdispatchおよびgetState関数が与えられることに注意してください。

OK、これはReduxで最もよく使用されるミドルウェアの1つであるReduxサンクの導入方法です。

Redux Thunkミドルウェアを使用すると、アクションではなく関数を返すアクションクリエーターを作成できます。サンクを使用して、アクションのディスパッチを遅らせたり、特定の条件が満たされた場合にのみディスパッチしたりできます。内部関数は、ストアメソッドのdispatchおよびgetStateをパラメーターとして受け取ります。

ご覧のとおり、アクションではなく関数を返します。つまり、関数なので、いつでも好きなときに呼び出すことができます...

一体何がサンクですか?これがウィキペディアで紹介された方法です。

コンピュータプログラミングでは、サンクは別のサブルーチンに追加の計算を注入するために使用されるサブルーチンです。サンクは、主に計算が必要になるまで計算を遅らせるため、または他のサブルーチンの最初または最後に操作を挿入するために使用されます。これらには、コンパイラー・コード生成およびモジュラー・プログラミングにおけるさまざまな他のアプリケーションがあります。

この用語は、「考える」の陽気な派生語として生まれました。

サンクは、式をラップしてその評価を遅らせる関数です。

//calculation of 1 + 2 is immediate 
//x === 3 
let x = 1 + 2;

//calculation of 1 + 2 is delayed 
//foo can be called later to perform the calculation 
//foo is a thunk! 
let foo = () => 1 + 2;

コンセプトがいかに簡単で、非同期アクションの管理にどのように役立つかを確認してください...

それはあなたがそれなしで生きることができるものですが、プログラミングでは物事を行うための常により良く、きちんとした適切な方法があることを覚えておいてください...

ミドルウェアReduxを適用する


1
SOで初めて、何も読みませんでした。しかし、写真を見つめる投稿が好きだった。驚くべき、ヒント、そしてリマインダー。
Bhojendra Rauniyar

2

Redux-sagaを使用することは、React-redux実装で最高のミドルウェアです。

例:store.js

  import createSagaMiddleware from 'redux-saga';
  import { createStore, applyMiddleware } from 'redux';
  import allReducer from '../reducer/allReducer';
  import rootSaga from '../saga';

  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(
     allReducer,
     applyMiddleware(sagaMiddleware)
   )

   sagaMiddleware.run(rootSaga);

 export default store;

そしてsaga.js

import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { push } from 'react-router-redux';
import data from './data.json';

export function* updateLesson(){
   try{
       yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
       yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js 
   }
   catch(e){
      console.log("error",e)
     }
  }

export function* updateDetail(action) {
  try{
       //To write store update details
   }  
    catch(e){
       console.log("error",e)
    } 
 }

export default function* rootSaga(){
    yield [
        updateLesson()
       ]
    }

そして、action.js

 export default function updateFruit(props,fruit) {
    return (
       {
         type:"UPDATE_DETAIL",
         payload:fruit,
         props:props
       }
     )
  }

そしてreducer.js

import {combineReducers} from 'redux';

const fetchInitialData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
 const updateDetailsData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
const allReducers =combineReducers({
   data:fetchInitialData,
   updateDetailsData
 })
export default allReducers; 

そして、main.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';

const initialState = {};
const store = configureStore(initialState, browserHistory);

ReactDOM.render(
       <Provider store={store}>
          <App />  /*is your Component*/
       </Provider>, 
document.getElementById('app'));

これを試してみてください。


3
これは、エンティティまたはエンティティのリストを返すためにAPIエンドポイントを呼び出したいだけの人にとって深刻なものです。あなたは、「これを実行してください...それから、これ、これ、他のこと、それから、それからこの他のもの、そして続けて、実行してください。」とお勧めします。しかし、これはFRONTENDです。フロントエンドで使用する準備ができているデータを提供するには、BACKENDを呼び出すだけです。これが進むべき道であるならば、何かが間違っている、何かが本当に間違っている、そして誰かが今日KISSを適用していない
zameb

こんにちは。API呼び出しにtryおよびcatchブロックを使用してください。APIが応答を返したら、Reducerアクションタイプを呼び出します。
SM Chinna

1
@zamebあなたは正しいかもしれませんが、あなたの不満はRedux自体にあり、複雑さを減らそうとしているときに耳にしたすべてのことです。
jorisw

1

同期アクションの作成者と非同期アクションの作成者がいます。

同期アクションクリエーターは、それを呼び出すとすぐに、すべての関連データがそのオブジェクトにアタッチされたActionオブジェクトを返し、レデューサーで処理する準備ができています。

非同期アクションの作成者は、最終的にアクションをディスパッチする準備ができるまでに少し時間がかかるものです。

定義により、ネットワークリクエストを行うアクションクリエーターがいる場合は常に、非同期アクションクリエーターとしての資格があります。

Reduxアプリケーション内に非同期アクションクリエーターを置きたい場合は、ミドルウェアと呼ばれるものをインストールして、非同期アクションクリエーターを処理できるようにする必要があります。

これは、非同期アクションにカスタムミドルウェアを使用することを通知するエラーメッセージで確認できます。

では、ミドルウェアとは何ですか。なぜReduxの非同期フローにミドルウェアが必要なのでしょうか。

redux-thunkなどのreduxミドルウェアのコンテキストでは、ミドルウェアは非同期アクションの作成者に対処するのに役立ちます。これは、Reduxがそのままでは処理できないものだからです。

Reduxサイクルに統合されたミドルウェアを使用して、まだアクションクリエーターを呼び出しています。これにより、ディスパッチされるアクションが返されますが、アクションをディスパッチするときに、すべてのレデューサーに直接送信するのではなく、アクションは、アプリケーション内のすべての異なるミドルウェアを介して送信されると言います。

単一のReduxアプリの内部には、必要な数のミドルウェアを含めることができます。ほとんどの場合、私たちが取り組んでいるプロジェクトでは、1つまたは2つのミドルウェアをReduxストアに接続します。

ミドルウェアは、ディスパッチするすべてのアクションで呼び出されるプレーンなJavaScript関数です。その関数の内部では、ミドルウェアは、アクションがレデューサーのいずれかにディスパッチされないようにする機会があります。アクションを変更したり、アクションをいじったりすることができます。たとえば、コンソールにログを記録するミドルウェアを作成できます。あなたが見る喜びのためにあなたが送るすべての行動。

プロジェクトに依存関係としてインストールできる膨大な数のオープンソースミドルウェアがあります。

オープンソースのミドルウェアを利用したり、依存関係としてインストールしたりするだけに限定されません。独自のカスタムミドルウェアを作成して、Reduxストア内で使用できます。

ミドルウェアのより一般的な使用方法の1つは、非同期アクションクリエーターを処理することです。おそらく、最も一般的なミドルウェアはredux-thunkであり、非同期アクションクリエーターの処理を支援することです。

非同期アクションクリエーターの処理にも役立つミドルウェアには、他にも多くの種類があります。


1

質問に答えるには:

コンテナコンポーネントが非同期APIを呼び出してアクションをディスパッチできないのはなぜですか?

私は少なくとも二つの理由で言うでしょう:

第一の理由は、それがの仕事ではないですが、関心事の分離でaction creatorコールするapiと、データを取り戻す、あなたへの2つの引数を渡すために持っている必要がありaction creator functionaction typeそしてpayload

2番目の理由は、redux storeが必須のアクションタイプとオプションでaを持つプレーンオブジェクトを待機しているためですpayload(ただし、ここではペイロードも渡す必要があります)。

アクションの作成者は、以下のようなプレーンオブジェクトである必要があります。

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

そしてあなたの結果Redux-Thunk midlewareへの仕事dispacheapi call適切なaction


0

エンタープライズプロジェクトで作業する場合、単純な非同期フローでは使用できない(サーガ)など、ミドルウェアで使用できる多くの要件があります。以下にいくつかの要件を示します。

  • リクエストを並行して実行する
  • 待つ必要なしに将来の行動を引き出す
  • ノンブロッキングコールレース効果、最初のピックアップ例
  • プロセスを開始するための応答タスクのシーケンス(最初の呼び出しの最初)
  • 作曲
  • タスクのキャンセルタスクを動的に分岐します。
  • Reduxミドルウェアの外部でSagaを実行する並行性をサポートします。
  • チャネルの使用

リストは長いですが、佐賀のドキュメントの詳細セクションを確認してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.