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 个具体的解决方案:
最原始的解决方案,仅处理来自组件的 'success'/'failed' 操作。现在这个解决方案我不是很喜欢。在我的具体实现中,没有指示异步请求已启动的操作。副作用是在组件中处理的,而不是在传奇中被抽象出来。很多潜在的代码重复。
运行 在发送请求操作之前的一次性传奇,它使 'success'/'failed' 操作相互竞争并允许对第一次发生的动作。为此,我写了一个帮助程序来抽象出传奇的 运行ning:https://github.com/milanju/redux-post-handling-example/blob/master/src/watchNext.js
这个例子我比 1. 更喜欢,因为它简单且声明性强。虽然我不知道在这样的 运行 时间内创建一个 saga 是否会产生任何负面影响,或者也许还有另一种 'proper' 方法来实现我正在用 redux-saga 做的事情?
将与动作相关的所有内容(加载、successFlag、错误)放入存储中,并在 componentWillReceiveProps 中对动作更改做出反应 ((!this.props.success && nextProps.success))表示操作已成功完成)。
这类似于第二个示例,但适用于您选择的任何副作用处理解决方案。
也许我正在监督一些事情,比如如果 props 非常快地进入 componentWillReceiveProps 的 props 将 'pile up' 并且组件完全跳过从不成功到成功的过渡,那么检测到一个动作成功不起作用?
请随时查看我为这个问题创建的示例项目,它实现了完整的示例解决方案:https://github.com/milanju/redux-post-handling-example
我希望就我用来处理所述操作流程的方法提供一些意见。
- 我是不是误会了什么?我想出的 'solutions' 对我来说一点都不直接。可能我看问题的角度不对。
- 上面的例子有什么问题吗?
- 是否有针对此问题的最佳实践或标准解决方案?
- 您如何处理所描述的流程?
感谢阅读。
如果我对你的问题的理解正确,你希望你的组件根据你的传奇触发的动作采取行动。这通常会在 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
是否存在来知道查询是否完成。
但这要付出代价:
- 向商店添加条目(尽管这是不可避免的)
- 在组件上增加了很多复杂性。
我预计:
asyncActionId
逻辑要在 saga 端进行(例如,当使用 mapActionsToProps
编辑异步操作时 connect
,它 returns id
调用时:const asyncActionId = this.props.asyncAction(params)
,如 setTimeout
)
- store部分被
redux-saga
抽象出来,就像react-router
负责将当前路由添加到store中。
目前,我还没有找到更简洁的方法来实现这一目标。但我很想对此有所了解!
这是我还没有找到标准解决方案的一件事。
我用 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 个具体的解决方案:
最原始的解决方案,仅处理来自组件的 'success'/'failed' 操作。现在这个解决方案我不是很喜欢。在我的具体实现中,没有指示异步请求已启动的操作。副作用是在组件中处理的,而不是在传奇中被抽象出来。很多潜在的代码重复。
运行 在发送请求操作之前的一次性传奇,它使 'success'/'failed' 操作相互竞争并允许对第一次发生的动作。为此,我写了一个帮助程序来抽象出传奇的 运行ning:https://github.com/milanju/redux-post-handling-example/blob/master/src/watchNext.js 这个例子我比 1. 更喜欢,因为它简单且声明性强。虽然我不知道在这样的 运行 时间内创建一个 saga 是否会产生任何负面影响,或者也许还有另一种 'proper' 方法来实现我正在用 redux-saga 做的事情?
将与动作相关的所有内容(加载、successFlag、错误)放入存储中,并在 componentWillReceiveProps 中对动作更改做出反应 ((!this.props.success && nextProps.success))表示操作已成功完成)。 这类似于第二个示例,但适用于您选择的任何副作用处理解决方案。 也许我正在监督一些事情,比如如果 props 非常快地进入 componentWillReceiveProps 的 props 将 'pile up' 并且组件完全跳过从不成功到成功的过渡,那么检测到一个动作成功不起作用?
请随时查看我为这个问题创建的示例项目,它实现了完整的示例解决方案:https://github.com/milanju/redux-post-handling-example
我希望就我用来处理所述操作流程的方法提供一些意见。
- 我是不是误会了什么?我想出的 'solutions' 对我来说一点都不直接。可能我看问题的角度不对。
- 上面的例子有什么问题吗?
- 是否有针对此问题的最佳实践或标准解决方案?
- 您如何处理所描述的流程?
感谢阅读。
如果我对你的问题的理解正确,你希望你的组件根据你的传奇触发的动作采取行动。这通常会在 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
是否存在来知道查询是否完成。
但这要付出代价:
- 向商店添加条目(尽管这是不可避免的)
- 在组件上增加了很多复杂性。
我预计:
asyncActionId
逻辑要在 saga 端进行(例如,当使用mapActionsToProps
编辑异步操作时connect
,它 returnsid
调用时:const asyncActionId = this.props.asyncAction(params)
,如setTimeout
)- store部分被
redux-saga
抽象出来,就像react-router
负责将当前路由添加到store中。
目前,我还没有找到更简洁的方法来实现这一目标。但我很想对此有所了解!