为什么我们需要中间件用于 Redux 中的异步流?

人气:580 发布:2022-10-16 标签: javascript asynchronous reactjs redux redux-thunk

问题描述

根据文档,

import * as React from 'react';从'redux'导入*作为Redux;从'react-redux'导入{提供者,连接};常量 ActionTypes = {STARTED_UPDATING: 'STARTED_UPDATING',更新:'更新'};类 AsyncApi {静态 getFieldValue() {const promise = new Promise((resolve) => {设置超时(() => {解决(Math.floor(Math.random() * 100));}, 1000);});回报承诺;}}类 App 扩展 React.Component {使成为() {返回 (

<输入值={this.props.field}/><button disabled={this.props.isWaiting} onClick={this.props.update}>获取</button>{this.props.isWaiting &&<div>等待中...</div>}</div>);}}App.propTypes = {调度:React.PropTypes.func,字段:React.PropTypes.any,isWaiting:React.PropTypes.bool};const reducer = (state = { field: 'No data', isWaiting: false }, action) =>{开关(动作类型){案例 ActionTypes.STARTED_UPDATING:返回 { ...state, isWaiting: true };案例 ActionTypes. 更新:return { ...state, isWaiting: false, field: action.payload };默认:返回状态;}};const store = Redux.createStore(reducer);常量 ConnectedApp = 连接((状态)=>{返回 { ...状态 };},(调度)=>{返回 {更新:()=>{派遣({类型:ActionTypes.STARTED_UPDATING});AsyncApi.getFieldValue().then(result => dispatch({类型:ActionTypes.UPDATED,有效载荷:结果}));}};})(应用程序);导出默认类扩展 React.Component {使成为() {return <Provider store={store}><ConnectedApp/></Provider>;}}

当导出的组件被渲染时,我可以点击按钮并且输入被正确更新.

注意 connect 调用中的 update 函数.它调度一个告诉应用程序它正在更新的操作,然后执行一个异步调用.调用完成后,提供的值将作为另一个操作的有效负载分派.

这种方法有什么问题?正如文档所建议的那样,我为什么要使用 Redux Thunk 或 Redux Promise?

我在 Redux 仓库中搜索了线索,发现过去要求 Action Creator 是纯函数.例如,这里有一个用户试图为异步数据流提供更好的解释:

action creator 本身仍然是一个纯函数,但它返回的 thunk 函数不需要,它可以做我们的异步调用

Action 创建者不再需要是纯粹的. 所以,thunk/promise 中间件过去肯定是需要的,但现在似乎不再需要了?

解决方案

这种方法有什么问题?正如文档所建议的那样,我为什么要使用 Redux Thunk 或 Redux Promise?

这种方法没有错.这在大型应用程序中很不方便,因为您将有不同的组件执行相同的操作,您可能想要消除一些操作的抖动,或者将一些本地状态(如自动递增 ID)保持在操作创建者附近等.所以从将动作创建者提取到单独的函数中的维护观点.

您可以阅读 我的回答如何调度具有超时的 Redux 操作" 以获得更详细的演练.

像 Redux Thunk 或 Redux Promise 这样的中间件只是为您提供语法糖"来调度 thunk 或 Promise,但您不必必须使用它.

因此,如果没有任何中间件,您的动作创建者可能看起来像

//动作创建者function loadData(dispatch, userId) {//需要 dispatch,所以是第一个参数返回获取(`http://data.com/${userId}`).then(res => res.json()).然后(数据=>调度({类型:'LOAD_DATA_SUCCESS',数据}),错误=>调度({类型:'LOAD_DATA_FAILURE',错误}));}//零件组件WillMount() {loadData(this.props.dispatch, this.props.userId);//不要忘记传递调度}

但是使用 Thunk Middleware 你可以这样写:

//动作创建者函数加载数据(用户 ID){返回调度=>fetch(`http://data.com/${userId}`)//Redux Thunk 处理这些.then(res => res.json()).然后(数据=>调度({类型:'LOAD_DATA_SUCCESS',数据}),错误=>调度({类型:'LOAD_DATA_FAILURE',错误}));}//零件组件WillMount() {this.props.dispatch(loadData(this.props.userId));//像往常一样调度}

