为什么在Redux中我们需要中间件来实现异步流?


682

根据文档,“没有中间件,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 Creators是纯函数。例如,以下用户试图为异步数据流提供更好的解释:

动作创建者本身仍然是一个纯函数,但它返回的thunk函数不必是纯函数,它可以执行我们的异步调用

动作创建者不再需要是纯粹的。因此,过去肯定需要使用thunk / promise中间件,但是似乎不再是这种情况了?


52
从未要求动作创建者是纯粹的功能。这是文档中的错误,而不是更改后的决定。
Dan Abramov'1

1
@DanAbramov出于可测试性,可能是一个很好的实践。Redux-saga允许这样做:stackoverflow.com/a/34623840/82609
Sebastien Lorber

Answers:


695

这种方法有什么问题?如文档所示,我为什么要使用Redux Thunk或Redux Promise?

这种方法没有错。这在大型应用程序中很不方便,因为您将有不同的组件执行相同的操作,您可能希望对某些操作进行反跳操作,或者将某些本地状态(例如,自动递增ID)保持在操作创建者附近,等等。从维护角度将动作创建者提取到单独的功能中。

您可以阅读我对“如何在超时时分派Redux操作”的回答,以获取更详细的演练。

中间件像终极版咚或终极版无极只是给你“语法糖”派遣的thunk或承诺,但你不必须使用它。

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

// 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中间件,您可以这样编写:

// 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通过某种约定来传递(例如,作为sync参数)。

还考虑一下此代码将如何更改。假设我们要具有第二个数据加载功能,并将它们组合在一个动作创建器中。

使用第一种方法时,我们需要注意所调用的动作创建者:

// 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传递给thunk 的第二个参数,而无需完全修改调用代码:

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 Saga,它使您可以定义长时间运行的守护程序(“ sagas”),这些守护程序会在操作到来时进行操作,并在输出操作之前转换或执行请求。这将逻辑从动作创建者转移到了传奇。您可能想要检查一下,然后选择最适合您的东西。

我在Redux存储库中搜索了线索,发现过去需要Action Creators是纯函数。

这是不正确的。文档说了这一点,但是文档是错误的。
从未要求动作创建者是纯粹的功能。
我们修复了文档以反映这一点。


56
也许说Dan的想法的简短方式是:中间件是集中式方法,这种方法可以使您的组件更简单,更通用,并将数据流控制在一处。如果您维护大型应用程序,您将享受它=)
谢尔盖·拉平

3
@asdfasdfads我不明白为什么它不起作用。它将完全相同。行动alert后放dispatch()
Dan Abramov'1

2
为什么我需要传递dispatchgetState?如果redux的前提是只有一个全局存储,那么为什么不使用它,即store.dispatch在需要调度操作时调用?
索伦Debois

9
第一个代码示例中的倒数第二行:loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch。为什么我需要通过调度?如果按照惯例,只有一个全局存储区,为什么我不直接引用它,并store.dispatch在需要时进行引用 ,例如in loadData
索伦Debois

10
@SørenDebois如果您的应用程序仅是客户端,则可以使用。如果它是在服务器上呈现的,则store每个请求都需要有一个不同的实例,因此您无法事先定义它。
Dan Abramov

446

你不知道

但是...您应该使用redux-saga :)

丹·阿布拉莫夫(Dan Abramov)的回答是正确的,redux-thunk但我将再讲一些关于redux-saga的内容,它非常相似,但功能更强大。

命令式VS声明式

  • DOM:jQuery是命令性的/ React是声明性的
  • Monads:IO势在必行/ Free是声明式
  • Redux效果redux-thunk命令式/ redux-saga声明式

当您手头有重击时,例如IO monad或诺言,执行后就不会轻易知道它会做什么。测试一个thunk的唯一方法是执行它,并模拟调度程序(如果它与更多的东西交互,则模拟整个外部世界……)。

如果您使用的是模拟,那么您就不在进行函数式编程。

从副作用的角度来看,模拟标志着您的代码是不纯的,并且在功能程序员的眼中,它证明了某些错误。与其下载库来帮助我们检查冰山是否完好,不如我们绕着它航行。一位TDD / Java顽固的家伙曾经问我如何在Clojure中进行模拟。答案是,我们通常不这样做。我们通常将其视为我们需要重构代码的标志。

