NGRX选择器使用filter操作符抛出source.lift is not a function异常
succeeded校验逻辑的问题 背景
我在使用NGRX与Angular 13的项目中,定义了如下状态:
export const initialState: SomethingState = { data: null, succeeded: false, message: "", errors: null, };
并创建了对应的特征选择器:
export const selectSomethingFeatureSelector = createFeatureSelector<SomethingState>("something");
每次API调用完成且无错误时,状态中的succeeded值会更新为true。
在使用该特征选择器的组件中,我都会校验succeeded是否为true(有时会搭配take(1)自动取消订阅),示例代码如下:
this.store.select(selectSomethingFeatureSelector) .pipe( filter((state) => state.succeeded), take(1), map((state) => { return state.data.items; }) );
由于所有状态都包含succeeded字段,这段逻辑重复出现,因此我希望将其迁移至选择器中。
最初我尝试了如下写法,但Observable仍会在succeeded为false时触发:
export const selectSomething = () => createSelector( selectSomethingFeatureSelector, (state: SomethingState) => { if(state.succeeded) { return state.data.items; } } );
之后我参考资料尝试了另一种写法,VS Code未提示错误,但控制台抛出异常:
TypeError: source.lift is not a function at filterOperatorFunction
写法如下:
export const selectSomething = () => createSelector( selectSomethingFeatureSelector, filter((state: SomethingState) => state.succeeded === true), (state: SomethingState) => { return state.data.items; } );
我的问题分为两部分:
- 将该逻辑迁移至选择器是否合理,还是应保留在组件中?
- 若选择器方案可行,我哪里出错了?
解答
问题1:逻辑迁移至选择器是否合理?
完全合理。NGRX选择器的核心作用之一就是复用状态派生逻辑,避免在多个组件中重复编写相同的状态转换、过滤逻辑。既然所有状态都有succeeded字段,把“校验succeeded为true后提取数据”的逻辑封装到选择器中,能大幅减少组件代码冗余,还能保证逻辑一致性。
不过要注意:take(1)属于Observable的订阅控制逻辑,更适合留在组件中——因为不同组件可能有不同的订阅需求(比如有的需要持续监听状态变化,而非只取一次)。
问题2:选择器写法的错误分析及正确实现
第一种写法的问题
你最初的写法中,选择器会始终返回值:当succeeded为false时,函数没有明确返回值,实际会返回undefined。而NGRX选择器只要源状态变化就会触发计算,不管返回的是undefined还是有效值,所以Observable还是会发出值,达不到过滤的效果。
第二种写法的问题
你错误地把RxJS的filter操作符直接放到了createSelector的参数里。createSelector的参数只能是其他选择器或者纯函数,不能是RxJS操作符——filter是用来处理Observable流的,不是处理状态快照的纯函数,所以才会抛出source.lift is not a function的错误。
正确实现方案
有两种可行的方式:
方式一:创建带过滤逻辑的高阶选择器
如果希望选择器只在succeeded为true时返回有效值,同时避免返回undefined,可以结合RxJS操作符封装一个高阶函数:
// 通用的过滤succeeded为true的高阶选择器 export const selectWhenSucceeded = <T>(selector: MemoizedSelector<object, T>) => { return (store: Store) => store.select(selector).pipe( filter(state => state.succeeded), map(state => state.data.items) ); }; // 针对SomethingState的具体选择器 export const selectSomethingData = selectWhenSucceeded(selectSomethingFeatureSelector); // 组件中使用 this.selectSomethingData(this.store).pipe(take(1)).subscribe(items => { // 处理items });
方式二:在选择器中返回可选值,组件中配合过滤
如果想严格使用NGRX的纯选择器,可以让选择器返回T | undefined,然后在组件中保留filter(但复用选择器已经减少了大部分重复逻辑):
export const selectSomethingData = createSelector( selectSomethingFeatureSelector, (state: SomethingState) => state.succeeded ? state.data.items : undefined ); // 组件中使用 this.store.select(selectSomethingData).pipe( filter(items => items !== undefined), take(1) ).subscribe(items => { // 处理items });
如果你的所有状态结构类似(都有succeeded和data),还可以创建一个通用的泛型选择器,进一步复用:
export const selectDataWhenSucceeded = <T, U>( featureSelector: MemoizedSelector<object, { succeeded: boolean; data: T }>, dataExtractor: (data: T) => U ) => { return createSelector( featureSelector, state => state.succeeded ? dataExtractor(state.data) : undefined ); }; // 针对SomethingState的使用 export const selectSomethingItems = selectDataWhenSucceeded( selectSomethingFeatureSelector, (data) => data.items );
内容的提问来源于stack exchange,提问作者Ben Thomson

