1. ホーム
  2. javascript

[解決済み] Reduxの非同期フローになぜミドルウェアが必要なのか?

2022-03-19 08:53:35

質問

ドキュメントによると ミドルウェアを使用しない場合、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 を呼び出します。これは、Appに更新中であることを伝えるアクションをディスパッチし、非同期呼び出しを実行します。呼び出しが終了すると、提供された値は、別のアクションのペイロードとしてディスパッチされます。

この方法のどこが問題なのでしょうか?ドキュメントにあるように、なぜRedux ThunkやRedux Promiseを使いたがるのでしょうか?

EDITです。 Reduxのレポを検索して手がかりを探したところ、Action Creatorは過去に純粋な関数であることが要求されていたことがわかりました。例えば 非同期データフローについて、より良い説明を提供しようとしているユーザーです。

<ブロッククオート

アクション・クリエーター自体はまだ純粋な関数ですが、それが返すサンク関数はその必要がなく、私たちの非同期呼び出しを行うことができます。

アクションクリエイターは純粋である必要がなくなりました。 つまり、thunk/promiseミドルウェアは、以前は間違いなく必要でしたが、現在はそうではなくなっているようですね?

どうすればいい?

<ブロッククオート

この方法のどこが問題なのでしょうか?ドキュメントにあるように、なぜRedux ThunkやRedux Promiseを使いたがるのでしょうか?

この方法は何も間違っていません。ただ、大規模なアプリケーションでは不便です。なぜなら、異なるコンポーネントが同じアクションを実行したり、いくつかのアクションをデバウンスしたり、自動インクリメントのIDなどのローカルステートをアクションクリエイターの近くに保持したりしたい場合があるからです。そのため、アクションクリエイターを別の関数に抽出する方がメンテナンスの観点からは簡単なのです。

を読むことができます。 Reduxのアクションをタイムアウトでディスパッチする方法」に対する私の回答です。 を使うと、より詳細なチュートリアルができます。

Redux ThunkやRedux Promiseのようなミドルウェアは、Thunkや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
}

しかし、Thunk Middlewareを使えば、このように書くことができます。

// 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
}

つまり、大きな違いはないのです。後者のアプローチで気に入っていることのひとつは、コンポーネントがアクションの作成者が非同期であることを気にしないことです。それはただ 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の状態を後で調べさせたい場合、2番目の getState の引数は、呼び出し側のコードをまったく修正せずにサンクに渡されます。

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アプリにおける非同期リクエストの可能なアプローチのひとつに過ぎません。もうひとつの興味深いアプローチは リダックス佐賀 このデーモンでは、アクションが来るとそれを受け取り、アクションを出力する前にリクエストを変換または実行する、長時間稼働するデーモン (「サーガ」) を定義できます。これはロジックをアクションクリエーターからサーガに移行させるものです。一度チェックしてみて、自分に合ったものを選ぶといいかもしれません。

<ブロッククオート

Reduxのレポで手がかりを探したところ、Action Creatorは過去に純粋な関数であることが要求されていたことがわかりました。

これは不正確です。docsにはこう書いてありましたが、docsは間違っていました。
アクションクリエイターは、純粋な関数であることを要求されたことはありません。
それを反映させるためにドキュメントを修正しました。