ngRx(Angular)如何复用实体数据 避免重复API调用实现搜索过滤
问题结论
你不需要也不能把selector直接放到reducer中使用,你现在的实现问题本质是混淆了ngRx中「原始状态存储」和「派生状态计算」的职责边界,才会导致每次输入都要重复调用API。
现有代码的核心问题
你当前的逻辑有两个明显的设计偏差:
- 把过滤后的电影列表这种「派生数据」直接存在store中,还通过
setAll覆盖了原始的全量电影数据,导致本地缓存的全量数据丢失,下次搜索只能重新请求API - 把本该在selector层完成的派生计算放到了reducer中,违背了reducer只负责更新原始状态、不处理业务派生逻辑的设计原则
正确实现方案
你完全可以用自己写的过滤逻辑实现无API调用的本地搜索,只需要调整状态结构和职责划分:
- 调整状态设计,store中只存两类原始数据:全量电影列表、当前用户输入的搜索关键词
- 过滤逻辑完全交给selector实现,基于已存的全量数据和关键词实时计算过滤结果,利用ngRx selector的记忆化能力保证性能
- 组件只在首次加载全量数据时调用一次API,后续输入变化时只更新store中的关键词,不需要重复请求
第一步:调整Action定义
去掉携带电影数据的过滤action,新增单独更新搜索关键词的action,全量数据拉取的逻辑只在首次加载时触发:
// 搜索关键词更新action,仅携带输入的查询词 export const updateSearchQuery = createAction( '[Movies Page] Update Search Query', props<{ query: string }>() ) // 全量电影数据拉取完成action,仅在首次加载/主动刷新时触发 export const retrievedMovieList = createAction( '[Movies API] Retrieved Movie List', props<{ movies: Movie[] }>() )
第二步:调整Reducer逻辑
Reducer只负责更新原始状态,不做任何过滤计算,注意不要覆盖已存的全量电影数据:
export const moviesReducer = createReducer<PopularMovieState>( // 初始状态新增searchQuery字段,默认空字符串 { ...initialState, searchQuery: '' }, on(retrievedMovieList, (state, { movies }) => movieAdapter.addMany(movies, state) ), // 仅更新搜索关键词,全量电影数据保持不变 on(updateSearchQuery, (state, { query }) => ({ ...state, searchQuery: query })) )
第三步:优化Selector实现
不要写动态传参生成selector的形式,直接组合状态中的搜索关键词和全量电影列表计算过滤结果,自动享受记忆化缓存:
export const moviesFeatureSelector = createFeatureSelector<PopularMovieState>('movies'); const { selectEntities, selectAll } = movieAdapter.getSelectors(); export const selectMoviesEntities = createSelector( moviesFeatureSelector, selectEntities ); export const selectMovies = createSelector(moviesFeatureSelector, selectAll); export const selectMovie = createSelector( selectMoviesEntities, selectRouteParams, (movies, { movieId }) => movies[movieId] ); // 新增搜索关键词选择器 export const selectSearchQuery = createSelector( moviesFeatureSelector, state => state.searchQuery ) // 调整过滤选择器,直接基于store内的状态计算,无需外部传参 export const selectFilteredMovies = createSelector( selectMovies, selectSearchQuery, (movies, searchQuery) => { const normalizedQuery = searchQuery.trim().toLowerCase() if (!normalizedQuery) return movies return movies.filter(movie => movie.title.toLowerCase().includes(normalizedQuery) ) } );
第四步:调整组件逻辑
组件仅在初始化时拉取一次全量数据,输入变化时只派发关键词更新action,直接订阅过滤后的选择器结果即可:
export class SearchComponent implements OnInit { // 直接订阅过滤后的电影流,输入变化时会自动更新 filteredMovies$ = this.store.select(selectFilteredMovies); constructor(private moviesService: MoviesService, private store: Store) {} ngOnInit() { // 首次进入页面拉取全量电影数据,存入store后后续搜索无需重复请求 this.moviesService .getMovies() .subscribe((movies) => this.store.dispatch(retrievedMovieList({ movies })) ); } searchFieldChanged(query: string) { // 输入变化仅更新搜索关键词,不触发API调用 this.store.dispatch(updateSearchQuery({ query })) } }
为什么不能在Reducer中使用Selector
Reducer是纯函数,执行时机是action派发之后、新状态生成之前,它的输入只有旧状态和action参数;而Selector的执行时机是新状态生成完成之后,用来基于最新状态计算派生值,二者执行时机完全错开。如果强行在Reducer中调用Selector,不仅拿不到最新的状态,还会破坏Reducer的纯函数特性,引入难以排查的状态不一致问题。所有派生值的计算都应该放在Selector层完成,不要冗余存储到Store中。
内容的提问来源于stack exchange,提问作者Guillermo Díaz Hernández
相关产品推荐
相关产品推荐

