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

Why do we need middleware for async flow in Redux?

提问人:Stas Bichenko 提问时间:1/3/2016 最后编辑:QwertyStas Bichenko 更新时间:7/21/2023 访问量:218649

问:

根据文档,“没有中间件,Redux 存储仅支持同步数据流”。我不明白为什么会这样。为什么容器组件不能先调用异步 API,然后调用操作?dispatch

例如,想象一个简单的 UI:一个字段和一个按钮。当用户按下按钮时,该字段将填充来自远程服务器的数据。

A field and a button

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

渲染导出的组件后,我可以单击该按钮,输入将正确更新。

记下调用中的函数。它调度一个操作,告诉应用程序它正在更新,然后执行异步调用。调用完成后,提供的值将作为另一个操作的有效负载进行调度。updateconnect

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

编辑:我在 Redux repo 中搜索了线索,发现过去要求 Action Creators 是纯函数。例如,下面是一个用户试图为异步数据流提供更好的解释:

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

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

javascript 异步 reactjs redux-thunk

评论

73赞 Dan Abramov 1/5/2016
动作创造者从来不被要求是纯粹的功能。这是文档中的错误,而不是改变的决定。
3赞 Sebastien Lorber 1/6/2016
然而,@DanAbramov对于可测试性而言,它可能是一个很好的做法。Redux-saga 允许这样做: stackoverflow.com/a/34623840/82609
0赞 Merc 9/16/2023
感谢上帝,感谢你,让这种垃圾技术变得无关紧要。花了足够长的时间。

答:

43赞 acjay 1/4/2016 #1

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

在从事我们刚刚开始工作的新项目时,我的想法非常相似。我是 vanilla Redux 优雅系统的忠实粉丝,该系统以一种远离 React 组件树的方式更新存储和重新渲染组件。在我看来,使用这种优雅的机制来处理异步似乎很奇怪。dispatch

我最终采用了一种与我从项目中分解出来的库中非常相似的方法,我们称之为 react-redux-controller

出于以下几个原因,我最终没有采用您上面的确切方法:

  1. 按照你的编写方式,这些调度函数无权访问商店。你可以通过让你的UI组件传入调度函数所需的所有信息来解决这个问题。但我认为,这不必要地将这些 UI 组件与调度逻辑耦合在一起。更成问题的是,调度函数没有明显的方法来访问异步延续中的更新状态。
  2. 调度函数可以通过词法范围访问自身。一旦该语句失控,这就限制了重构的选项——而且仅使用这种方法看起来非常笨拙。因此,如果你将这些调度程序函数分解为单独的模块,你需要一些系统来组合它们。dispatchconnectupdate

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

  • redux-thunk 以一种功能性的方式做到这一点,将它们传递到你的 thunk 中(根据 dome 的定义,使它们根本不完全是 thunk)。我没有使用过其他中间件方法,但我认为它们基本相同。dispatch
  • react-redux-controller 通过协程来做到这一点。作为奖励,它还允许您访问“选择器”,这些函数是您可能作为第一个参数传递给的函数,而不必直接使用原始的规范化存储。connect
  • 你也可以以面向对象的方式,通过各种可能的机制将它们注入到上下文中。this

更新

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

19赞 Michelle Tilley 1/4/2016 #2

要回答开头提出的问题:

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

请记住,这些文档是针对 Redux 的,而不是针对 Redux 和 React 的。连接到 React 组件的 Redux 存储可以完全按照你说的去做,但是没有中间件的 Plain Jane Redux 存储不接受除普通对象之外的参数。dispatch

如果没有中间件,你当然仍然可以这样做

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

它看起来与原始版本没有什么不同——它只是稍微洗牌了一下——并且不知道它是(或需要)异步的。connectupdateThing

如果你还想支持承诺可观察对象传奇,或者疯狂的自定义高度声明性的动作创建者,那么 Redux 可以通过改变你传递给的内容(也就是你从动作创建者那里返回的内容)来实现。无需对 React 组件(或调用)进行处理。dispatchconnect

评论

0赞 catamphetamine 1/8/2016
您建议在操作完成时再发送一个事件。当您需要在操作完成后显示 alert() 时,这将不起作用。不过,React 组件中的承诺确实有效。我目前推荐 Promise 方法。
905赞 Dan Abramov 1/5/2016 #3

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

这种方法没有错。这在大型应用程序中很不方便,因为您将有不同的组件执行相同的操作,您可能希望对某些操作进行去抖动,或者将某些本地状态(如自动递增 ID )保持在靠近操作创建者的位置等。因此,从维护的角度来看,将动作创建者提取到单独的函数中会更容易。

