Angular5+NgRx:Effect中withLatestFrom未触发Action却调用Selector报错
为什么选择器会在Action未触发时执行?
这其实是withLatestFrom操作符和ngrx选择器的响应式特性共同导致的:
withLatestFrom的订阅时机:当你的Effect初始化时(比如应用启动、page1加载时),withLatestFrom会立即订阅所有传入的Observable,包括this.store.select(fromReducers.getItemsList)。这意味着不管ActionTypes.ChangesInItems有没有触发,只要getItemsList依赖的状态切片(allItems或currentItemId)发生变化,选择器就会重新计算。- 选择器的响应式计算:
createSelector创建的是memoized选择器,但只要它的输入选择器(fromAnotherReducer.getAllItems、getCurrentItemId)返回的状态有更新,它就会重新运行计算逻辑。在page1中,allItems是空的,但currentItemId可能有值(比如从其他状态继承过来的),这时候allItems[itemId]就是undefined,访问subItems自然会抛出错误。
如何解决这个问题?
你有几种方案可以避免在每个选择器里都加校验,同时保证Effect只在有效状态下执行:
1. 在Effect中添加过滤逻辑
在withLatestFrom之后加上filter操作符,确保拿到的itemsList是有效的,再继续执行后续逻辑:
@Effect() updateAfterChanges$ = this.actions$.pipe( ofType(ActionTypes.ChangesInItems), withLatestFrom(this.store.select(fromReducers.getItemsList)), // 过滤掉无效的itemsList filter(([action, itemsList]) => !!itemsList && itemsList.length > 0), switchMap(([action, itemsList]: [ActionType, Items[]]) => { return [new UpdateItems()] }) )
这样即使选择器返回空数组或undefined,Effect也不会继续执行,避免错误。
2. 优化选择器的健壮性(推荐)
虽然你不想每个选择器都加校验,但让选择器处理空状态其实是最佳实践——因为选择器是状态到视图/逻辑的转换层,应该保证无论状态是什么样的,都能返回合法值。你可以优化你的选择器逻辑,覆盖更多边缘情况:
export const getItemsList = createSelector( fromAnotherReducer.getAllItems, getCurrentItemId, (allItems, itemId) => { // 校验allItems是否为空、itemId是否存在、对应的item是否有subItems if (!allItems || Object.keys(allItems).length === 0 || !itemId || !allItems[itemId] || !allItems[itemId].subItems) { return []; } return collectItems(allItems, allItems[itemId].subItems); } );
这样不管状态处于什么阶段,选择器都能返回一个安全的默认值(空数组),不会抛出错误。
3. 确保状态的初始值合法
在对应的reducer里设置合理的初始状态,从根源上避免undefined的情况:
// fromAnotherReducer的初始状态 const initialAnotherState = { allItems: {}, // 初始化为空对象,而不是undefined // ...其他状态 }; // 管理currentItemId的reducer初始状态 const initialItemState = { currentItemId: null, // 初始化为null,而不是某个可能不存在的ID // ...其他状态 };
初始状态合理的话,选择器计算时就不会遇到allItems[itemId]为undefined的情况(因为itemId初始是null,你可以在选择器里提前判断)。
4. 调整状态获取的时机
如果想彻底避免Effect初始化时订阅选择器,可以把状态获取移到switchMap内部,只有当Action触发时才去获取最新的状态值:
@Effect() updateAfterChanges$ = this.actions$.pipe( ofType(ActionTypes.ChangesInItems), switchMap(action => this.store.select(fromReducers.getItemsList).pipe( take(1), // 只获取当前最新的状态值一次 filter(itemsList => !!itemsList && itemsList.length > 0) ) ), switchMap(itemsList => { return [new UpdateItems()] }) )
这种方式下,只有当ActionTypes.ChangesInItems触发时,才会去调用选择器获取状态,避免了page1加载时的无效计算。
总结
本质问题是withLatestFrom会在Effect初始化时就订阅选择器,导致选择器在状态未准备好时执行。你可以根据自己的项目情况选择合适的方案:如果想最小化修改,就在Effect里加过滤;如果想从根源解决,就优化选择器或初始状态。
内容的提问来源于stack exchange,提问作者mr__brainwash

