You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 10:31:18