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

IT技术 javascript asynchronous reactjs redux redux-thunk
2021-01-17 19:09:37

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

例如,想象一个简单的 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 repo 中搜索了一些线索,发现过去要求 Action Creators 是纯函数。例如,这里有一个用户试图为异步数据流提供更好的解释:

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

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

6个回答

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

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

您可以阅读我对“如何使用超时调度 Redux 操作”的回答,以获得更详细的演练。

中间件像终极版咚或终极版无极只是给你“语法糖”派遣的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 Middleware 你可以这样写:

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

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

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

// 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 这样的中间件的好处是,组件不知道 action creators 是如何实现的,也不知道它们是否关心 Redux state,它们是同步的还是异步的,以及它们是否调用了其他的 action creators . 缺点是有点间接,但我们相信在实际应用中它是值得的。

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

找了Redux repo找线索,发现过去要求Action Creators是纯函数。

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

@asdfasdfads 我不明白为什么它不起作用。它的工作原理完全相同;alert经过dispatch()荷兰国际集团的行动。
2021-03-10 19:09:37
@SørenDebois 如果您的应用程序是客户端,只有这样才能工作。如果它在服务器上呈现,您将希望store为每个请求使用不同的实例,因此您无法事先定义它。
2021-03-12 19:09:37
只是想指出,这个答案有 139 行,比 redux-thunk 的源代码 14 行多 9.92 倍:github.com/gaearon/redux-thunk/blob/master/src/index.js
2021-03-20 19:09:37
第一个代码示例中的倒数第二行:loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. 为什么我需要传入 dispatch ?如果按照惯例只有一个全局存储,为什么我不直接引用它并 store.dispatch在需要时执行,例如 in loadData
2021-04-03 19:09:37
简而言之,Dan 的想法是:中间件是集中式方法,这种方式允许您使组件更简单和通用,并在一个地方控制数据流。如果您维护大型应用程序,您会喜欢它=)
2021-04-09 19:09:37

你没有。

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

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

命令式 VS 声明式

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

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

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

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

来源

传奇(因为它们在 中实现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.

使用takeLatestpermit 表示你只对最后点击的用户名感兴趣(处理并发问题,以防用户在很多用户名上点击非常快)。这种东西很难用 thunk。takeEvery如果您不想要这种行为,您可以使用

你让动作创作者保持纯洁。请注意,保留 actionCreators(在 sagasput和 components 中dispatch仍然很有用,因为它可能会帮助您在未来添加动作验证(断言/流程/typescript)。

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

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

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

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

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

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

传奇流记录

解耦

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

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

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

为了简化前端世界,假设有小部件 1 和小部件 2。当widget1 上的某个按钮被点击时,它应该会对widget2 产生影响。不是将 2 个小部件耦合在一起(即,小部件 1 调度一个针对小部件 2 的动作),小部件 1 仅调度它的按钮被点击。然后 saga 监听这个按钮的点击,然后通过分发一个 widget2 知道的新事件来更新 widget2。

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

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

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

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

我不希望我的 JSX 组件决定何时显示/隐藏通知。我只是赋予它请求通知的能力,并将复杂的规则留在传奇中。这种东西很难用 thunk 或 promise 来实现。

通知

我已经在这里描述如何用 saga 来做到这一点

为什么叫佐贺呢?

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

最初,该术语是在一篇论文中引入的,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 调用是不值得的。
  • 对于大多数简单的情况,不要从项目中删除 thunk。
  • yield put(someActionThunk)如果有意义,请毫不犹豫地发送 thunk

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

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});
不错的回答。现在还有另一种选择:github.com/blesh/redux-observable
2021-03-13 19:09:37
每次我重新访问时,这都会变得更好。考虑把它变成一篇博文:)。
2021-03-22 19:09:37
谢谢你写得很好。但是,我不同意某些方面。LOAD_USER 是如何势在必行的?对我来说,它不仅是声明性的——它还提供了很好的可读代码。像例如。“当我按下这个按钮时,我想要 ADD_ITEM”。我可以查看代码并确切了解发生了什么。如果它被称为“BUTTON_CLICK”的效果,我必须查一下。
2021-03-22 19:09:37
我不喜欢这个答案,因为它分散了实际问题的注意力,以推销某人自己的图书馆。这个答案提供了两个库的比较,这不是问题的意图。实际问题是询问是否完全使用中间件,这由已接受的答案解释。
2021-04-05 19:09:37
@swelet 抱歉迟到了。当您 dispatch 时ADD_ITEM,它是必不可少的,因为您发送了一个旨在对您的商店产生影响的操作:您希望该操作执行某些操作。声明式包含事件溯源的理念:您不会分派操作来触发应用程序的更改,而是分派过去的事件来描述应用程序中发生的事情。事件的调度应该足以考虑应用程序的状态已更改。有一个响应事件的 Redux 存储是一个可选的实现细节
2021-04-07 19:09:37

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

