Redux-Observable:修改状态触发后续动作
Redux-Observable: modify state and trigger follow up action
我在 redux-observable 中有以下场景。我有一个组件可以检测要使用哪个后端,并且应该设置 api-client 使用的后端 URL。客户端和 URL 都处于全局状态 object.
执行顺序应该是:
1.检查后台
2. 出错时替换后端 URL 保持状态
3. 使用新的后端状态触发 3 个动作来加载资源 URL
到目前为止我所做的是,在第 1 步中。从我的史诗中访问 state$ object 并修改支持的 URL。这似乎只奏效了一半。状态由 3 中触发的操作更新。仍然看到旧状态并使用错误的后端。
如果您依赖执行顺序,在操作之间更新状态的标准方法是什么?
我的 API-Epic 看起来像这样:
export const authenticate = (action$, state$) => action$.pipe(
ofType(actions.API_AUTHENTICATE),
mergeMap(action =>
from(state$.value.apiState.apiClient.authenticate(state$.value.apiState.bearer)).pipe(
map(bearer => apiActions.authenticatedSuccess(bearer))
)
)
)
export const authenticatedSuccess = (action$, state$) => action$.pipe(
ofType(actions.API_AUTHENTICATED_SUCCESS),
concatMap(action => concat(
of(resourceActions.doLoadAResource()),
of(resourceActions.doLoadOtherResource()),
of(resourceActions.doLoadSomethingElse()))
)
)
我发现用户在 GitHub 和 Whosebug 上讨论的一种常见方法是链接多个 epics,就像我相信您的示例试图演示的那样。第一个史诗在 "done" 时调度一个动作。 reducer 侦听此操作并更新商店的状态。第二个史诗(或者许多额外的epics,如果你想要并发操作)监听这个相同的动作并启动工作流的下一个序列。减速器之后的辅助 epics 运行 从而看到更新后的状态。 From the docs:
Epics run alongside the normal Redux dispatch channel, after the reducers have already received them...
我发现链接方法可以很好地解耦较大工作流的各个阶段。您可能出于设计原因(例如关注点分离)而希望解耦,以重用较大工作流的较小部分,或者制作较小的单元以便于测试。当您的史诗在较大工作流的不同阶段之间调度操作时,这是一种易于实施的方法。
但是,请记住 state$
是可观察的。您可以使用它在任何时间点获取 current 值——包括在单个史诗中调度不同操作之间。例如,考虑以下情况并假设我们的商店有一个简单的计数器:
export const workflow = (action$, state$) => action$.pipe(
ofType(constants.START),
withLatestFrom(state$),
mergeMap(([action, state]) => // "state" is the value when the START action was dispatched
concat(
of(actions.increment()),
state$.pipe(
first(),
map(state => // this new "state" is the _incremented_ value!
actions.decrement()),
),
defer(() => {
const state = state$.value // this new "state" is now the _decremented_ value!
return empty()
}),
),
),
)
有很多方法可以从 observable 获取当前状态!
关于您示例中的以下代码行:
state$.value.apiState.apiClient.authenticate(state$.value.apiState.bearer)
首先,使用状态传递 API 客户端不是 common/recommended 模式。您可能需要查看 injecting the API client as a dependency 到 epics(这使单元测试更容易!)。其次,尚不清楚 API 客户端如何从状态中获取 current 后端 URL。 API 客户端是否可能正在使用状态的缓存版本?如果是,您可能需要重构 authenticate
方法并 传入 当前后端 URL.
这是一个处理错误并包含上述内容的示例:
/**
* Let's assume the state looks like the following:
* state: {
* apiState: {
* backend: "URL",
* bearer: "token"
* }
*/
// Note how the API client is injected as a dependency
export const authenticate = (action$, state$, { apiClient }) => action$.pipe(
ofType(actions.API_AUTHENTICATE),
withLatestFrom(state$),
mergeMap(([action, state]) =>
// Try to authenticate against the current backend URL
from(apiClient.authenticate(state.apiState.backend, state.apiState.bearer)).pipe(
// On success, dispatch an action to kick off the chained epic(s)
map(bearer => apiActions.authenticatedSuccess(bearer)),
// On failure, dispatch two actions:
// 1) an action that replaces the backend URL in the state
// 2) an action that restarts _this_ epic using the new/replaced backend URL
catchError(error$ => of(apiActions.authenticatedFailed(), apiActions.authenticate()),
),
),
)
export const authenticatedSuccess = (action$, state$) => action$.pipe(
ofType(actions.API_AUTHENTICATED_SUCCESS),
...
)
此外,在链接 epics 时请记住,concat
不会 等待链接的 epics 到 "finish".例如:
concat(
of(resourceActions.doLoadAResource()),
of(resourceActions.doLoadOtherResource()),
of(resourceActions.doLoadSomethingElse()))
)
如果这些 doLoadXXX
动作中的每一个都 "starts" 是一部史诗,那么这三个动作很可能会同时 运行。每个action都会一个接一个的派发,每个epic都会"start"一个接一个运行,而不用等到前一个"finish"。这是因为 epics 从未真正 完整 。他们是 long-lived,永无止境的流媒体。如果您想 doLoadOtherResource
到 运行 after doLoadAResource
,您将需要明确地等待一些标识何时完成的信号。
我在 redux-observable 中有以下场景。我有一个组件可以检测要使用哪个后端,并且应该设置 api-client 使用的后端 URL。客户端和 URL 都处于全局状态 object.
执行顺序应该是: 1.检查后台 2. 出错时替换后端 URL 保持状态 3. 使用新的后端状态触发 3 个动作来加载资源 URL
到目前为止我所做的是,在第 1 步中。从我的史诗中访问 state$ object 并修改支持的 URL。这似乎只奏效了一半。状态由 3 中触发的操作更新。仍然看到旧状态并使用错误的后端。
如果您依赖执行顺序,在操作之间更新状态的标准方法是什么?
我的 API-Epic 看起来像这样:
export const authenticate = (action$, state$) => action$.pipe(
ofType(actions.API_AUTHENTICATE),
mergeMap(action =>
from(state$.value.apiState.apiClient.authenticate(state$.value.apiState.bearer)).pipe(
map(bearer => apiActions.authenticatedSuccess(bearer))
)
)
)
export const authenticatedSuccess = (action$, state$) => action$.pipe(
ofType(actions.API_AUTHENTICATED_SUCCESS),
concatMap(action => concat(
of(resourceActions.doLoadAResource()),
of(resourceActions.doLoadOtherResource()),
of(resourceActions.doLoadSomethingElse()))
)
)
我发现用户在 GitHub 和 Whosebug 上讨论的一种常见方法是链接多个 epics,就像我相信您的示例试图演示的那样。第一个史诗在 "done" 时调度一个动作。 reducer 侦听此操作并更新商店的状态。第二个史诗(或者许多额外的epics,如果你想要并发操作)监听这个相同的动作并启动工作流的下一个序列。减速器之后的辅助 epics 运行 从而看到更新后的状态。 From the docs:
Epics run alongside the normal Redux dispatch channel, after the reducers have already received them...
我发现链接方法可以很好地解耦较大工作流的各个阶段。您可能出于设计原因(例如关注点分离)而希望解耦,以重用较大工作流的较小部分,或者制作较小的单元以便于测试。当您的史诗在较大工作流的不同阶段之间调度操作时,这是一种易于实施的方法。
但是,请记住 state$
是可观察的。您可以使用它在任何时间点获取 current 值——包括在单个史诗中调度不同操作之间。例如,考虑以下情况并假设我们的商店有一个简单的计数器:
export const workflow = (action$, state$) => action$.pipe(
ofType(constants.START),
withLatestFrom(state$),
mergeMap(([action, state]) => // "state" is the value when the START action was dispatched
concat(
of(actions.increment()),
state$.pipe(
first(),
map(state => // this new "state" is the _incremented_ value!
actions.decrement()),
),
defer(() => {
const state = state$.value // this new "state" is now the _decremented_ value!
return empty()
}),
),
),
)
有很多方法可以从 observable 获取当前状态!
关于您示例中的以下代码行:
state$.value.apiState.apiClient.authenticate(state$.value.apiState.bearer)
首先,使用状态传递 API 客户端不是 common/recommended 模式。您可能需要查看 injecting the API client as a dependency 到 epics(这使单元测试更容易!)。其次,尚不清楚 API 客户端如何从状态中获取 current 后端 URL。 API 客户端是否可能正在使用状态的缓存版本?如果是,您可能需要重构 authenticate
方法并 传入 当前后端 URL.
这是一个处理错误并包含上述内容的示例:
/**
* Let's assume the state looks like the following:
* state: {
* apiState: {
* backend: "URL",
* bearer: "token"
* }
*/
// Note how the API client is injected as a dependency
export const authenticate = (action$, state$, { apiClient }) => action$.pipe(
ofType(actions.API_AUTHENTICATE),
withLatestFrom(state$),
mergeMap(([action, state]) =>
// Try to authenticate against the current backend URL
from(apiClient.authenticate(state.apiState.backend, state.apiState.bearer)).pipe(
// On success, dispatch an action to kick off the chained epic(s)
map(bearer => apiActions.authenticatedSuccess(bearer)),
// On failure, dispatch two actions:
// 1) an action that replaces the backend URL in the state
// 2) an action that restarts _this_ epic using the new/replaced backend URL
catchError(error$ => of(apiActions.authenticatedFailed(), apiActions.authenticate()),
),
),
)
export const authenticatedSuccess = (action$, state$) => action$.pipe(
ofType(actions.API_AUTHENTICATED_SUCCESS),
...
)
此外,在链接 epics 时请记住,concat
不会 等待链接的 epics 到 "finish".例如:
concat(
of(resourceActions.doLoadAResource()),
of(resourceActions.doLoadOtherResource()),
of(resourceActions.doLoadSomethingElse()))
)
如果这些 doLoadXXX
动作中的每一个都 "starts" 是一部史诗,那么这三个动作很可能会同时 运行。每个action都会一个接一个的派发,每个epic都会"start"一个接一个运行,而不用等到前一个"finish"。这是因为 epics 从未真正 完整 。他们是 long-lived,永无止境的流媒体。如果您想 doLoadOtherResource
到 运行 after doLoadAResource
,您将需要明确地等待一些标识何时完成的信号。