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

Posted

技术标签:

【中文标题】Redux:从组件请求成功或错误流(使用 redux-saga)【英文标题】:Redux: request into success or error flow from Component (using redux-saga) 【发布时间】:2017-12-01 00:48:02 【问题描述】:

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

我使用 redux-saga 设置了我的商店以进行副作用处理,我从组件调度了一个动作(具有异步副作用),现在希望组件在处理完副作用后执行某些操作(例如导航到另一条路线/屏幕,弹出模式/吐司或其他任何东西)。

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

在 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 个动作,“请求”、“成功”、“失败”。 在我的组件中,我将分派请求的操作。 然后,负责的 saga 将在处理“请求”时调度“成功”或“失败”操作。

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

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

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

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

    最原始的解决方案,仅处理来自组件的“成功”/“失败”操作。现在这个解决方案我不是很喜欢。在我的具体实现中,没有任何动作表明异步请求已启动。副作用直接在组件中处理,而不是在 saga 中抽象出来。大量潜在的代码重复。

    在调度请求操作之前运行一次性 saga,使“成功”/“失败”操作相互竞争,并允许对第一个发生的操作做出反应。为此,我编写了一个帮助程序将 saga 的运行抽象化:https://github.com/milanju/redux-post-handling-example/blob/master/src/watchNext.js 这个例子我更喜欢 1. 因为它简单且具有声明性。虽然我不知道在运行时创建这样的 saga 是否会产生负面影响,或者也许还有另一种“正确”的方式来实现我正在使用 redux-saga 做的事情?

    将与操作相关的所有内容(加载、成功标志、错误)放入存储中,并在 componentWillReceiveProps 中对操作更改做出反应((!this.props.success && nextProps.success))意味着操作已成功完成)。 这类似于第二个示例,但适用于您选择的任何副作用处理解决方案。 也许我正在监督一些事情,比如检测一个动作是否成功,如果道具很快出现并且进入 componentWillReceiveProps 的道具将“堆积”并且组件完全跳过从不成功到成功的过渡?

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

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

    我在这里误解了什么吗?我想出的“解决方案”对我来说根本不是直截了当的。也许我从错误的角度看待问题。 上面的例子有什么问题吗? 是否有针对此问题的最佳实践或标准解决方案? 您如何处理所描述的流程?

感谢阅读。

【问题讨论】:

这里有同样的问题。我很惊讶这不是图书馆已经处理的事情,因为这感觉很基础。 【参考方案1】:

我通过以下方式使用 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 是否存在来知道查询是否完成。

但这是以以下为代价的:

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

我希望:

    asyncActionId 逻辑被保存在 saga 端(例如,当异步操作是 connected 使用 mapActionsToProps 时,它在调用时返回 idconst asyncActionId = this.props.asyncAction(params),如 setTimeout) 要被redux-saga抽象的store部分,就像react-router负责将当前路由添加到store。

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

【讨论】:

【参考方案2】:

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

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动作被调度时,它的reducer会将存储状态更新为loading: true, error: null

succeeded动作被调度时,它的reducer会将存储状态更新为loading: false, error: null, data: results

failed动作被调度时,它的reducer会将存储状态更新为loading: false, error: theError

这样您就不必使用 componentWillReceiveProps 。 我希望它很清楚。

【讨论】:

这行得通,但这并不真正与触发查询的组件无关(如果有多个)。我会报告我的解决方案,即使我不是它的忠实粉丝。 看看我的回答,如果这对你有意义,请告诉我。【参考方案3】:

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

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

如果我误解了某些内容,请告诉我。

【讨论】:

这个很不清楚。请求/成功/失败是操作。你在店里放什么? componentWillReceiveProps 也被弃用了。 @AugustinRiedinger 如果这不是一个反问,那么我添加了关于 Richard 解决方案的详细答案。

以上是关于Redux:从组件请求成功或错误流(使用 redux-saga)的主要内容,如果未能解决你的问题,请参考以下文章

无法通过 React 和 Redux 将相同的表单组件用于添加或编辑模式

意外的 redux 操作调度行为

使用 Redux 进行组件验证

Redux-Form 与 A​​pollo

在 React 组件中订阅 redux 操作

Redux 中间件和 GraphQL