为什么我们在 Redux 中需要异步流中间件? [英] Why do we need middleware for async flow in Redux?

查看:39
本文介绍了为什么我们在 Redux 中需要异步流中间件?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

根据文档,

import * as React from 'react';import * as Redux from 'redux';从'react-redux'导入{提供者,连接};const ActionTypes = {STARTED_UPDATING: 'STARTED_UPDATING',更新:'更新'};类异步 API {静态 getFieldValue() {const promise = new Promise((resolve) => {setTimeout(() => {解决(Math.floor(Math.random()* 100));}, 1000);});回报承诺;}}类 App 扩展了 React.Component {使成为() {返回 (<div><输入值={this.props.field}/><button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>{this.props.isWaiting &&<div>正在等待...</div>}

);}}App.propTypes = {调度:React.PropTypes.func,字段:React.PropTypes.any,正在等待:React.PropTypes.bool};const reducer = (state = { field: 'No data', isWaiting: false }, action) =>{开关(动作.类型){案例 ActionTypes.STARTED_UPDATING:return { ...state, isWaiting: true };案例 ActionTypes.UPDATED:return { ...state, isWaiting: false, field: action.payload };默认:返回状态;}};const store = Redux.createStore(reducer);const 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 Creators 是纯函数.例如,这里有一个用户试图为异步数据流提供更好的解释:><块引用>

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

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

解决方案

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

这种方法没有任何问题.这只是在大型应用程序中不方便,因为您将有不同的组件执行相同的操作,您可能希望对某些操作进行去抖动,或者保持一些本地状态,例如靠近操作创建者的自动递增 ID 等.所以它更容易将动作创建者提取到单独的函数中的维护观点.

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

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

所以,如果没有任何中间件,你的动作创建者可能看起来像

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

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

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

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

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

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

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

有了 Redux Thunk,动作创建者可以dispatch其他动作创建者的结果,甚至不用考虑这些是同步的还是异步的:

//动作创建者函数 loadSomeData(userId) {返回调度 =>获取(`http://data.com/${userId}`).then(res => res.json()).然后(数据 =>dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),错误 =>dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err }));}函数 loadOtherData(userId) {返回调度 =>获取(`http://data.com/${userId}`).then(res => res.json()).然后(数据 =>dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', 数据 }),错误 =>dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err }));}函数 loadAllData(userId) {返回调度 =>承诺.all(dispatch(loadSomeData(userId)),//正常调度即可!dispatch(loadOtherData(userId))//正常调度即可!);}//成分componentWillMount() {this.props.dispatch(loadAllData(this.props.userId));//正常发送即可!}

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

function loadSomeData(userId) {//感谢 Redux Thunk 我可以在这里使用 getState() 而不改变调用者返回(调度,getState)=>{如果 (getState().data[userId].isLoaded) {返回 Promise.resolve();}获取(`http://data.com/${userId}`).then(res => res.json()).然后(数据 =>dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),错误 =>dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err }));}}

如果需要改成同步,也可以不改任何调用代码:

//我可以在不接触调用者的情况下将其更改为常规动作创建器函数 loadSomeData(userId) {返回 {类型:'LOAD_SOME_DATA_SUCCESS',数据:localStorage.getItem('my-data')}}

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

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

<块引用>

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

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

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.

这篇关于为什么我们在 Redux 中需要异步流中间件?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