资源

sagas(在中实现了redux-saga)是声明性的,就像Free monad或React组件一样,它们无需任何模拟就更容易测试。

另请参阅本文

在现代FP中,我们不应该编写程序,而应该编写程序的描述,然后可以对其进行自省,转换和解释。

(实际上,Redux-saga就像一个混合体:流程是必须的,但效果是声明性的)

混乱:动作/事件/命令...

在前端世界上,如何将某些后端概念(例如CQRS / EventSourcing和Flux / Redux)相关联造成很多混乱,这主要是因为在Flux中,我们使用“动作”一词,有时可以表示命令性代码(LOAD_USER)和事件(USER_LOADED)。我相信,像事件来源一样,您应该只调度事件。

在实践中使用sagas

想象一个带有指向用户个人资料链接的应用程序。每个中间件处理此问题的惯用方式是:

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许可证的使用表示您只对获取最后一次单击的用户名的数据感兴趣(如果用户在许多用户名上单击得非常快,则可以处理并发问题)。这类东西很难用th子来打。takeEvery如果您不希望这种行为,您可能已经使用过。

您可以使动作创作者保持纯正。请注意,保留actionCreators(位于sagas put和components中dispatch)仍然很有用,因为将来可能会帮助您添加动作验证(断言/流程/打字稿)。

由于效果是声明性的,因此您的代码变得更具可测试性

您不再需要触发类似rpc的调用actions.loadUser()。您的UI只需调度已发生的事件。我们仅触发事件(始终以过去时!),不再执行任何操作。这意味着您可以创建解耦的“鸭子”有界上下文,并且传奇可以充当这些模块化组件之间的耦合点。

这意味着您的视图更易于管理,因为它们不再需要在已发生的事情和应该发生的事情之间包含转换层

例如,想象一个无限滚动视图。CONTAINER_SCROLLED可以导致NEXT_PAGE_LOADED,但是可滚动容器的责任是真的决定我们是否应该加载另一页吗?然后,他必须知道更复杂的内容,例如是否成功加载了最后一页,或者是否已有试图加载的页面,或者是否还有其他要加载的项目?我不这么认为:为了获得最大的可重用性,可滚动容器应该只描述它已经被滚动。页面的加载是该滚动的“业务影响”

有人可能会争辩说,生成器可以使用本地变量将状态固有地隐藏在redux存储之外,但是如果您通过启动计时器等开始在thunk中编排复杂的东西,则无论如何都会遇到同样的问题。select现在有一种效果可以从Redux存储中获取一些状态。

Sagas可以进行时间旅行,还可以启用当前正在使用的复杂流日志记录和开发工具。这是一些已经实现的简单异步流日志记录:

传奇流记录

去耦

Sagas不仅在替换redux的thunk。它们来自后端/分布式系统/事件源。

一个普遍的误解是,sagas即将到来,以更好的可测试性代替您的redux thunk。实际上,这只是redux-saga的实现细节。使用声明性效果比使用thunk更好,因为可测试性更好,但是saga模式可以在命令性或声明性代码之上实现。

首先,传奇是一款软件,可以协调长期运行的事务(最终的一致性)以及跨不同有界上下文的事务(域驱动的设计术语)。

为了简化前端世界的操作,假设有widget1和widget2。单击widget1上的某个按钮时,它将对widget2产生影响。与其将2个小部件耦合在一起(即,小部件1调度以小部件2为目标的动作),小部件1仅调度其按钮被单击。然后,传奇侦听此按钮的单击,然后通过调度widget2知道的新事件来更新widget2。

这增加了简单应用程序不必要的间接级别,但使扩展复杂应用程序更加容易。现在,您可以将widget1和widget2发布到不同的npm存储库,这样它们就不必彼此了解,而不必共享全局的动作注册表。现在这2个小部件是可以单独使用的有界上下文。他们不需要彼此保持一致,也可以在其他应用程序中重复使用。传奇是两个小部件之间的耦合点,以对您的业务有意义的方式协调它们。

关于如何构建Redux应用程序的一些不错的文章,出于解耦的原因,您可以在其中使用Redux-saga:

