如何在NgRx Effects中正确访问Store状态数据
问题根源
你没法正确拿到角色值的核心原因有3个:
- 参数解构遗漏:你在
exhaustMap的入参只解构了[payload],完全没有接收withLatestFrom输出的第二个参数(也就是路由里的账户角色),自然拿不到动态值 - 操作符组合有风险:你把
withLatestFrom放在exhaustMap外层,而路由状态是应用启动后异步初始化的,如果用户在路由状态完成首次赋值前触发登录动作,整个流会直接卡住,不会执行后续登录请求 - 选择器(selector)没有空值保护和兜底返回:路由未初始化、当前路由没有role参数时,你的
getAccountRole会返回undefined,很容易引发后续逻辑报错
可直接运行的实现方案
这里给两种常用的正确写法,推荐第一种,稳定性更高。
写法1:用take(1)取状态快照(推荐)
这种写法会在登录动作触发的瞬间,主动取一次Store里的最新角色值,不需要等待选择器首次发值,完全不会出现流卡住的问题。
首先调整Effects代码,记得补充导入take操作符:
import { Injectable } from "@angular/core"; import { map, exhaustMap, take } from 'rxjs/operators'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { AuthService } from './auth.service'; import * as fromRoot from '@app/store'; import * as fromActions from './auth.actions'; import * as fromRouter from '@app/store/router'; import { Store } from "@ngrx/store"; @Injectable() export class AuthEffects { constructor( private authService: AuthService, private actions$: Actions, private store: Store<fromRoot.State>, ){} signIn$ = createEffect(() => this.actions$.pipe( ofType(fromActions.Types.LOG_IN), map((action: fromActions.LogIn) => action.payload), exhaustMap((payload) => { // 先拿点击登录瞬间的最新账户角色 return this.store.select(fromRouter.getAccountRole).pipe( take(1), // 只取一次值后自动完成,不会造成内存泄漏 exhaustMap((accountRole) => { // 再发起登录请求 return this.authService.logIn(payload.email, payload.password).pipe( map((user) => { return new fromActions.LogInSuccess({ token: user.token, type: accountRole }) }) ) }) ) }) ) ) }
然后优化selector,加上空值保护和兜底返回:
import { createFeatureSelector, createSelector } from "@ngrx/store"; import { RouterReducerState } from '@ngrx/router-store'; import { RouterStateUrl } from "./router.serializer"; export const getRouterState = createFeatureSelector<RouterReducerState<RouterStateUrl>>('router'); export const getCurrentRoute = createSelector( getRouterState, (router) => router?.state // 可选链避免路由未初始化时报错 ) export const getAccountRole = createSelector ( getCurrentRoute, (route?: RouterStateUrl) => { if(route?.params?.['role']) { return route.params['role']; } // 兜底返回默认角色,和业务默认值保持一致即可 return 'advertiser'; } )
写法2:保留withLatestFrom的写法
如果你习惯用withLatestFrom的组合逻辑,只要保证selector有兜底返回、正确解构参数即可,代码更简洁:
// 记得补充导入withLatestFrom import { map, exhaustMap, withLatestFrom } from 'rxjs/operators'; signIn$ = createEffect(() => this.actions$.pipe( ofType(fromActions.Types.LOG_IN), map((action: fromActions.LogIn) => action.payload), // 因为selector有兜底,订阅后会立刻返回值,不会卡住流 withLatestFrom(this.store.select(fromRouter.getAccountRole)), // 这里要把第二个参数accountRole解构出来 exhaustMap(([payload, accountRole]) => { return this.authService.logIn(payload.email, payload.password).pipe( map((user) => { return new fromActions.LogInSuccess({ token: user.token, type: accountRole }) }) ) }) ) )
注意事项
- 不要在高阶映射操作符(
exhaustMap/mergeMap/switchMap)外层直接使用withLatestFrom读取异步初始化的状态值,除非你能保证对应的selector在应用启动时就会立刻返回值,否则很容易出现首次触发动作无响应的问题 withLatestFrom会按照传入顺序把所有流的最新值拼成数组传给下游,解构参数时不要遗漏你需要的数值- 所有自定义selector尽量添加空值保护和兜底返回,避免应用初始化阶段状态未生成时抛出JS异常
内容的提问来源于stack exchange,提问作者orphen92300
相关产品推荐
相关产品推荐