你可以阅读我对 “How to dispatch a Redux action with a timeout” 的回答,以获得更详细的演练。

像 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 中间件,你可以这样编写它:

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

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

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

对于第一种方法,我们需要注意我们所说的动作创建者是什么样的:

// 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 状态,你可以只使用传递给 thunks 的第二个参数,而不修改调用代码: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 Saga,它允许你定义长时间运行的守护进程(“sagas”),它们在它们出现时执行操作,并在输出操作之前转换或执行请求。这将逻辑从动作创作者转移到传奇中。您可能想检查一下,然后选择最适合您的。

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

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

评论

86赞 Sergey Lapin 1/5/2016
也许可以简单地说 Dan 的想法是:中间件是集中式方法,这种方式允许你保持组件更简单和通用,并在一个地方控制数据流。如果你维护大应用程序,你会喜欢它=)
4赞 Dan Abramov 1/9/2016
@asdfasdfads我不明白为什么它不起作用。它的工作原理完全相同;放在动作之后。alertdispatch()
11赞 Søren Debois 1/25/2016
第一个代码示例中的倒数第二行:。为什么我需要通过调度?如果按照惯例只有一个全局存储,我为什么不直接引用它,并在我需要的时候这样做,例如,在?loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatchstore.dispatchloadData
13赞 Dan Abramov 1/26/2016
@S ørenDebois:如果您的应用程序仅是客户端,那将起作用。如果它在服务器上呈现,则需要为每个请求使用不同的实例,因此无法事先定义它。store
12赞 Guy 7/21/2016
只想指出,这个答案有 139 行,是 redux-thunk 源代码的 9.92 倍,后者由 14 行组成: github.com/gaearon/redux-thunk/blob/master/src/index.js
495赞 Sebastien Lorber 1/6/2016 #4

你没有。

但。。。你应该使用 redux-saga :)

Dan Abramov 的回答是正确的,但我会更多地谈论 redux-saga,它非常相似但更强大。redux-thunk

命令式 VS 声明式

  • DOM:jQuery 是命令式的 / React 是声明式的
  • Monads:IO 是命令式的 / Free 是声明式的
  • Redux 效果:命令式/声明式redux-thunkredux-saga