一个具体的用例:通知系统

我希望我的组件能够触发应用内通知的显示。但是我不希望我的组件与具有自己的业务规则的通知系统高度耦合(最多同时显示3条通知,通知队列,4秒显示时间等)。

我不希望我的JSX组件决定何时显示/隐藏通知。我只是给它请求通知的功能,而将复杂的规则留在了传奇中。这种东西很难通过大块头或诺言实现。

通知

我在这里描述了佐贺如何做到这一点

为什么叫佐贺?

saga一词来自后端世界。在最初的长时间讨论中,我最初将Yassine(Redux-saga的作者)介绍给该术语。

最初,该术语是在论文中引入的,该传奇模式本来应该用于处理分布式事务中的最终一致性,但是后端开发人员已将其用法扩展到更广泛的定义,因此现在它也涵盖了“流程管理器”模式(某种程度上,原始的传奇模式是流程管理器的一种特殊形式)。

今天,“传奇”一词令人困惑,因为它可以描述两种不同的事物。由于它在redux-saga中使用,因此它并不描述处理分布式事务的方法,而是协调应用程序中的操作的方法。redux-saga也可以被称为redux-process-manager

也可以看看:

备择方案

如果您不喜欢使用生成器的想法,但是对saga模式及其解耦属性感兴趣,则还可以使用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

4
谢谢你写的好。但是我在某些方面不同意。LOAD_USER势在必行吗?对我来说,这不仅是声明性的-还提供了清晰易读的代码。像。“当我按下此按钮时,我想添加”。我可以看一下代码,确切了解发生了什么。如果它被称为“ BUTTON_CLICK”效果的东西,我将不得不查一下。
swelet,

4
好答案。现在还有另一种选择:github.com/blesh/redux-observable
swennemen

4
@swelet对不起,迟到了。当您调度时ADD_ITEM,这是必须的,因为您要调度一个旨在对商店产生影响的动作:您希望该动作会有所作为。声明式地接受事件源的哲学:您不调度动作来触发应用程序上的更改,而是调度过去的事件来描述应用程序中发生的事情。事件的分派应足以考虑到应用程序的状态已更改。事实上,有一个终极版商店能够反应事件是一个可选的实施细节
塞巴斯蒂安·洛伯

3
我不喜欢这个答案,因为它会分散实际问题,从而推销某人自己的图书馆。这个答案提供了两个库的比较,这不是问题的意图。实际的问题是询问是否完全使用中间件,这由公认的答案来解释。
Abhinav Manchanda '18

31

简短的回答:对我来说,这似乎是一种完全合理的解决异步问题的方法。有几个警告。

在刚开始工作的新项目中,我有非常相似的思路。我是Vanilla Redux优雅的系统的忠实粉丝,该系统用于更新商店和重新渲染组件,而这种方式不影响React组件树。对我来说,加入这种优雅的dispatch机制来处理异步似乎很奇怪。

最后,我采用了一种非常相似的方法,该方法与您从我们的项目中剔除出来的库(我们称为react-redux-controller)中的内容相同

由于以下几个原因,我最终没有采用您所拥有的确切方法:

  1. 按照您编写的方式,这些分派功能无法访问商店。您可以通过让UI组件传递分派功能所需的所有信息来解决该问题。但是我认为这不必要地将这些UI组件耦合到调度逻辑。更成问题的是,没有明显的方法可以使分派函数以异步连续的方式访问更新后的状态。
  2. 调度功能可以dispatch通过词法范围访问自身。一旦该connect语句失控,就限制了重构的选项-仅用一种update方法看起来就很笨拙。因此,如果需要将这些调度程序功能分解为单独的模块,则需要一些系统来组成这些调度程序功能。

总而言之,您必须装配一些系统以允许dispatch将商店以及事件的参数注入到您的调度功能中。我知道这种依赖注入的三种合理方法:

  • redux-thunk通过将它们传递到您的thunk中(通过圆顶定义使其完全不完全是thunk)以一种功能性的方式实现了此目的。我没有使用其他dispatch中间件方法,但是我认为它们基本相同。
  • react-redux-controller用协程执行此操作。另外,它还使您可以访问“选择器”,这些选择器是您可能作为第一个参数传入的函数connect,而不必直接与原始的规范化商店一起使用。
  • 您还可以this通过多种可能的机制将它们注入到上下文中,从而实现面向对象的方式。

