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

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

问题描述

根据文档,没有中间件,终极版店内只支持同步数据流。我不明白为什么是这样的情况。为什么不能在容器组件调用异步API,然后调度的行动?

例如,想象一个简单的用户界面:一个字段和一个按钮。当用户按下按钮,则该字段被填充数据从远程服务器

 进口*为'反应'动作;
进口*作为终极版的终极版;
进口从反应 - 终极版{提供商,连接};常量ActionTypes = {
    STARTED_UPDATING:STARTED_UPDATING',
    更新:'更新'
};类AsyncApi {
    静态getFieldValue(){
        常量承诺=新的承诺((解析)=> {
            的setTimeout(()=> {
                决心(Math.floor(的Math.random()* 100));
            },1000);
        });
        返回的承诺;
    }
}一流的应用程序扩展React.Component {
    渲染(){
        返回(
            < D​​IV>
                <输入值= {} this.props.field />
                <按钮禁用= {} this.props.isWaiting的onClick = {this.props.update}>抓取并LT; /按钮>
                {this.props.isWaiting&放大器;&安培; < D​​IV>等待...< / DIV>}
            < / DIV>
        );
    }
}
App.propTypes = {
    调度:React.PropTypes.func,
    现场:React.PropTypes.any,
    isWaiting:React.PropTypes.bool
};常量减速=(州= {场:无数据,isWaiting:假},动作)=> {
    开关(action.type){
        案例ActionTypes.STARTED_UPDATING:
            返回{...状态,isWaiting:真正};
        案例ActionTypes.UPDATED:
            返回{...状态,isWaiting:假的,场:action.payload};
        默认:
            返回状态;
    }
};
常量存储= Redux.createStore(减速机);
常量ConnectedApp =连接(
    (州)=> {
        返回{...}状态;
    },
    (派遣)=> {
        返回{
            更新:()=> {
                调度({
                    类型:ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    。然后(结果=>讯({
                        类型:ActionTypes.UPDATED,
                        有效载荷:结果
                    }));
            }
        };
    })(应用);
出口默认类扩展React.Component {
    渲染(){
        返回<提供者储存库= {存储}>< ConnectedApp />< /提供者取代;
    }
}

当导出组件呈现出来,我可以单击该按钮,输入正确更新。

请注意在连接通话更新功能。据调度,告诉应用程序正在更新,然后执行一个异步调用的操作。通话结束后,所提供的价值被分派作为另一个动作的有效载荷。

什么是错的这种做法?为什么我会想使用Redux的咚或终极版无极,如文档建议?

编辑:我搜索了终极版回购为线索,发现创作者的行动都必须是在过去的纯函数。例如,这里是试图为异步数据流的一个更好的解释用户:


  

动作创建者本身仍然是一个纯粹的功能,但它返回thunk函数并不需要是,它可以做到异步调用


行动创造者不再需要是纯的。的所以,咚/许中间件的肯定是必需的过去,但似乎这已不再是这样吗?


解决方案

  

什么是错的这种做法?为什么我会想使用Redux的咚或终极版无极,如文档建议?


有什么不对这种方法。这是在一个大的应用程序只是不方便的,因为你有不同的分量执行相同的操作,你可能要抖一些行动,或继续喜欢接近创造者的行动,等自动递增的ID当地一些状态,所以它是从只是更容易的维护角度来提取行动创成独立的功能。

您可以阅读<一href=\"http://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559\">my回答如何调度与超时终极版行动了解更详细的演练。

中间件像终极版咚或终极版无极只是给你语法糖派遣的thunk或承诺,但你不知道的有无以的使用它。

所以,没有任何的中间件,你的行动创造者可能类似于

  //动作的创造者
功能loadData(调度,用户id){//需要调度,所以它是第一个参数
  返回取(`http://data.com/$ {}用户id`)
    。然后(RES =&GT; res.json())
    。然后(
      数据=&GT;讯({类型:'LOAD_DATA_SUCCESS,数据})
      ERR =&GT;讯({类型:'LOAD_DATA_FAILURE',呃})
    );
}// 零件
componentWillMount(){
  loadData(this.props.dispatch,this.props.userId); //不要忘记通过调度
}

但随着咚中间件你可以写这样的:

  //动作的创造者
功能loadData(用户id){
  返回调度=&GT;取(`http://data.com/$ {}用户id`)//终极版咚处理这些
    。然后(RES =&GT; res.json())
    。然后(
      数据=&GT;讯({类型:'LOAD_DATA_SUCCESS,数据})
      ERR =&GT;讯({类型:'LOAD_DATA_FAILURE',呃})
    );
}// 零件
componentWillMount(){
  this.props.dispatch(loadData(this.props.userId)); //调度就像你通常做
}

因此​​,有没有巨大的差异。有一件事我喜欢后一种方式是,组件不关心动作创造者是异步。它只是调用调度通常情况下,它也可以使用 mapDispatchToProps 这样的行动创造者绑定在短语法等。组件不知道行动的创作者是如何实现的,并且可以在不改变不同成分的方法异步(终极版咚,终极版无极,终极版佐贺)之间切换。在另一方面,同前,明确的方式,你的组件知道的究竟的一个特定的呼叫是异步的,需要调度来被传递一些约定(例如,作为同步参数)。

想想也是这个code将如何变化。假设我们希望有第二个数据加载功能,并把它们在一个单一的行动创造者结合起来。

随着我们必须牢记什么样的,我们呼吁采取行动创造者的第一种方式:

  //动作的创造者
功能loadSomeData(调度,用户id){
  返回取(`http://data.com/$ {}用户id`)
    。然后(RES =&GT; res.json())
    。然后(
      数据=&GT;讯({类型:'LOAD_SOME_DATA_SUCCESS,数据})
      ERR =&GT;讯({类型:'LOAD_SOME_DATA_FAILURE',呃})
    );
}
功能loadOtherData(调度,用户id){
  返回取(`http://data.com/$ {}用户id`)
    。然后(RES =&GT; res.json())
    。然后(
      数据=&GT;讯({类型:'LOAD_OTHER_DATA_SUCCESS,数据})
      ERR =&GT;讯({类型:'LOAD_OTHER_DATA_FAILURE',呃})
    );
}
功能loadAllData(调度,用户id){
  返回Promise.all(
    loadSomeData(调度,用户id)//传球调度第一:它是异步
    loadOtherData(调度,用户id)//先通过调度:它是异步
  );
}
// 零件
componentWillMount(){
  loadAllData(this.props.dispatch,this.props.userId); //传球调度第一
}

随着终极版咚行动的创作者可以通过调度其他行动创造者的结果,也别想那些无论是同步或异步的:

  //动作的创造者
功能loadSomeData(用户id){
  返回调度=&GT;取(`http://data.com/$ {}用户id`)
    。然后(RES =&GT; res.json())
    。然后(
      数据=&GT;讯({类型:'LOAD_SOME_DATA_SUCCESS,数据})
      ERR =&GT;讯({类型:'LOAD_SOME_DATA_FAILURE',呃})
    );
}
功能loadOtherData(用户id){
  返回调度=&GT;取(`http://data.com/$ {}用户id`)
    。然后(RES =&GT; res.json())
    。然后(
      数据=&GT;讯({类型:'LOAD_OTHER_DATA_SUCCESS,数据})
      ERR =&GT;讯({类型:'LOAD_OTHER_DATA_FAILURE',呃})
    );
}
功能loadAllData(用户id){
  返回调度=&GT; Promise.all(
    讯(loadSomeData(用户ID))//只是派遣正常!
    讯(loadOtherData(用户ID))//只是派遣正常!
  );
}
// 零件
componentWillMount(){
  this.props.dispatch(loadAllData(this.props.userId)); //只是派遣正常!
}

通过这种方法,如果你以后想你的行动创造者寻找到当前Redux的状态,你可以使用传递给thunk出现第二个的getState 参数,而无需修改主叫code在所有的:

 函数loadSomeData(用户ID){
  //感谢终极版咚我可以使用的getState()在这里不改变主叫
  回报(调度,的getState)=&GT; {
    如果(的getState()。数据[用户id] .isLoaded){
      返回Promise.resolve();
    }    取(`http://data.com/$ {}用户id`)
      。然后(RES =&GT; res.json())
      。然后(
        数据=&GT;讯({类型:'LOAD_SOME_DATA_SUCCESS,数据})
        ERR =&GT;讯({类型:'LOAD_SOME_DATA_FAILURE',呃})
      );
  }
}

如果您需要更改它是同步的,你也可以做到这一点没有改变任何调用code:

  //我可以改变它是一个常规动作的创造者不接触呼叫者
功能loadSomeData(用户id){
  返回{
    类型:'LOAD_SOME_DATA_SUCCESS',
    数据:localStorage.getItem(我的数据)
  }
}

因此​​,使用中间件像终极版咚或终极版承诺的好处是组件不知道如何行动的创作者的落实,以及他们是否关心终极版状态,无论是同步或异步的,以及是否他们叫其他行动创造者。缺点是间接的一点点,但我们认为这是值得的,在实际应用中。

最后,终极版咚和朋友只是一个可能的方法,以Redux的应用异步请求。另一个有趣的方法是终极版传奇,它可以让你定义长期运行的,因为他们来了,是采取行​​动守护程序(传奇), ,改造或进行输出操作之前请求。这会将从创作者的行动逻辑为传奇。您可能要检查出来,后来选择什么最适合自己。


  

我搜索了终极版回购为线索,发现创作者的行动都必须是在过去的纯函数。


这是不正确。该文档说这个,但该文档是错的。结果
行动创造者从来没有要求是纯函数。结果
我们固定的文档,以反映这一点。

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.

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

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