当你手里拿着一个砰砰声,比如一个 IO monad 或一个 promise,你很难知道它一旦你执行会做什么。测试 thunk 的唯一方法是执行它,并模拟调度程序(如果它与更多东西交互,则嘲笑整个外部世界......

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

从副作用的角度来看,模拟是你的代码不纯的标志,在函数式程序员的眼中,是有问题的证据。与其下载一个库来帮助我们检查冰山是否完好无损,不如绕着它航行。 一个铁杆 TDD/Java 专家曾经问我,你是如何在 Clojure 中模拟的。答案是,我们通常不会。我们通常将其视为需要重构代码的标志。

传奇(正如它们在 中实现的那样)是声明式的,就像 Free monad 或 React 组件一样,它们更容易测试,而无需任何模拟。redux-saga

另请参阅本文

在现代FP中,我们不应该编写程序——我们应该编写程序的描述,然后我们可以随意内省、转换和解释。

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

混淆:操作/事件/命令...

在前端世界中,对于一些后端概念(如 CQRS/EventSourcing 和 Flux/Redux)之间的关系存在很多混淆,主要是因为在 Flux 中,我们使用术语“操作”,它有时可以表示命令式代码 () 和事件 ()。我认为,就像事件溯源一样,你应该只调度事件。LOAD_USERUSER_LOADED

在实践中使用传奇

想象一下,一个带有指向用户配置文件链接的应用。处理每个中间件的惯用方法是:

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

使用允许表示您只对获取上次单击的用户名的数据感兴趣(如果用户在大量用户名上单击非常快,则处理并发问题)。这种东西很难用砰砰声。如果您不想要这种行为,则可以使用。takeLatesttakeEvery

你让动作创作者保持纯粹。请注意,保留 actionCreators(在 sagas 和组件中)仍然很有用,因为它可能有助于您将来添加操作验证(断言/流/打字稿)。putdispatch

由于效果是声明性的,因此代码的可测试性大大提高

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

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

例如,想象一个无限滚动视图。 可以导致,但决定是否应该加载另一个页面真的是可滚动容器的责任吗?然后,他必须注意更复杂的事情,例如最后一页是否已成功加载,或者是否已经有一个页面尝试加载,或者是否没有更多项目可以加载?我不这么认为:为了获得最大的可重用性,可滚动容器应该只描述它已被滚动。页面的加载是该滚动的“业务效果”CONTAINER_SCROLLEDNEXT_PAGE_LOADED

有些人可能会争辩说,生成器本质上可以用局部变量隐藏 redux 存储之外的状态,但如果你开始通过启动计时器等来编排 thunks 内部的复杂事情,你无论如何都会遇到同样的问题。现在有一种效果允许从你的 Redux 商店获取一些状态。select

Sagas 可以跨越时间,还可以支持当前正在开发的复杂流日志记录和开发工具。下面是一些已经实现的简单异步流日志记录:

saga flow logging

解耦

Sagas 不仅取代了 redux thunks。它们来自后端/分布式系统/事件溯源。

一个非常普遍的误解是,传奇只是为了用更好的可测试性来取代你的 redux thunks。实际上,这只是 redux-saga 的一个实现细节。在可测试性方面,使用声明性效果比 thunks 更好,但 saga 模式可以在命令式或声明式代码之上实现。

首先,saga 是一个软件,它允许协调长时间运行的事务(最终一致性)和跨不同边界上下文的事务(领域驱动的设计术语)。

为了简化前端世界,假设有 widget1 和 widget2。当单击 widget1 上的某个按钮时,它应该对 widget2 产生影响。widget1 不是将 2 个 widget 耦合在一起(即 widget1 调度一个针对 widget2 的动作),而是只调度其按钮被点击。然后,saga 侦听此按钮,单击,然后通过删除 widget2 知道的新事件来更新 widget2。

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

一些关于如何构建 Redux 应用程序的好文章,你可以在上面使用 Redux-saga 来解耦:

A concrete usecase: notification system

I want my components to be able to trigger the display of in-app notifications. But I don't want my components to be highly coupled to the notification system that has its own business rules (max 3 notifications displayed at the same time, notification queueing, 4 seconds display-time etc...).

I don't want my JSX components to decide when a notification will show/hide. I just give it the ability to request a notification, and leave the complex rules inside the saga. This kind of stuff is quite hard to implement with thunks or promises.

notifications

I've described here how this can be done with saga

Why is it called a Saga?

The term saga comes from the backend world. I initially introduced Yassine (the author of Redux-saga) to that term in a long discussion.

Initially, that term was introduced with a paper, the saga pattern was supposed to be used to handle eventual consistency in distributed transactions, but its usage has been extended to a broader definition by backend developers so that it now also covers the "process manager" pattern (somehow the original saga pattern is a specialized form of process manager).

Today, the term "saga" is confusing as it can describe 2 different things. As it is used in redux-saga, it does not describe a way to handle distributed transactions but rather a way to coordinate actions in your app. could also have been called . redux-sagaredux-process-manager

See also:

Alternatives

If you don't like the idea of using generators but you are interested by the saga pattern and its decoupling properties, you can also achieve the same with redux-observable which uses the name to describe the exact same pattern, but with RxJS. If you're already familiar with Rx, you'll feel right at home.epic

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 调用是不值得的。
  • 对于大多数简单情况,不要从项目中移除 thunks。
  • 如果有意义,不要犹豫,派遣 thunks 进来。yield put(someActionThunk)

如果你害怕使用 Redux-saga(或 Redux-observable),但只需要解耦模式,请检查 redux-dispatch-subscribe:它允许监听调度并在侦听器中触发新的调度。

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});

评论

83赞 RainerAtSpirit 2/25/2016
每次我重温时,情况都会变得更好。考虑将其变成博客文章:)。
4赞 swelet 5/9/2016
谢谢你的好文章。但是,我在某些方面不同意。LOAD_USER如何势在必行?对我来说,它不仅是声明性的,而且还提供了很好的可读性代码。例如。“当我按下这个按钮时,我想ADD_ITEM”。我可以查看代码并确切地了解发生了什么。如果它被称为“BUTTON_CLICK”,我必须查一下。
4赞 swennemen 5/17/2016
Ni回答。现在还有另一种选择:github.com/blesh/redux-observable
6赞 Sebastien Lorber 9/1/2016
@swelet对不起,安维尔迟到了。当您调度时,这是必要的,因为您调度了一个旨在对您的商店产生影响的操作:您希望该操作执行某些操作。声明式采用事件溯源的理念:您不调度操作来触发应用程序上的更改,而是调度过去的事件来描述应用程序中发生的情况。事件的调度应足以考虑应用程序的状态已更改。事实上,有一个对事件做出反应的 Redux 存储是一个可选的实现细节ADD_ITEM
8赞 Abhinav Manchanda 10/31/2018
我不喜欢这个答案,因为它分散了对实际问题的注意力,以便推销某人自己的图书馆。这个答案提供了两个库的比较,这不是问题的意图。实际的问题是询问是否使用中间件,这可以通过公认的答案来解释。
35赞 XML 3/6/2017 #5

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

