在redux-observable中编写和排序多个史诗 [英] Composing and sequencing multiple epics in redux-observable

查看:101
本文介绍了在redux-observable中编写和排序多个史诗的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个问题,我不知道如何解决。

I have a problem that I don't know how to resolve.

我有两个史诗要求api并更新商店:

I have two epics that do requests to api and update the store:

const mapSuccess = actionType => response => ({
  type: actionType + SUCCESS,
  payload: response.response,
});

const mapFailure = actionType => error => Observable.of({
  type: actionType + FAILURE,
  error,
});

const characterEpic = (action$, store) =>
  action$.ofType(GET_CHARACTER)
    .mergeMap(({ id }) => {
      return ajax(api.fetchCharacter(id))
        .map(mapSuccess(GET_CHARACTER))
        .catch(mapFailure(GET_CHARACTER));
    });

const planetsEpic = (action$, store) =>
  action$.ofType(GET_PLANET)
    .mergeMap(({ id }) => {
      return ajax(api.fetchPlanet(id))
        .map(mapSuccess(GET_PLANET))
        .catch(mapFailure(GET_PLANET));
    });

现在我有一个简单的场景,我想创建第三个动作,结合上面两个,我们称之为 fetchCharacterAndPlanetEpic 。我该怎么做?
我认为在很多情况下(以及在我看来),第一个动作的结果在第二个动作开始之前被分派到商店是很重要的。对于Promises和 redux-thunk ,这可能是微不足道的,但我无法想办法用 rxjs来做到这一点 redux-observable

Now I have a simple scenario where I would like to create the third action that combines the two above, let's call it fetchCharacterAndPlanetEpic. How can I do it? I think in many cases (and in my) it's important that result of the first action is dispatched to the store before the second begins. That would be probably trivial to do with Promises and redux-thunk, but I can't somehow think of a way to do it with rxjs and redux-observable.

谢谢!

推荐答案

