Angular中从NgRx Reducer获取数据的问题排查
在Angular项目中使用NgRx管理应用状态,分别为管理员用户、所属企业、调研类型创建了多个Reducer。最初尝试多次调用StoreModule.forFeature时出错,无论指定哪个featureKey,系统始终尝试访问“管理员用户”的featureKey。改用ActionReducerMap映射Reducer后,数据成功存入Store,但出现多层嵌套的admins属性,尝试通过admin.admins访问时因数据模型不匹配报错。需要了解如何正确创建多个featureReducer,以及如何处理多层嵌套的数据访问。
相关代码示例
admin.actions.ts
import { createAction, props } from '@ngrx/store'; import { Admin } from "../../components/models"; export const GET_ADMIN = createAction('[ADMIN PAGE] Init'); export const GET_ADMINS_SUCCESS = createAction( '[ADMIN PAGE] Admins Success', props<{ admins: Admin[] }>() ); export const GET_ADMINS_ERROR = createAction( '[ADMIN PAGE] Admins Error', props<{ error: string }>() );
admin.effects.ts
import { Injectable } from "@angular/core"; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { catchError, map, of, exhaustMap } from 'rxjs'; import { AdminService } from '../../services/admin.service'; import { GET_ADMIN, GET_ADMINS_ERROR, GET_ADMINS_SUCCESS } from '../actions/admin.actions'; @Injectable() export class AdminsEffect { constructor( private actions$: Actions, private adminService: AdminService, ) {} loadAllAdmins$ = createEffect(() => this.actions$.pipe( ofType(GET_ADMIN), exhaustMap((initAction) => { return this.adminService.getAllAdmins().pipe( map(admins => GET_ADMINS_SUCCESS({admins})), catchError(() => of(GET_ADMINS_ERROR({error: "There was an error getting the admins"}))), ); }) ) ) };
admin.reducers.ts
import { Action, createReducer, on } from "@ngrx/store"; import { Admin } from "../../components/models/Admin"; import { GET_ADMINS_SUCCESS } from "../actions/admin.actions"; export interface AdminState { admins: Admin[]; } const initialAdminState: AdminState = { admins: [], }; export const adminReducer = createReducer( initialAdminState, on(GET_ADMINS_SUCCESS, (state, {admins}) => ({ ...state, admins, })) );
admin.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store'; import { adminFeatureKey } from '../admin.state'; import { AdminState } from '../reducers/admin.reducers'; export const selectAdmins = createFeatureSelector<AdminState>(adminFeatureKey); export const admins = createSelector( selectAdmins, (selectAdmins) => selectAdmins.admins )
admin.state.ts
import { ActionReducerMap } from '@ngrx/store'; import { AdminState, adminReducer } from './reducers/admin.reducers'; export interface AdminFlowState { admins: AdminState; } export const reducers: ActionReducerMap <AdminFlowState> = { admins: adminReducer } export const adminFeatureKey = "adminState"
admin.service.ts
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; import { CreateAdmin } from '../components/interfaces/CreateAdmin'; import { Admin } from "../components/models/Admin"; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AdminService { serverURL = environment.serverURL; constructor(private http: HttpClient) { } getAllAdmins(): Observable<Admin[]> { return this.http.get<Admin[]>(`${this.serverURL}/admins`) } ... }
Admin.ts
export interface Admin { code: string; name: string; fsbs: boolean; createdIp: string; createdAt: Date; updatedIp: string; updatedAt: Date; logo?: Blob; pwd?: string; }
admin.module.ts
... imports: [ CommonModule, MaterialModule, NgxChartsModule, AdminRoutingModule, StoreModule.forFeature(adminFeatureKey, reducers), EffectsModule.forFeature([AdminsEffect]), ReactiveFormsModule, FormsModule ], ...
admins-table.component.ts
import { Component, ViewChild } from '@angular/core'; import { MatPaginator } from '@angular/material/paginator'; import { MatTableDataSource } from '@angular/material/table'; import { MatSort } from '@angular/material/sort'; import { Admin } from "../models/Admin"; import { select, Store } from '@ngrx/store'; import { admins } from '../../store/selectors/admin.selectors'; import { GET_ADMIN } from '../../store/actions/admin.actions'; @Component({ selector: 'app-admins-table', templateUrl: '../views/admins-table.component.html', styleUrls: ['../styles/admins-table.component.scss'] }) export class AdminsTableComponent { columnas: string[] = ['code', 'name', 'fsbs', 'createdIp', 'createdAt', 'updatedIp', 'updatedAt', 'logo', 'edit']; admins: Admin[] = []; dataSource: any; @ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator; @ViewChild(MatSort, { static: true }) sort!: MatSort; constructor(private store: Store) {} ngOnInit() { this.store.pipe(select(admins)) .subscribe(admin => { console.log(admin); this.store.dispatch(GET_ADMIN()); this.admins = admin; this.dataSource = new MatTableDataSource<Admin>(this.admins); this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; }); } filter(event: Event) { const filtro = (event.target as HTMLInputElement).value; this.dataSource.filter = filtro.trim().toLowerCase(); } }
控制台输出
{ "admins": [ ... { "code": "002", "name": "Manuel", "pwd": "...", "fsbs": false, "createdIp": "157.100.75.247", "createdAt": "2022-09-07T23:08:08.310Z", "updatedIp": "157.100.75.247", "updatedAt": "2022-09-07T23:08:08.310Z", "logo": "..." }, ... ] }
解决方案
1. 修复多层嵌套数据的访问问题
当前出现{ admins: [...] }嵌套的核心原因是:AdminFlowState中定义了admins: AdminState,而AdminState本身又包含admins: Admin[]字段,导致选择器返回的数据结构与预期的Admin[]不匹配。
方案一:简化状态结构
修改admin.state.ts,移除不必要的嵌套层级,直接将adminReducer作为feature的reducer:
// admin.state.ts import { AdminState, adminReducer } from './reducers/admin.reducers'; export const adminFeatureKey = "adminState"; export const adminReducerMap = adminReducer;
然后在admin.module.ts中更新注册代码:
StoreModule.forFeature(adminFeatureKey, adminReducerMap),
方案二:调整选择器路径
如果需要保留当前的ActionReducerMap结构,修改选择器以匹配嵌套层级:
// admin.selectors.ts import { createFeatureSelector, createSelector } from '@ngrx/store'; import { adminFeatureKey, AdminFlowState } from '../admin.state'; export const selectAdminFlow = createFeatureSelector<AdminFlowState>(adminFeatureKey); export const selectAdminState = createSelector(selectAdminFlow, state => state.admins); export const admins = createSelector(selectAdminState, state => state.admins);
修改后,admins选择器将正确返回Admin[]类型的数据,组件订阅后不会再出现类型不匹配报错。
2. 正确创建多个Feature Reducer
为不同业务模块(管理员、企业、调研类型)创建独立的feature reducer,需遵循以下步骤:
- 每个模块独立创建状态文件、reducer、actions、selectors
- 在各自的feature模块中调用
StoreModule.forFeature(featureKey, reducer)注册,确保每个feature的featureKey唯一 - 各模块的effects通过
EffectsModule.forFeature独立注册
以企业模块为例:
- 创建
enterprise.state.ts:
import { EnterpriseState, enterpriseReducer } from './reducers/enterprise.reducers'; export const enterpriseFeatureKey = "enterpriseState";
- 在
enterprise.module.ts中注册:
imports: [ ... StoreModule.forFeature(enterpriseFeatureKey, enterpriseReducer), EffectsModule.forFeature([EnterpriseEffects]), ... ]
若需在同一模块中注册多个feature,可通过ActionReducerMap批量注册,但需保证每个key对应独立的reducer:
StoreModule.forFeature('businessModules', { adminState: adminReducer, enterpriseState: enterpriseReducer, surveyTypeState: surveyTypeReducer })
更推荐的做法是每个feature模块独立注册自身的reducer,保持代码模块化和解耦。
内容的提问来源于stack exchange,提问作者Manuel Pereira Maldonado