在标准 Redux 数据流中执行此操作的最佳位置在哪里?怎么样:

  • 减速机?不可能。它们应该是没有副作用的纯函数。更新商店是一项严肃而复杂的业务。不要污染它。
  • 哑视图组件?绝对不是。他们有一个关注点:演示和用户交互,并且应该尽可能简单。
  • 容器组件?可能,但次优。这是有道理的,因为容器是我们封装一些与视图相关的复杂性并与商店交互的地方,但是:
    • 容器确实需要比哑组件更复杂,但它仍然是一个单一的职责:在视图和状态/存储之间提供绑定。您的异步逻辑与此完全不同。
    • 通过将其放置在容器中,可以将异步逻辑锁定到单个上下文中,并耦合到一个或多个视图/路由。坏主意。理想情况下,它都是可重用的,并且与视图完全分离。
    • (与所有规则一样,如果您具有恰好可在多个上下文中重用的有状态绑定逻辑,或者您可以以某种方式将所有状态泛化为集成 GraphQL 模式之类的内容,则可能会有例外。好吧,好吧,这可能很酷。但。。。大多数时候,绑定似乎最终都是特定于上下文/视图的。
  • 其他服务模块?坏主意:您需要注入对商店的访问权限,这是可维护性/可测试性的噩梦。最好使用 Redux 的颗粒,仅使用提供的 API/模型访问商店。
  • 操作和解释它们的中间件?为什么不呢?!对于初学者来说,这是我们剩下的唯一主要选择。:-)从逻辑上讲,操作系统是解耦的执行逻辑,您可以在任何地方使用。它有权访问商店,并可以调度更多操作。它只有一个职责,即围绕应用程序组织控制和数据流,而大多数异步都适合这一点。
    • 动作创作者呢?为什么不直接在中间件中执行异步操作,而不是在操作本身中执行异步呢?
      • 首先也是最重要的一点,创建者无法像中间件那样访问商店。这意味着您无法调度新的或有操作,无法从存储中读取来编写异步等。
      • 所以,把复杂性放在一个复杂的地方,让其他一切都保持简单。然后,创建者可以是简单、相对纯净、易于测试的功能。

评论

2赞 Estus Flask 9/15/2018
容器组件 - 为什么不呢?由于组件在 React 中扮演的角色,容器可以充当服务类,并且它已经通过 DI(props)获得了存储。通过将其放置在容器中,您将异步逻辑锁定到单个上下文中,用于单个视图/路由 - 这是怎么回事?一个组件可以有多个实例。它可以与演示解耦,例如使用渲染道具。我想答案可以从证明这一点的简短例子中受益更多。
1赞 Dilip Agheda 1/12/2021
这是一个很好的答案
15赞 Alireza 11/18/2017 #6

好了,让我们先来看看中间件是如何工作的,这很好地回答了这个问题,这是 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);
}
  • 请注意,每个中间件都将被赋予 and 函数作为命名参数。dispatchgetState

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

Redux Thunk 中间件允许你编写返回的动作创建者 一个函数,而不是一个动作。thunk 可用于延迟 调度操作,或仅在特定条件 遇到。内部函数接收存储方法调度和 getState 作为参数。

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

那么这到底是什么?这就是它在维基百科中的介绍方式:

在计算机编程中,thunk 是用于注入 将其他计算添加到另一个子例程中。Thunks 主要是 用于延迟计算,直到需要它,或插入 在另一个子例程的开头或结尾处执行的操作。他们有 各种其他应用程序来生成编译代码和 模块化编程。

该术语起源于“思考”的诙谐衍生物。

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;

因此,看看这个概念是多么简单,以及它如何帮助你管理异步操作......

这是你可以没有它的东西,但请记住,在编程中,总是有更好、更整洁和更正确的做事方式......

Apply middleware Redux

评论

1赞 Bhojendra Rauniyar 11/21/2018
第一次在 SO 上,什么都没读。但只是喜欢凝视图片的帖子。惊人,提示和提醒。
6赞 SM Chinna 2/9/2018 #7

使用 Redux-saga 是 React-redux 实现中最好的中间件。

前任: 存储.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()
       ]
    }

然后操作.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'));

试试这个..正在工作

评论

