如何处理 Redux 中复杂的副作用? [英] How to handle complex side-effects in Redux?

查看:21
本文介绍了如何处理 Redux 中复杂的副作用?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在努力寻找这个问题的解决方案......

I've been struggling for hours to finding a solution to this problem...

我正在开发一款带有在线记分牌的游戏.玩家可以随时登录和退出.完成一场比赛后,玩家会看到计分板,看到自己的排名,分数会自动提交.

I am developing a game with an online scoreboard. The player can log in and log out at any time. After finishing a game, the player will see the scoreboard, and see their own rank, and the score will be submitted automatically.

记分牌显示玩家的排名和排行榜.

The scoreboard shows the player’s ranking, and the leaderboard.

计分板在用户完成游戏(提交分数)和用户只想查看他们的排名时使用.

The scoreboard is used both when the user finishes playing (to submit a score), and when the user just wants to check out their ranking.

这就是逻辑变得非常复杂的地方:

This is where the logic becomes very complicated:

  • 如果用户已登录,则将首先提交分数.保存新记录后,将加载记分板.

  • If the user is logged in, then the score will be submitted first. After the new record is saved then the scoreboard will be loaded.

否则,记分板将立即加载.玩家可以选择登录或注册.之后提交分数,然后再次刷新记分板.

Otherwise, the scoreboard will be loaded immediately. The player will be given an option to log in or register. After that, the score will be submitted, and then the scoreboard will be refreshed again.

但是,如果没有分数提交(只需查看高分表).在这种情况下,只需下载玩家现有的记录.但是由于这个动作不影响计分板,所以应该同时下载计分板和玩家的记录.

However, if there is no score to submit (just viewing the high score table). In this case, the player’s existing record is simply downloaded. But since this action does not affect the scoreboard, both the scoreboard and the player’s record should be downloaded simultaneously.

有无限数量的级别.每个级别都有不同的记分牌.当用户查看记分牌时,用户正在观察"该记分牌.当它关闭时,用户停止观察它.

There is an unlimited number of levels. Each level has a different scoreboard. When the user views a scoreboard, then the user is ‘observing’ that scoreboard. When it is closed, the user stops observing it.

用户可以随时登录和注销.如果用户退出,该用户的排名应该会消失,如果用户以另一个账户登录,那么应该获取并显示该账户的排名信息.

The user can log in and log out at any time. If the user logs out, the user’s ranking should disappear, and if the user logs in as another account, then the ranking information for that account should be fetched and displayed.

...但是这种获取此信息的操作只应针对用户当前正在观察的记分板进行.

...but this fetching this information should only take place for the scoreboard whose user is currently observing.

对于查看操作,结果应该缓存在内存中,这样如果用户重新订阅同一个记分板,就不会获取.但是,如果有分数正在提交,则不应使用缓存.

For viewing operations, the results should be cached in-memory, so that if user re-subscribes to the same scoreboard, there will be no fetching. However, if there is a score being submitted, the cache should not be used.

任何这些网络操作都可能失败,玩家必须能够重试.

Any of these network operations may fail, and the player must be able to retry them.

这些操作应该是原子的.所有状态都应该一次性更新(没有中间状态).

These operations should be atomic. All the states should be updated in one go (no intermediate states).

目前,我可以使用 Bacon.js(一个函数式反应式编程库)来解决这个问题,因为它支持原子更新.代码相当简洁,但现在它是一个凌乱的不可预测的意大利面条式代码.

Currently, I am able to solve this using Bacon.js (a functional reactive programming library), as it comes with atomic update support. The code is quite concise, but right now it is a messy unpredictable spaghetti code.

我开始研究 Redux.所以我尝试构建商店,并想出了这样的东西(在 YAMLish 语法中):

I started looking at Redux. So I tried to structure the store, and came up with something like this (in YAMLish syntax):

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)

问题变成了:我把副作用放在哪里.

The problem becomes: where do I put the side-effects.

对于无副作用的操作,这变得非常容易.例如,在 LOGOUT 操作上,record 减速器可以简单地将所有记录炸掉.

For side-effect-free actions, this becomes very easy. For instance, on LOGOUT action, the record reducer could simply blast all the records off.