更新资料

在我看来,这个难题的一部分是react-redux的局限性。第一个参数connect获取状态快照,但不获取调度。第二个参数获取调度,但不获取状态。由于能够在继续/回调时看到更新的状态,所以两个参数都不会关闭当前状态。


22

Abramov的目标-也是每个人的理想-只是在最合适的地方封装复杂性(和异步调用)

在标准Redux数据流中最好的位置是哪里?怎么样:

  • 减速器?没门。它们应该是纯函数,没有副作用。更新商店是严肃的,复杂的业务。不要污染它。
  • 哑巴视图组件?绝对不能。它们有一个问题:表示和用户交互,并且应尽可能简单。
  • 容器组件?可能,但次优。有意义的是,容器是一个我们封装一些与视图相关的复杂性并与商店交互的地方,但是:
    • 容器确实需要比笨拙的组件更复杂,但这仍然是一个单一的责任:在视图与状态/存储之间提供绑定。您的异步逻辑与此完全无关。
    • 通过将其放置在容器中,您可以将异步逻辑锁定在单个上下文中,用于单个视图/路由。馊主意。理想情况下,它们都是可重用的,并且完全是分离的。
  • š 青梅等服务模块?坏主意:您需要注入对商店的访问权限,这是可维护性/可测试性的噩梦。最好使用Redux,仅使用提供的API /模型访问商店。
  • 动作和解释它们的中间件?为什么不?!对于初学者来说,这是我们剩下的唯一主要选择。:-)从逻辑上讲,动作系统是分离的执行逻辑,可以在任何地方使用。它可以访问商店,并且可以调度更多操作。它的职责是组织应用程序周围的控制流和数据流,并且大多数异步处理都适合于此。
    • 动作创作者呢?为什么不在那里异步,而不是在动作本身和中间件中同步呢?
      • 首先也是最重要的是,创建者无权访问商店,就像中间件一样。这意味着您无法调度新的或有操作,无法从存储中读取以构成异步,等等。
      • 因此,请将复杂性放在必不可少的地方,并将其他所有内容保持简单。然后,创建者可以是易于测试的简单,相对纯净的功能。

容器组件 -为什么不呢?由于组件在React中扮演的角色,容器可以充当服务类,并且它已经通过DI(属性)获得了商店。通过将其放在容器中,您会将异步逻辑锁定在单个上下文中,用于单个视图/路由 -怎么办?一个组件可以有多个实例。可以与呈现分离,例如使用渲染道具。我猜答案可以通过证明这一点的简短示例进一步受益。
Estus Flask

我喜欢这个答案!
MauricioAvendaño

13

要回答开始时提出的问题:

为什么容器组件不能调用异步API,然后分派操作?

请记住,这些文档适用于Redux,而不适用于Redux plus React。连接到React组件的 Redux存储可以完全满足您的要求,但是没有中间件的Plain Jane Redux存储不接受dispatch除普通ol'对象之外的参数。

没有中间件,您当然仍然可以做

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

但这是类似的情况,其中异步被围绕在 Redux上,而不是 Redux 处理。因此,中间件通过修改可以直接传递给的内容来实现异步dispatch


也就是说,我认为您的建议的精神是正确的。当然,您还可以使用其他方法在Redux + React应用程序中处理异步。

使用中间件的好处之一是,您可以继续正常使用动作创建者,而不必担心它们是如何连接的。例如,使用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是异步的(或需要是异步的)。

如果您还想支持promiseobservablessagas疯狂的自定义高度声明性的动作创建者,那么Redux可以通过更改传递给您的东西dispatch(也就是您从动作创建者那里得到的回报)来做到这一点。无需对React组件(或connect调用)进行处理。


您建议仅在动作完成时再调度另一个事件。当您需要在操作完成后显示alert()时,该方法将无效。不过,React组件内部的承诺确实可以工作。我目前建议使用Promises方法。
catamphetamine

8

好的,让我们开始看看中间件是如何工作的,这很好地回答了这个问题,这是Redux中pplyMiddleWare函数的源代码:

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);
}
  • 请注意,每个中间件都将被赋予dispatchgetState功能作为命名参数。

