如何在 redux-observable 中的同一史诗中发出动作和流?

How can I emit an action and a stream in the same epic in redux-observable?

我正在尝试将我的 executeWithRetries 实用程序(见下文)升级到最新版本的 redux-observable,其中不再允许在史诗中调度。

目前,此实用程序会查看所需令牌是否已过期,如果已过期,它会调度一个 REFRESH_TOKEN 操作和 returns 一个新的 action$ 以响应生成的 REFRESH_TOKEN_SUCCESS 通过执行原始请求来执行操作,其中包含新令牌。这工作正常。

如果不显式分派操作,我怎么能同时:

  1. 触发 REFRESH_TOKEN 动作并且
  2. 创建响应另一个(结果)操作的流?
import {isEmpty} from 'ramda';
import { of } from 'rxjs/observable/of';
import {
  actions as guestActions,
  selectApiKey,
  selectGuestRefreshToken,
  selectGuestTokenExpiryMs,
  types as guestTypes,
} from '../reducers/guest';

const retryOrFail = ({ requestFactory, failure, success, store }) => {
  const state = store.getState();
  const apiKey = selectApiKey(state);
  const tokens = { apiKey, accessToken };
  return requestFactory.retry(tokens)
    .map(res => {
      return success(res);
    })
    .catch(err => {
      return retryOrFail({ requestFactory, failure, success, store });
    });
};

const refreshTokensIfNecessary = (store) => {
  const state = store.getState();
  const guestRefreshToken = selectGuestRefreshToken(state);
  const guestTokenExpiryMs = selectGuestTokenExpiryMs(state);
  const { dispatch } = store;

  const guestTokenExpired = () => guestRefreshToken && guestTokenExpiryMs && guestTokenExpiryMs < Date.now();

  if (guestTokenExpired()) {
    dispatch(guestActions.refreshToken(guestRefreshToken));
    return true;
  }
  return false;
}

export default function executeWithRetries({ requestFactory, success, failure, store, action$ }) {
  const retryAction = action$.ofType(
    guestTypes.REFRESH_TOKEN_SUCCESS
  )
    .take(1)
    .switchMap(() => retryOrFail({ requestFactory, failure, success, store }));

  if (refreshTokensIfNecessary(store)) {
    return retryAction;
  }

  return requestFactoryToUse.execute()
    .map(res => success(res))
    .catch((err) => {
      return of(failure(err.toString()));
    });
}

这段代码的结构可能不再与 redux-observable@latest 兼容,因为它依赖于分派一个动作,但我不知道有其他方法可以处理这个问题案例.

编辑——这个解决方案不是最优的——请参阅我的其他答案。


事实证明我仍然可以直接使用dispatch如果我:

  1. 导出实例化store,以及
  2. 直接将商店导入我的史诗实用程序并使用 store.dispatch

configureStore.js

...

export const store = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(epicMiddleware)),
);

const configureStore = () => {
  epicMiddleware.run(rootEpic);
  return store;
};

export default configureStore;

(下面的示例史诗在减速器中使用 mapTo 或(甚至更好)会更好,但这无论如何都证明了这种能力):

cancelOrder.js

import { store } from '../configureStore';

export const CancelOrder = (action$, store, { dispatch }) => action$
  .pipe(
    ofType(types.CANCEL_ORDER),
    tap(() => store.dispatch(cancelSideEffectRequestAction)),
    ignoreElements()
  );

可以将 store.dispatch 添加到史诗中间件的依赖项中。

我们需要扩展 redux-observable 的 epicMiddleware 以包含商店:

epicMiddleware.js

import { createEpicMiddleware } from 'redux-observable'

const epicMiddleware = (store) => {
    const originalEpicMiddleware = createEpicMiddleware({
        dependencies: {
            dispatch: store.dispatch,
        },
    })

    const storeMiddleware = originalEpicMiddleware(store)

    epicMiddleware.run = originalEpicMiddleware.run

    return storeMiddleware
}

export default epicMiddleware

然后,我们在商店实例化中包含我们的 epicMiddleware 版本:

store.js

import { createStore, applyMiddleware } from 'redux';

import epicMiddleware from './epicMiddleware';
import rootEpic from './rootEpic';
import rootReducer from './rootReducer';

const store = createStore(
    rootReducer,
    [],
    applyMiddleware(epicMiddleware),
);

epicMiddleware.run(rootEpic);

export default store;

最后,我们可以像这样在史诗中包含 dependencies 中的 dispatch

triggerEpic.js

import { ignoreElements, tap } from 'rxjs/operators';
import { ofType } from 'redux-observable';

const triggerEpic = (action$, state$, { dispatch }) =>
    action$.pipe(
        ofType('TRIGGER_EPIC'),
        tap(() => {
            dispatch({ type: 'EPIC_TRIGGERED ' })
        }),
        ignoreElements(),
    );

export default triggerEpic;

在此处查看有效的 CodeSandbox:https://codesandbox.io/s/redux-observable-middleware-with-dispatch-0s09v

h/t @Sawtaytoes

直接从 redux-observable 史诗中调用商店的 dispatch 方法通常并不理想,实际上违背了库的目的,即抽象出动作的直接分派以支持转换将一系列动作转化为一系列新动作。

您的目标是分派令牌刷新操作以及触发流侦听某些结果操作。一般解决方案类似于:

action$.pipe(
  ofType('SOME_TYPE'),
  op.mergeMap(action => {
    const onSuccess$ = action$.pipe(
      ofType('TOKEN_REFRESH_SUCCESS'),
      // ...rest of refreshed token logic
    );
    return Rx.merge(Rx.of(refreshToken()), onSuccess$);
  })
);

这将导致令牌刷新操作的分派,以及侦听令牌刷新成功并发出任何进一步操作的流的启动。

有一个不需要调度的非常简单的解决方案:

const refreshTokensIfNecessary = () => {
  ...

  if (tokenExpired) {
    return { type: 'REFRESH_TOKEN' };
  }

  return false;
};


...

  const retryAction$ = action$
    .pipe(
      ofType(
        types.REFRESH_TOKEN_SUCCESS,
      ),
      take(1),
      switchMap(() => retryOrFail({
        requestFactory: requestFactoryToUse, failure, success, store,
      })),
    );

  const refreshAction = refreshTokensIfNecessary();

  if (refreshAction) {

    return concat(
      of(refreshAction),
      retryAction$,
    );

  }

这里我所需要的只是一个流 concat动作对象和 retryAction$ 的流