4赞 zameb 8/29/2019
对于只想调用 API 端点以返回实体或实体列表的人来说,这是一件严肃的事情。你建议,“就这样做......然后这个,然后这个,然后这个其他的东西,然后那个,然后这个其他的东西,然后继续,然后做......”但是伙计,这是 FRONTEND,我们只需要调用 BACKEND 就可以为我们提供可以在前端使用的数据。如果这是要走的路,那就出问题了,真的出了问题,现在有人没有应用KISS
1赞 jorisw 4/26/2020
@zameb 你可能是对的,但是你的抱怨是针对 Redux 本身的,以及它在试图降低复杂性时带来的所有无意中听到的。
8赞 Daniel 2/21/2019 #8

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

同步动作创建器是指当我们调用它时,它会立即返回一个 Action 对象,其中包含附加到该对象的所有相关数据,并准备好由我们的 reducer 处理。

异步操作创建器是一种需要一点时间才能准备好最终调度操作的创建者。

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

如果你想在 Redux 应用程序中拥有异步动作创建者,你必须安装一个叫做中间件的东西,它允许你处理这些异步动作创建者。

您可以在错误消息中验证这一点,该消息告诉我们使用自定义中间件执行异步操作。

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

在 redux 中间件(如 redux-thunk)的上下文中,中间件可以帮助我们处理异步动作创建者,因为这是 Redux 无法开箱即用地处理的事情。

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

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

中间件是一个普通的 JavaScript 函数,它将在我们调度的每个操作中被调用。在该函数内部,中间件有机会阻止将操作调度到任何 reducer,它可以修改操作或以任何方式弄乱操作,例如,我们可以创建一个中间件,控制台记录您调度的每个操作,只是为了您的观看乐趣。

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

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

中间件的一个更流行的用途(以及得到你的答案)是处理异步动作创建者,可能是最流行的中间件是 redux-thunk,它是关于帮助你处理异步动作创建者。

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

5赞 Mselmi Ali 8/13/2019 #9

要回答问题:

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

我想说至少有两个原因:

第一个原因是关注点的分离,调用和获取数据不是 的工作,你必须将两个参数传递给你的 , the 和 a .action creatorapiaction creator functionaction typepayload

第二个原因是 正在等待一个具有强制操作类型和可选的普通对象(但在这里你也必须传递有效负载)。redux storepayload

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

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

而工作的结果则由你来适当。Redux-Thunk midlewaredispacheapi callaction

3赞 0xFK 5/16/2020 #10

在企业项目中工作时,中间件中有许多可用的要求,例如(saga)在简单的异步流中不可用,以下是一些:

  • 并行运行请求
  • 无需等待即可提取未来的操作
  • 非阻塞呼叫 争用效果,例如先拾取
  • 启动流程的响应 对任务进行排序(第一次呼叫中的第一个)
  • 构成
  • 任务取消 动态分叉任务。
  • 支持并发运行 redux 中间件之外的 Saga。
  • 使用通道

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

1赞 coder9833idls 9/8/2020 #11

Redux 不能返回函数而不是操作。这只是一个事实。这就是人们使用 Thunk 的原因。阅读以下 14 行代码,了解它如何允许异步周期与一些添加的函数分层一起工作:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

https://github.com/reduxjs/redux-thunk

1赞 Muhammad Abubakar 4/15/2022 #12

我想说至少有两个原因:

第一个原因是关注点的分离,调用 api 并取回数据不是动作创建者的工作,您必须将两个参数传递给您的动作创建者函数,即操作类型和有效负载。

第二个原因是因为 redux 存储正在等待一个具有强制操作类型和可选有效负载的普通对象(但在这里你也必须传递有效负载)。

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

函数 addTodo(文本) { 返回 { 类型: ADD_TODO, 发短信 } } Redux-Thunk 中间件的工作是将 api 调用的结果分解为适当的操作。

0赞 raj 5/4/2023 #13

异步意味着如果一个任务需要时间来执行,那么它将进入单独的内存空间,并且不会阻塞 js 的主引擎,这是 js 引擎唯一的一个调用堆栈。这将有助于在该任务之后执行代码。一旦调用堆栈变为空,任务就会立即执行。 因此,如果你从客户端调度一些东西,那么中间件就会发挥作用。 最流行的中间件是 thunk。 因此,thunk 会查看通过我们系统的每个调度。 Dispatch 只是一种事件,它包含操作名称和您从客户端传递的数据。 因此,如果你想从数据库中请求一些东西,并在存储到 redux 之前使用一些数据,那么在你通过执行 api 调用从数据库获得响应之前,中间件将不会执行。它只会保持执行。 因此,中间件的工作是暂停执行,直到从数据库获得一些响应。