在我们刚开始工作的一个新项目中,我有一个非常相似的想法。我是 vanilla Redux 优雅系统的忠实粉丝,该系统以一种不受 React 组件树结构影响的方式更新存储和重新渲染组件。使用这种优雅的dispatch机制来处理异步对我来说似乎很奇怪

我最终采用了一种与我从我们的项目中提取的库中所拥有的非常相似的方法,我们称之为react-redux-controller

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

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

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

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

更新

我突然想到这个难题的一部分是react-redux的限制第一个参数connect获取状态快照,但不是调度。第二个参数得到调度而不是状态。这两个参数都没有获得关闭当前状态的 thunk,因为能够在继续/回调时看到更新的状态。

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

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

  • 减速机没门。它们应该是没有副作用的纯函数。更新商店是一项严肃而复杂的工作。不要污染它。
  • 哑视图组件?绝对不。他们关心一个问题:演示和用户交互,并且应该尽可能简单。
  • 容器组件?可能,但次优。容器是我们封装一些与视图相关的复杂性并与存储交互的地方,这是有道理的,但是:
    • 容器确实需要比哑组件更复杂,但它仍然是一个单一的职责:提供视图和状态/存储之间的绑定。您的异步逻辑与此完全不同。
    • 通过将其放置在容器中,您会将异步逻辑锁定到单个上下文中,并与一个或多个视图/路由耦合。馊主意。理想情况下,它都是可重用的,并且与视图完全分离。
    • (与所有规则一样,如果您有可在多个上下文中重用的有状态绑定逻辑,或者您可以以某种方式将所有状态概括为集成的 GraphQL 模式之类的东西,则可能会有一个例外。好吧,好吧,那可能是很酷。但是……大多数情况下,这些绑定似乎都以特定于上下文/视图的方式结束。)
  • 其他一些服务module?坏主意:您需要注入对商店的访问权限,这是可维护性/可测试性的噩梦。更好地使用 Redux 并仅使用提供的 API/模型访问商店。
  • 操作和解释它们的中间件?为什么不?!首先,这是我们剩下的唯一主要选择。:-) 更合乎逻辑的是,动作系统是分离的执行逻辑,您可以在任何地方使用。它可以访问 store 并可以发送更多操作。它有一个单一的职责,即围绕应用程序组织控制流和数据流,大多数异步都适合这一点。
    • 动作创作者呢?为什么不在那里做异步,而不是在动作本身和中间件中?
      • 首先,也是最重要的一点,创建者无法像中间件那样访问商店。这意味着您无法分派新的偶然操作,无法从存储中读取以组成您的异步等。
      • 因此,将复杂性放在必要的复杂位置,并保持其他一切简单。然后,创建者可以是易于测试的简单、相对纯的函数。
容器组件- 为什么不呢?由于组件在 React 中扮演的角色,容器可以充当服务类,并且它已经通过 DI(道具)获得了一个存储。通过将其放置在容器中,您会将异步逻辑锁定到单个上下文中,用于单个视图/路由- 怎么办?一个组件可以有多个实例。它可以与演示分离,例如使用渲染道具。我想答案可以从证明这一点的简短例子中受益更多。
2021-03-11 19:09:37
这是一个很好的答案
2021-03-29 19:09:37

回答一开始提出的问题:

为什么容器组件不能调用async API,然后dispatch action?

请记住,这些文档适用于 Redux,而不是 Redux 和 React。连接到 React 组件的Redux 存储可以完全按照您说的做,但是没有中间件的普通 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是(或需要)异步的。

如果您还想支持promisesobservablessagas疯狂的自定义高度声明性的动作创建者,那么 Redux 可以通过更改您传递给的内容dispatch(也就是您从动作创建者返回的内容)来实现。无需处理 React 组件(或connect调用)。

您建议在操作完成时再发送另一个事件。当您需要在操作完成后显示 alert() 时,这将不起作用。不过 React 组件中的 Promise 确实有效。我目前推荐 Promises 方法。
2021-04-06 19:09:37