Tomasz的答案有效并且有利有弊(最初建议在 redux-observable#33 )。一个潜在的问题是它使测试更难,但并非不可能。例如你可能不得不使用依赖注入来注入分叉史诗的模拟。

Tomasz's answer works and has pros and cons (it was originally suggested in redux-observable#33). One potential issue is that it makes testing harder, but not impossible. e.g. you may have to use dependency injection to inject a mock of the forked epic.

我开始在看到他之前输入一个答案,所以我想我也不如发布给后人,以防任何人感兴趣。

I had started typing up an answer prior to seeing his, so I figured I might as well post it for posterity in case it's interesting to anyone.

我之前也回答过另一个非常相似的问题,可能会有所帮助:如何延迟一个史诗,直到另一个史诗发出价值

I also previously answered another question which is very similar that may be helpful: How to delay one epic until another has emitted a value

我们可以发出 getCharacter(),然后等待匹配的 GET_CHARACTER_SUCCESS 在我们发出 getPlanet()之前。

We can emit the getCharacter(), then wait for a matching GET_CHARACTER_SUCCESS before we emit the getPlanet().

const fetchCharacterAndPlanetEpic = (action$, store) =>
  action$.ofType(GET_CHARACTER_AND_PLANET)
    .mergeMap(({ characterId, planetId }) =>
      action$.ofType(GET_CHARACTER_SUCCESS)
        .filter(action => action.payload.id === characterId) // just in case
        .take(1)
        .mapTo(getPlanet(planetId))
        .startWith(getCharacter(characterId))
    );

这种方法的一个潜在负面因素是理论上 GET_CHARACTER_SUCCESS 这个史诗收到的可能与我们正在等待的一个不同。过滤器 action.payload.id === characterId check主要针对这一点进行保护,因为如果它具有相同的ID,那么它是否特别适合你。

One potential negative of this approach is that theoretically the GET_CHARACTER_SUCCESS this epic receives could be a different one the exact one we were waiting for. The filter action.payload.id === characterId check protects you mostly against that, since it probably doesn't matter if it was specifically yours if it has the same ID.

要真正解决该问题,您需要某种独特的交易跟踪。我个人使用自定义解决方案,该解决方案涉及使用帮助程序函数来包含唯一的事务ID。类似这样的事情:

To truly fix that issue you'd need some sort of unique transaction tracking. I personally use a custom solution that involves using helper functions to include a unique transaction ID. Something like these:

let transactionID = 0;

const pend = action => ({
  ...action,
  meta: {
    transaction: {
      type: BEGIN,
      id: `${++transactionID}`
    }
  }
});

const fulfill = (action, payload) => ({
  type: action.type + '_FULFILLED',
  payload,
  meta: {
    transaction: {
      type: COMMIT,
      id: action.meta.transaction.id
    }
  }
});

const selectTransaction = action => action.meta.transaction;

然后可以这样使用:

const getCharacter = id => pend({ type: GET_CHARACTER, id });

const characterEpic = (action$, store) =>
  action$.ofType(GET_CHARACTER)
    .mergeMap(action => {
      return ajax(api.fetchCharacter(action.id))
        .map(response => fulfill(action, payload))
        .catch(e => Observable.of(reject(action, e)));
    });

const fetchCharacterAndPlanetEpic = (action$, store) =>
  action$.ofType(GET_CHARACTER_AND_PLANET)
    .mergeMap(action =>
      action$.ofType(GET_CHARACTER_FULFILLED)
        .filter(responseAction => selectTransaction(action).id === selectTransaction(responseAction).id)
        .take(1)
        .mapTo(getPlanet(action.planetId))
        .startWith(getCharacter(action.characterId))
    );

关键细节是初始挂起操作在元对象中保存唯一的事务ID。因此,初始操作基本上代表待处理的请求,然后在有人想要履行,拒绝或取消它时使用。 履行(行动,有效载荷)

The key detail is that the initial "pend" action holds a unique transaction ID in the meta object. So that initial action basically represents the pending request and is then used when someone wants to fulfill, reject, or cancel it. fulfill(action, payload)

我们的 fetchCharacterAndPlanetEpic 代码有点冗长,如果我们使用这样的东西,我们会做很多事情。所以让我们创建一个自定义运算符来为我们处理它。

Our fetchCharacterAndPlanetEpic code is kinda verbose and if we used something like this we'd be doing it a lot. So let's make a custom operator that handles it all for us.

// Extend ActionsObservable so we can have our own custom operators.
// In rxjs v6 you won't need to do this as it uses "pipeable" aka "lettable"
// operators instead of using prototype-based methods.
// https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md
class MyCustomActionsObservable extends ActionsObservable {
  takeFulfilledTransaction(input) {
    return this
      .filter(output =>
        output.type === input.type + '_FULFILLED' &&
        output.meta.transaction.id === input.meta.transaction.id
      )
      .take(1);
  }
}
// Use our custom ActionsObservable
const adapter = {
  input: input$ => new MyCustomActionsObservable(input$),
  output: output$ => output$
};
const epicMiddleware = createEpicMiddleware(rootEpic, { adapter });

然后我们可以在我们的史诗中使用该自定义运算符,并且干净利落地

Then we can use that custom operator in our epic nice and cleanly

const fetchCharacterAndPlanetEpic = (action$, store) =>
  action$.ofType(GET_CHARACTER_AND_PLANET)
    .mergeMap(action =>
      action$.takeFulfilledTransaction(action)
        .mapTo(getPlanet(action.planetId))
        .startWith(getCharacter(action.characterId))
    );






此处描述的交易式解决方案是真正的实验性。在实践中,我已经注意到了多年来的一些瑕疵,我还没有考虑如何解决它们。也就是说,总的来说,它在我的应用程序中非常有用。实际上,它也可以用来做乐观的更新和回滚!几年前,我将这个模式和可选的乐观更新内容放入库 redux-transaction 但是我从来没有回过头来给它一些爱,所以使用风险自负。它应被视为放弃,即使我可以回到它。


The transaction-style solution described here is truly experimental. In practice there are some warts with it I've noticed over the years and I haven't gotten around to thinking about how to fix them. That said, overall it's been pretty helpful in my apps. In fact, it can also be used to do optimistic updates and rollbacks too! A couple years ago I made this pattern and the optional optimistic update stuff into the library redux-transaction but I've never circled back to give it some love, so use at your own risk. It should be considered abandoned, even if I may come back to it.

这篇关于在redux-observable中编写和排序多个史诗的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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