NgRx/Redux通用Reducer优化:减少电影列表页面重复代码
用NgRx实现通用电影列表Reducer的方案
这个场景太常见了——重复写几乎一样的Reducer简直是浪费时间!NgRx其实提供了很多方式来实现通用化,我给你捋一个最实用的方案,核心是用单一Reducer管理多页面状态+带标识的通用Actions,完全避免重复代码:
1. 先定义通用状态结构
首先统一所有列表页面的状态格式,然后用一个对象来存储不同页面的状态(key是页面类型,比如search/actor/category):
// 单个列表页面的状态 export interface MovieListPageState { ids: number[]; // 待展示的电影ID列表 loading: boolean; error: string | null; } // 所有列表页面的根状态(整合到你的全局State里) export interface MovieListsState { [pageType: string]: MovieListPageState; } // 初始状态:空对象,每个页面第一次加载时自动初始化 export const initialMovieListsState: MovieListsState = {};
2. 定义带页面标识的通用Actions
所有列表页面的操作(加载请求/成功/失败)用一套Action,通过pageType字段区分是哪个页面的操作:
import { createAction, props } from '@ngrx/store'; // 加载请求:携带页面类型和查询参数(比如搜索关键词、演员ID、分类ID) export const loadMovieList = createAction( '[Movie List] Load Request', props<{ pageType: string; params: any }>() ); // 加载成功:携带页面类型和返回的电影ID列表 export const loadMovieListSuccess = createAction( '[Movie List] Load Success', props<{ pageType: string; ids: number[] }>() ); // 加载失败:携带页面类型和错误信息 export const loadMovieListFailure = createAction( '[Movie List] Load Failure', props<{ pageType: string; error: string }>() );
3. 编写通用Reducer
只需要一个Reducer就能处理所有页面的状态更新,根据Action里的pageType找到对应的页面状态分支进行操作:
import { Action, createReducer, on } from '@ngrx/store'; import { initialMovieListsState } from './movie-lists.state'; import { loadMovieList, loadMovieListSuccess, loadMovieListFailure } from './movie-lists.actions'; const movieListsReducer = createReducer( initialMovieListsState, // 处理加载请求:初始化该页面状态(如果还没存在)并设置loading为true on(loadMovieList, (state, { pageType }) => ({ ...state, [pageType]: { ...state[pageType] || { ids: [], loading: false, error: null }, loading: true, error: null } })), // 处理加载成功:更新该页面的ID列表,关闭loading on(loadMovieListSuccess, (state, { pageType, ids }) => ({ ...state, [pageType]: { ...state[pageType], ids, loading: false } })), // 处理加载失败:设置错误信息,关闭loading on(loadMovieListFailure, (state, { pageType, error }) => ({ ...state, [pageType]: { ...state[pageType], error, loading: false } })) ); export function reducer(state: MovieListsState | undefined, action: Action) { return movieListsReducer(state, action); }
4. 创建页面专属的选择器
为了让组件方便获取对应页面的状态,创建通用的选择器工厂,或者直接为每个页面写专属选择器:
import { createFeatureSelector, createSelector } from '@ngrx/store'; import { MovieListsState } from './movie-lists.state'; // 获取所有列表页面的根状态 const selectMovieListsState = createFeatureSelector<MovieListsState>('movieLists'); // 通用选择器工厂:传入pageType,返回该页面的状态选择器 export const selectMovieListPageState = (pageType: string) => createSelector( selectMovieListsState, (state) => state[pageType] || { ids: [], loading: false, error: null } ); // 也可以为每个页面单独创建选择器(更直观) export const selectSearchMovieList = selectMovieListPageState('search'); export const selectActorMovieList = selectMovieListPageState('actor'); export const selectCategoryMovieList = selectMovieListPageState('category'); // 进一步封装:获取某个页面的电影ID列表 export const selectSearchMovieIds = createSelector( selectSearchMovieList, (state) => state.ids );
5. 通用Effects处理(可选但推荐)
同样,Effects也可以通用化,监听loadMovieList Action,根据pageType和params调用对应的服务逻辑:
import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, map, mergeMap } from 'rxjs/operators'; import { MovieService } from '../services/movie.service'; import { loadMovieList, loadMovieListSuccess, loadMovieListFailure } from './movie-lists.actions'; @Injectable() export class MovieListsEffects { loadMovieList$ = createEffect(() => this.actions$.pipe( ofType(loadMovieList), mergeMap(({ pageType, params }) => { // 根据pageType调用对应的服务方法 let movieObservable$; switch (pageType) { case 'search': movieObservable$ = this.movieService.searchMovies(params.keyword); break; case 'actor': movieObservable$ = this.movieService.getMoviesByActor(params.actorId); break; case 'category': movieObservable$ = this.movieService.getMoviesByCategory(params.categoryId); break; default: return of([]); } return movieObservable$.pipe( map(movies => loadMovieListSuccess({ pageType, ids: movies.map(m => m.id) })), catchError(error => of(loadMovieListFailure({ pageType, error: error.message }))) ); }) ) ); constructor(private actions$: Actions, private movieService: MovieService) {} }
组件中使用示例
在搜索页面组件里,只需要分发带pageType: 'search'的Action,并用对应的选择器获取状态:
import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { loadMovieList } from '../store/movie-lists.actions'; import { selectSearchMovieIds, selectSearchMovieList } from '../store/movie-lists.selectors'; @Component({ selector: 'app-search-page', templateUrl: './search-page.component.html' }) export class SearchPageComponent implements OnInit { movieIds$ = this.store.select(selectSearchMovieIds); loading$ = this.store.select(selectSearchMovieList).pipe(map(state => state.loading)); error$ = this.store.select(selectSearchMovieList).pipe(map(state => state.error)); constructor(private store: Store) {} ngOnInit(): void { // 初始加载搜索结果(比如默认关键词) this.store.dispatch(loadMovieList({ pageType: 'search', params: { keyword: 'action' } })); } onSearch(keyword: string): void { this.store.dispatch(loadMovieList({ pageType: 'search', params: { keyword } })); } }
这个方案的好处是:
- 完全避免重复代码,新增页面只需要加个
pageType标识,不用写新的Reducer/Actions - 状态结构统一,维护成本低
- 扩展灵活,后续如果要加新的列表页面(比如导演页),只需要在Effects里加个case,组件里用对应的选择器就行
内容的提问来源于stack exchange,提问作者Daniil Andreyevich Baunov
相关产品推荐
相关产品推荐

