Redux:从组件请求成功或错误流(使用 redux-saga)

Redux: request into success or error flow from Component (using redux-saga)

这是我还没有找到标准解决方案的一件事。

我用 redux-saga 设置了我的商店来处理副作用,我从组件分派一个动作(具有异步副作用),现在希望组件在处理完副作用后做一些事情(对于例如导航到另一个 route/screen,弹出一个 modal/toast 或其他任何东西)。

我还想在失败时显示加载指示器或任何错误。

在 redux 之前,这种流程很简单,看起来像这样:

try {
  this.setState({loading: true});
  const result = await doSomeRequest();
  this.setState({item: result, loading: false});
} catch (e) {
  this.setState({loading: false, error: e});
}

使用 redux,我通常希望分派一个启动流程的操作并在存储中包含所有相关信息,以允许许多组件收听正在发生的事情。

我可以有 3 个动作,'requested'、'success'、'failed'。 在我的组件中,我将发送请求的操作。 负责的 saga 将在处理 'requested'.

时发送 'success' 或 'failed' 操作

我的组件将反映更改。 但是我还没有找到一种标准方法来确定操作是否已完成。也许商店没有因为异步操作而更新(NO-OP,但我猜加载状态仍然会改变)。但是操作还是成功了,我想做一些导航到另一个屏幕之类的事情。

我真的尝试在 redux 文档、redux-saga 文档或 Whosebug/Google 中找到这种(看似常见的)场景,但没有成功。

注意:同样使用 redux-thunk 我认为这种行为是直接实现的,因为我可以 .then 进行异步操作调度并且会在 catch 中接收成功操作或错误操作(如果我纠正我错了,从来没有真正使用过 thunk)。但是我还没有看到使用 redux-saga 实现的相同行为。

我提出了 3 个具体的解决方案:

  1. 最原始的解决方案,仅处理来自组件的 'success'/'failed' 操作。现在这个解决方案我不是很喜欢。在我的具体实现中,没有指示异步请求已启动的操作。副作用是在组件中处理的,而不是在传奇中被抽象出来。很多潜在的代码重复。

  2. 运行 在发送请求操作之前的一次性传奇,它使 'success'/'failed' 操作相互竞争并允许对第一次发生的动作。为此,我写了一个帮助程序来抽象出传奇的 运行ning:https://github.com/milanju/redux-post-handling-example/blob/master/src/watchNext.js 这个例子我比 1. 更喜欢,因为它简单且声明性强。虽然我不知道在这样的 运行 时间内创建一个 saga 是否会产生任何负面影响,或者也许还有另一种 'proper' 方法来实现我正在用 redux-saga 做的事情?

  3. 将与动作相关的所有内容(加载、successFlag、错误)放入存储中,并在 componentWillReceiveProps 中对动作更改做出反应 ((!this.props.success && nextProps.success))表示操作已成功完成)。 这类似于第二个示例,但适用于您选择的任何副作用处理解决方案。 也许我正在监督一些事情,比如如果 props 非常快地进入 componentWillReceiveProps 的 props 将 'pile up' 并且组件完全跳过从不成功到成功的过渡,那么检测到一个动作成功不起作用?

请随时查看我为这个问题创建的示例项目,它实现了完整的示例解决方案:https://github.com/milanju/redux-post-handling-example

我希望就我用来处理所述操作流程的方法提供一些意见。

  1. 我是不是误会了什么?我想出的 'solutions' 对我来说一点都不直接。可能我看问题的角度不对。
  2. 上面的例子有什么问题吗?
  3. 是否有针对此问题的最佳实践或标准解决方案?
  4. 您如何处理所描述的流程?

感谢阅读。

如果我对你的问题的理解正确,你希望你的组件根据你的传奇触发的动作采取行动。这通常会在 componentWillReceiveProps 中发生 - 使用新道具调用该方法,而旧道具仍然可以通过 this.props 获得。

然后将状态(请求/成功/失败)与旧状态进行比较,并相应地处理各种转换。

如果我误解了什么,请告诉我。

也许我不明白你面临的问题,但我猜他们的意思是客户看起来像这样:

mapStateToProps = state => {
 return {
   loading: state.loading,
   error  : state.error,
   data   : state.data 
 };
}

组件将呈现为:

return(
   {this.props.loading  && <span>loading</span>}
   {!this.props.loading && <span>{this.props.data}</span>}
   {this.props.error    && <span>error!</span>}
)

并且当 requested 动作被分派时,其减速器将更新存储状态为 {loading: true, error: null}

并且当 succeeded 动作被分派时,它的 reducer 会将存储状态更新为 {loading: false, error: null, data: results}

并且当 failed 动作被分派时,其减速器将更新存储状态为 {loading: false, error: theError}

这样您就不必使用 componentWillReceiveProps 了。 我希望已经清楚了。

我通过以下方式使用 saga 在组件中实现了异步操作回调:

class CallbackableComponent extends Component {
  constructor() {
    super()
    this.state = {
      asyncActionId: null,
    }
  }

  onTriggerAction = event => {
    if (this.state.asyncActionId) return; // Only once at a time
    const asyncActionId = randomHash();
    this.setState({
      asyncActionId
    })
    this.props.asyncActionWithId({
      actionId: asyncActionId,
      ...whateverParams
    })
  }

  static getDerivedStateFromProps(newProps, prevState) {
    if (prevState.asyncActionId) {
      const returnedQuery = newProps.queries.find(q => q.id === prevState.asyncActionId)
      return {
        asyncActionId: get(returnedQuery, 'status', '') === 'PENDING' ? returnedQuery.id : null
      }
    }
    return null;
  }
}

像这样使用 queries 减速器:

import get from 'lodash.get'

const PENDING = 'PENDING'
const SUCCESS = 'SUCCESS'
const FAIL = 'FAIL'

export default (state = [], action) => {
  const id = get(action, 'config.actionId')
  if (/REQUEST_DATA_(POST|PUT|DELETE|PATCH)_(.*)/.test(action.type)) {
    return state.concat({
      id,
      status: PENDING,
    })
  } else if (
    /(SUCCESS|FAIL)_DATA_(GET|POST|PUT|PATCH)_(.*)/.test(action.type)
  ) {
    return state
      .filter(s => s.status !== PENDING) // Delete the finished ones (SUCCESS/FAIL) in the next cycle
      .map(
        s =>
          s.id === id
            ? {
                id,
                status: action.type.indexOf(SUCCESS) === 0 ? SUCCESS : FAIL,
              }
            : s
      )
  }
  return state
}

最后,我的 CallbackableComponent 通过检查 this.state.asyncActionId 是否存在来知道查询是否完成。

但这要付出代价:

  1. 向商店添加条目(尽管这是不可避免的)
  2. 在组件上增加了很多复杂性。

我预计:

  1. asyncActionId 逻辑要在 saga 端进行(例如,当使用 mapActionsToProps 编辑异步操作时 connect,它 returns id 调用时:const asyncActionId = this.props.asyncAction(params),如 setTimeout)
  2. store部分被redux-saga抽象出来,就像react-router负责将当前路由添加到store中。

目前,我还没有找到更简洁的方法来实现这一目标。但我很想对此有所了解!