好的,这是Redux-thunk作为Redux最常用的中间件之一介绍的方式:

Redux Thunk中间件使您可以编写返回函数而不是操作的操作创建者。重击程序可用于延迟操作的分发,或者仅在满足特定条件时调度。内部函数接收存储方法dispatch和getState作为参数。

如您所见,它将返回一个函数而不是一个动作,这意味着您可以随时等待并调用它,因为它是一个函数...

那么到底是什么东西?这就是在维基百科中引入的方式:

在计算机编程中,thunk是用于将其他计算注入到另一个子例程中的子例程。Thunk主要用于将计算延迟到需要时,或在其他子例程的开头或末尾插入操作。它们还有各种其他应用程序可用于编译器代码生成和模块化编程。

该术语起源于“思想”的俗称。

thunk是包装表达式以延迟其求值的函数。

//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 and catch块。API提供响应后,请调用Reducer操作类型。
SM Chinna,

1
@zameb您可能是对的,但是您的抱怨是Redux本身的问题,它在尝试降低复杂性时带来的所有窃听。
jorisw

1

有同步动作创建者,然后有异步动作创建者。

同步动作创建者是一个当我们调用它时立即返回一个Action对象,并将所有相关数据附加到该对象上,并准备由我们的reduce处理。

异步动作创建者需要花费一些时间才能准备好分派动作。

根据定义,只要您有一个发出网络请求的动作创建者,它总是有资格作为异步动作创建者。

如果要在Redux应用程序中包含异步操作创建者,则必须安装称为中间件的东西,该中间件将允许您处理这些异步操作创建者。

您可以在错误消息中对此进行验证,该错误消息告诉我们将定制中间件用于异步操作。

那么什么是中间件?为什么在Redux中我们需要它来实现异步流?

在redux中间件(例如redux-thunk)的上下文中,中间件可以帮助我们与异步操作创建者打交道,因为Redux无法开箱即用。

通过将中间件集成到Redux周期中,我们仍在调用动作创建者,这将返回将要分派的动作,但是现在当我们分派动作时,而不是直接将其发送给所有的reducer说一个动作将通过应用程序内部的所有不同中间件发送。

在单个Redux应用程序内部,我们可以拥有任意数量的中间件。在大多数情况下,在我们从事的项目中,我们将在Redux商店中连接一个或两个中间件。

中间件是普通的JavaScript函数,将在我们分派的每个动作中调用该中间件。在该功能的内部,中间件有机会阻止将操作分派到任何化简器,它可以以任何方式修改操作或使操作混乱,例如,我们可以创建一个控制台日志的中间件您派发的每一个动作只是为了您的观看乐趣。

您可以将大量的开源中间件作为依赖项安装到项目中。

您不仅限于仅使用开源中间件或将它们安装为依赖项。您可以编写自己的自定义中间件,并在Redux商店中使用它。

中间件最流行的用途之一(并得到答案)是与异步操作创建者打交道,可能最流行的中间件是redux-thunk,它是帮助您与异步操作创建者打交道。

还有许多其他类型的中间件,它们也可以帮助您处理异步操作创建者。


1

回答问题:

为什么容器组件不能调用异步API,然后分派操作?

我至少要说两个原因:

第一个原因是关注点分离,action creator调用api和获取数据并不是任务,您必须将两个参数传递给action creator functionthe action type和a payload

第二个原因是因为,redux store正在等待具有强制操作类型的纯对象和可选的a payload(但是您也必须传递有效负载)。

动作创建者应该是一个普通的对象,如下所示:

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

和的工作Redux-Thunk midleware,以dispache您的结果api call来适当action


0

在企业项目中工作时,中间件有很多需求,例如简单的异步流程中没有的(传奇),以下是一些需求:

  • 并行运行请求
  • 无需等待即可采取进一步行动
  • 非阻塞呼叫竞赛效果,首先是代答示例
  • 响应以启动流程对您的任务进行排序(第一次呼叫)
  • 构成
  • 任务取消动态分派任务。
  • 在Redux中间件外部支持并发运行Saga。
  • 使用渠道

该列表很长,只需查看saga文档中的高级部分即可。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.