但是,某些操作确实有副作用.比如我在提交分数之前没有登录,那么我登录成功,SET_USER动作将用户保存到商店中.

However, some actions do have side effect. For example, if I am not logged in before submitting the score, then I log in successfully, the SET_USER action saves the user into the store.

但是因为我有一个分数要提交,这个SET_USER动作也必须导致一个AJAX请求被触发,同时设置record.levelN.status加载.

But because I have a score to submit, this SET_USER action must also cause an AJAX request to be fired off, and at the same time, set the record.levelN.status to loading.

问题是:我如何表示副作用(分数提交)当我以原子方式登录时应该发生?

在 Elm 架构中,更新程序在使用 Action -> 的形式时也可以发出副作用.模型 ->(Model, Effects Action),但在 Redux 中,它只是 (State, Action) ->状态.

In Elm architecture, an updater can also emit side-effects when using the form of Action -> Model -> (Model, Effects Action), but in Redux, it’s just (State, Action) -> State.

Async Actions 文档中,他们推荐的方式是将它们放入操作中创造者.这是否意味着提交分数的逻辑也必须放在动作创建者中才能成功登录动作?

From the Async Actions docs, the way they recommend is to put them in an action creator. Does this means that the logic of submitting the score will have to be put in the action creator for a successful login action as well?

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)
    }
  }
}

我觉得这有点奇怪,因为负责这个连锁反应的代码现在存在于 2 个地方:

I find this a bit odd, because the code responsible for this chain reaction now exists in 2 places:

  1. 在减速器中.当SET_USER动作被分派时,recordreducer还必须将属于观察记分板的记录的状态设置为loading.
  2. 在动作创建者中,它执行获取/提交分数的实际副作用.
  1. In the reducer. When SET_USER action is dispatched, the record reducer must also set the status of the records belonging to the observed scoreboards to loading.
  2. In the action creator, which performs the actual side-effect of fetching/submitting score.

看来我还必须手动跟踪所有活动的观察者.而在 Bacon.js 版本中,我做了这样的事情:

It also seems that I have to manually keep track of all the active observers. Whereas in Bacon.js version, I did something like this:

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))

实际的 Bacon 代码要长得多,因为上述所有复杂的规则,使得 Bacon 版本几乎不可读.

The actual Bacon code is a lot longer, because of the all the complex rules above, that made the Bacon version barely-readable.

但培根会自动跟踪所有活动订阅.这让我开始质疑它可能不值得切换,因为将其重写为 Redux 需要大量手动处理.任何人都可以建议一些指针吗?

But Bacon kept track of all active subscriptions automatically. This led me to start questioning that it might not be worth the switch, because rewriting this to Redux would require a lot of manual handling. Can anyone suggest some pointer?

推荐答案

当您需要复杂的异步依赖项时,只需使用 Bacon、Rx、channels、sagas 或其他异步抽象.您可以在有或没有 Redux 的情况下使用它们.Redux 示例:

When you want complex async dependencies, just use Bacon, Rx, channels, sagas, or another asynchronous abstraction. You can use them with or without Redux. Example with Redux:

observeSomething()
  .flatMap(someTransformation)
  .filter(someFilter)
  .map(createActionSomehow)
  .subscribe(store.dispatch);

您可以按照自己喜欢的任何方式编写异步操作——唯一重要的部分是它们最终会变成 store.dispatch(action) 调用.

You can compose your asynchronous actions any way you like—the only important part is that eventually they turn into store.dispatch(action) calls.

Redux Thunk 对于简单的应用程序来说已经足够了,但是随着您的异步需求变得越来越复杂,您需要使用真正的异步组合抽象,Redux 不关心你使用哪一个.

Redux Thunk is enough for simple apps, but as your async needs get more sophisticated, you need to use a real asynchronous composition abstraction, and Redux doesn't care which one you use.

更新:一段时间过去了,出现了一些新的解决方案.我建议您查看 Redux Saga,它已成为 Redux 中相当流行的异步控制流解决方案.

Update: Some time has passed, and a few new solutions have emerged. I suggest you to check out Redux Saga which has become a fairly popular solution for async control flow in Redux.

这篇关于如何处理 Redux 中复杂的副作用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