如何在Redux中处理复杂的副作用?
我一直在努力寻找解决这个问题的几个小时…
我正在开发一个在线记分牌游戏。 玩家可以随时login和注销。 游戏结束后,玩家将看到记分牌,并看到自己的排名,并自动提交分数。
记分牌显示玩家的排名和排行榜。
记分牌在用户完成游戏(提交分数)和用户只是想要查看他们的排名时都使用。
这是逻辑变得非常复杂的地方:
-
如果用户已经login,则分数将首先被提交。 新纪录保存后,记分板将被加载。
-
否则,记分牌将被立即加载。 玩家将有权selectlogin或注册。 之后,将提交比分,然后记分牌再次刷新。
-
但是,如果没有分数提交(只是查看高分表)。 在这种情况下,玩家现有的logging只需下载。 但是由于这个动作不会影响记分牌,记分牌和玩家的logging应该同时下载。
-
有无限的层次。 每个级别都有不同的记分牌。 当用户查看记分牌时,用户正在“观察”该记分牌。 closures时,用户停止观察。
-
用户可以随时login和注销。 如果用户注销,则用户的排名应该消失,如果用户以另一个帐户login,则应该获取并显示该帐户的排名信息。
…但是这只能获取用户当前正在观察的记分牌。
-
对于查看操作,结果应该caching在内存中,这样如果用户重新订阅相同的记分板,则不会有取回。 但是,如果提交了分数,则不应使用caching。
-
任何这些networking操作都可能失败,玩家必须能够重试。
-
这些操作应该是primefaces的。 所有的州应该一次更新(没有中间状态)。
目前,我能够使用Bacon.js(一个functionreact native编程库)来解决这个问题,因为它带有primefaces更新支持。 代码非常简洁,但现在却是一团乱七八糟的不可预知的意大利面代码。
我开始看Redux。 所以我试图构build商店,并提出了这样的事情(在YAMLish语法):
user: (user information) record: level1: status: (loading / completed / error) data: (record data) error: (error / null) scoreboard: level1: status: (loading / completed / error) data: - (record data) - (record data) - (record data) error: (error / null)
问题变成:我在哪里放置副作用。
对于无副作用的操作,这变得非常简单。 例如,在LOGOUT
操作中, record
简化器可以简单地closures所有的logging。
但是,有些行为确实有副作用。 例如,如果我在提交分数前未login,则我成功login, SET_USER
操作会将用户保存到商店中。
但是因为我有一个提交的分数,所以这个SET_USER
操作还必须导致一个AJAX请求被触发,同时将record.levelN.status
设置为loading
。
问题是: 我如何表示 在以primefaces方式login时应该发生 副作用 (得分提交) ?
在Elm体系结构中,当使用Action -> Model -> (Model, Effects Action)
,更新器也可以发出副作用,但是在Redux中,它只是(State, Action) -> State
。
从asynchronous操作文档中,他们推荐的方式是将其放入操作创build者。 这是否意味着提交分数的逻辑将不得不放在动作创build者的成功login行动?
function login (options) { return (dispatch) => { service.login(options).then(user => dispatch(setUser(user))) } } function setUser (user) { return (dispatch, getState) => { dispatch({ type: 'SET_USER', user }) let scoreboards = getObservedScoreboards(getState()) for (let scoreboard of scoreboards) { service.loadUserRanking(scoreboard.level) } } }
我觉得这有点奇怪,因为负责这个连锁反应的代码现在存在于两个地方:
- 在减速机中。 当调度
SET_USER
动作时,record
SET_USER
器还必须将属于观察记分板的logging的状态设置为loading
。 - 在行动创造者,其中执行获取/提交分数的实际副作用。
我似乎也必须手动跟踪所有活跃的观察者。 而在Bacon.js版本中,我做了这样的事情:
Bacon.once() // When first observing the scoreboard .merge(resubmit口) // When resubmitting because of network error .merge(user川.changes().filter(user => !!user).first()) // When user logs in (but only once) .flatMapLatest(submitOrGetRanking(data))
由于上述所有复杂的规则,实际的培根代码要长得多,这使得培根版本几乎不可读。
但培根自动跟踪所有有效的订阅。 这导致我开始质疑,这可能不值得转换,因为重写这个到Redux将需要大量的手动处理。 任何人都可以提示一些指针
当你想要复杂的asynchronous依赖关系时,只需使用Bacon,Rx,channels,sagas或其他asynchronous抽象。 你可以使用或不使用Redux。 Redux示例:
observeSomething() .flatMap(someTransformation) .filter(someFilter) .map(createActionSomehow) .subscribe(store.dispatch);
你可以用任何你喜欢的方式来store.dispatch(action)
你的asynchronous操作,唯一重要的部分是它最终会变成store.dispatch(action)
调用。
Redux Thunk对于简单的应用程序已经足够了,但是随着asynchronous需求变得更加复杂,您需要使用真正的asynchronous构build抽象,而Redux并不关心您使用的是哪一种。
更新:一段时间过去了,出现了一些新的解决scheme。 我build议你查看Redux Saga ,它已经成为Redux中非常stream行的asynchronous控制stream程解决scheme。
编辑 :现在有一个由这些想法启发的简化项目
这里有一些不错的资源
- 比较redux-saga和redux-thunk
- Redux-saga vs Redux-thunk with async / await
- 在Redux Saga中pipe理进程
- 从ActionCreators到Sagas
- 用Redux-saga实现的贪吃蛇游戏
Flux / Redux受到后端事件stream处理的启发(无论名称是:eventsourcing,CQRS,CEP,lambda体系结构…)。
我们可以比较Flux的ActionCreators / Actions和Commands / Events(通常在后台系统中使用的术语)。
在这些后端体系结构中,我们使用一种通常被称为Saga或Process Manager的模式。 基本上它是系统中接收事件的一部分,可以pipe理自己的状态,然后可以发出新的命令。 为了简单起见,这有点像实现IFTTT(If-This-Then-That)。
你可以用FRP和BaconJS来实现这个,但是你也可以在Redux reducer的基础上实现它。
function submitScoreAfterLoginSaga(action, state = {}) { switch (action.type) { case SCORE_RECORDED: // We record the score for later use if USER_LOGGED_IN is fired state = Object.assign({}, state, {score: action.score} case USER_LOGGED_IN: if ( state.score ) { // Trigger ActionCreator here to submit that score dispatch(sendScore(state.score)) } break; } return state; }
为了说清楚:从reducer驱动React渲染计算的状态应绝对保持纯粹! 但是并不是所有的应用程序状态都有触发渲染的目的,在这种情况下,需要使用复杂的规则同步应用程序的不同部分。 这个“传奇”的状态不应该触发一个React渲染!
我不认为Redux提供任何支持这种模式的东西,但你可以很容易地自己实现它。
我已经在我们的启动框架中完成了这个模式,这个模式工作正常 它允许我们处理IFTTT事情,如:
-
当用户启动和用户closuresPopup1,然后打开Popup2,并显示一些提示工具提示。
-
当用户使用移动网站并打开Menu2时,closuresMenu1
重要提示 :如果您正在使用某些框架(如Redux)的撤消/重做/重播function,重要的是在重播事件日志期间,所有这些传奇都是无线的,因为您不希望在重播期间触发新事件!