所以没有太大的区别.我喜欢后一种方法的一点是,组件不关心动作创建者是异步的.它只是正常调用dispatch,也可以使用mapDispatchToProps来绑定这样的action creator,语法短等等.组件不知道action creator是如何实现的,并且您可以在不同的异步方法(Redux Thunk、Redux Promise、Redux Saga)之间切换,而无需更改组件.另一方面,使用前一种显式方法,您的组件确切知道特定调用是异步的,并且需要按某种约定传递 dispatch(例如,作为同步参数).

还要考虑这段代码将如何变化.假设我们想要第二个数据加载功能,并将它们组合在一个动作创建器中.

对于第一种方法,我们需要注意我们调用的是哪种动作创建者:

//动作创建者函数 loadSomeData(dispatch, userId) {返回获取(`http://data.com/${userId}`).then(res => res.json()).然后(数据=>调度({类型:'LOAD_SOME_DATA_SUCCESS',数据}),错误=>调度({类型:'LOAD_SOME_DATA_FAILURE',错误}));}函数 loadOtherData(dispatch, userId) {返回获取(`http://data.com/${userId}`).then(res => res.json()).然后(数据=>调度({类型:'LOAD_OTHER_DATA_SUCCESS',数据}),错误=>调度({类型:'LOAD_OTHER_DATA_FAILURE',错误}));}函数 loadAllData(dispatch, userId) {返回 Promise.all(loadSomeData(dispatch, userId),//先传递 dispatch:它是异步的loadOtherData(dispatch, userId)//先传递 dispatch:它是异步的);}//零件组件WillMount() {loadAllData(this.props.dispatch, this.props.userId);//先传递调度}

使用 Redux Thunk,动作创建者可以调度其他动作创建者的结果,甚至不用考虑它们是同步的还是异步的:

//动作创建者函数 loadSomeData(userId) {返回调度=>获取(`http://data.com/${userId}`).then(res => res.json()).然后(数据=>调度({类型:'LOAD_SOME_DATA_SUCCESS',数据}),错误=>调度({类型:'LOAD_SOME_DATA_FAILURE',错误}));}函数加载其他数据(用户 ID){返回调度=>获取(`http://data.com/${userId}`).then(res => res.json()).然后(数据=>调度({类型:'LOAD_OTHER_DATA_SUCCESS',数据}),错误=>调度({类型:'LOAD_OTHER_DATA_FAILURE',错误}));}函数加载所有数据(用户 ID){返回调度=>承诺.所有(dispatch(loadSomeData(userId)),//正常调度!dispatch(loadOtherData(userId))//只是正常调度!);}//零件组件WillMount() {this.props.dispatch(loadAllData(this.props.userId));//正常发送!}

使用这种方法,如果您稍后希望您的操作创建者查看当前的 Redux 状态,您可以只使用传递给 thunk 的第二个 getState 参数,而无需修改调用代码:

函数 loadSomeData(userId) {//感谢 Redux Thunk,我可以在这里使用 getState() 而无需更改调用者return (dispatch, getState) =>{if (getState().data[userId].isLoaded) {返回 Promise.resolve();}获取(`http://data.com/${userId}`).then(res => res.json()).然后(数据=>调度({类型:'LOAD_SOME_DATA_SUCCESS',数据}),错误=>调度({类型:'LOAD_SOME_DATA_FAILURE',错误}));}}

如果您需要将其更改为同步,您也可以在不更改任何调用代码的情况下这样做:

//我可以将其更改为常规动作创建者,而无需接触调用者函数 loadSomeData(userId) {返回 {类型:'LOAD_SOME_DATA_SUCCESS',数据:localStorage.getItem('我的数据')}}

所以使用像 Redux Thunk 或 Redux Promise 这样的中间件的好处是组件不知道动作创建者是如何实现的,它们是否关心 Redux 状态,它们是同步的还是异步的,以及它们是否调用其他动作创建者.缺点是有点间接,但我们相信在实际应用中这是值得的.

最后,Redux Thunk 和朋友只是 Redux 应用程序中异步请求的一种可能方法.另一个有趣的方法是 Redux Saga,它允许您定义长时间运行的守护进程(sagas")动作,并在输出动作之前转换或执行请求.这将逻辑从动作创建者转移到 sagas 中.您可能想检查一下,然后再选择最适合您的.

我在 Redux repo 中搜索了线索,发现过去要求 Action Creator 是纯函数.

这是不正确的.文档是这样说的,但文档是错误的.动作创建者从来不需要是纯函数.我们修复了文档以反映这一点.

According to the docs, "Without middleware, Redux store only supports synchronous data flow". I don't understand why this is the case. Why can't the container component call the async API, and then dispatch the actions?

For example, imagine a simple UI: a field and a button. When user pushes the button, the field gets populated with data from a remote server.

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

When the exported component is rendered, I can click the button and the input is updated correctly.

Note the update function in the connect call. It dispatches an action that tells the App that it is updating, and then performs an async call. After the call finishes, the provided value is dispatched as a payload of another action.

What is wrong with this approach? Why would I want to use Redux Thunk or Redux Promise, as the documentation suggests?

EDIT: I searched the Redux repo for clues, and found that Action Creators were required to be pure functions in the past. For example, here's a user trying to provide a better explanation for async data flow:

The action creator itself is still a pure function, but the thunk function it returns doesn't need to be, and it can do our async calls

Action creators are no longer required to be pure. So, thunk/promise middleware was definitely required in the past, but it seems that this is no longer the case?

解决方案

What is wrong with this approach? Why would I want to use Redux Thunk or Redux Promise, as the documentation suggests?

There is nothing wrong with this approach. It’s just inconvenient in a large application because you’ll have different components performing the same actions, you might want to debounce some actions, or keep some local state like auto-incrementing IDs close to action creators, etc. So it is just easier from the maintenance point of view to extract action creators into separate functions.

You can read my answer to "How to dispatch a Redux action with a timeout" for a more detailed walkthrough.

Middleware like Redux Thunk or Redux Promise just gives you "syntax sugar" for dispatching thunks or promises, but you don’t have to use it.

So, without any middleware, your action creator might look like

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

But with Thunk Middleware you can write it like this:

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

So there is no huge difference. One thing I like about the latter approach is that the component doesn’t care that the action creator is async. It just calls dispatch normally, it can also use mapDispatchToProps to bind such action creator with a short syntax, etc. The components don’t know how action creators are implemented, and you can switch between different async approaches (Redux Thunk, Redux Promise, Redux Saga) without changing the components. On the other hand, with the former, explicit approach, your components know exactly that a specific call is async, and needs dispatch to be passed by some convention (for example, as a sync parameter).

Also think about how this code will change. Say we want to have a second data loading function, and to combine them in a single action creator.

With the first approach we need to be mindful of what kind of action creator we are calling:

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

With Redux Thunk action creators can dispatch the result of other action creators and not even think whether those are synchronous or asynchronous:

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

With this approach, if you later want your action creators to look into current Redux state, you can just use the second getState argument passed to the thunks without modifying the calling code at all:

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

If you need to change it to be synchronous, you can also do this without changing any calling code:

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

So the benefit of using middleware like Redux Thunk or Redux Promise is that components aren’t aware of how action creators are implemented, and whether they care about Redux state, whether they are synchronous or asynchronous, and whether or not they call other action creators. The downside is a little bit of indirection, but we believe it’s worth it in real applications.

Finally, Redux Thunk and friends is just one possible approach to asynchronous requests in Redux apps. Another interesting approach is Redux Saga which lets you define long-running daemons ("sagas") that take actions as they come, and transform or perform requests before outputting actions. This moves the logic from action creators into sagas. You might want to check it out, and later pick what suits you the most.

I searched the Redux repo for clues, and found that Action Creators were required to be pure functions in the past.

This is incorrect. The docs said this, but the docs were wrong. Action creators were never required to be pure functions. We fixed the docs to reflect that.

886