NgRx:调整子状态属性以支持Datatable组件复用优化
我之前也帮很多团队解决过类似的NgRx下通用组件复用的问题,你的痛点确实很典型——重复配置不仅浪费时间,还容易导致各个实例行为不一致。下面是一套完整的解决方案,把Datatable封装成带NgRx状态的独立Feature Module,实现核心行为的统一维护:
1. 创建Datatable Feature Module(核心状态容器)
首先我们要把表格的核心状态、Reducer、Actions都封装到独立的Feature Module中,这样所有复用实例共享同一套状态处理逻辑。
1.1 定义状态结构
创建datatable.state.ts,定义表格的核心状态(分页、排序、筛选、选中行等),同时支持多实例隔离(通过tableId区分不同表格实例):
import { EntityState, createEntityAdapter } from '@ngrx/entity'; // 单个表格实例的状态 export interface DatatableInstanceState { tableId: string; pageIndex: number; pageSize: number; sortColumn: string; sortDirection: 'asc' | 'desc'; filterText: string; selectedRows: string[]; } // 所有表格实例的实体状态 export interface DatatableState extends EntityState<DatatableInstanceState> {} // 创建实体适配器,方便管理多实例状态 export const datatableAdapter = createEntityAdapter<DatatableInstanceState>({ selectId: (instance) => instance.tableId, }); // 初始状态 export const initialDatatableState: DatatableState = datatableAdapter.getInitialState();
1.2 定义Actions
创建datatable.actions.ts,定义所有核心行为对应的Action,比如分页切换、排序、筛选:
import { createAction, props } from '@ngrx/store'; import { DatatableInstanceState } from './datatable.state'; // 初始化表格实例状态 export const initDatatable = createAction( '[Datatable] Init Instance', props<{ tableId: string; initialState?: Partial<DatatableInstanceState> }>() ); // 切换分页 export const changePage = createAction( '[Datatable] Change Page', props<{ tableId: string; pageIndex: number }>() ); // 修改排序 export const updateSort = createAction( '[Datatable] Update Sort', props<{ tableId: string; column: string; direction: 'asc' | 'desc' }>() ); // 更多核心行为Action(比如筛选、选中行)可以在这里扩展
1.3 编写Reducer
创建datatable.reducer.ts,统一处理所有核心行为的状态变更——这里是核心逻辑的统一维护点,修改这里所有实例的行为都会同步更新:
import { createReducer, on } from '@ngrx/store'; import { initialDatatableState, datatableAdapter } from './datatable.state'; import { initDatatable, changePage, updateSort } from './datatable.actions'; export const datatableReducer = createReducer( initialDatatableState, on(initDatatable, (state, { tableId, initialState }) => { return datatableAdapter.upsertOne( { tableId, pageIndex: 0, pageSize: 10, sortColumn: '', sortDirection: 'asc', filterText: '', selectedRows: [], ...initialState, }, state ); }), on(changePage, (state, { tableId, pageIndex }) => { return datatableAdapter.updateOne( { id: tableId, changes: { pageIndex } }, state ); }), on(updateSort, (state, { tableId, column, direction }) => { return datatableAdapter.updateOne( { id: tableId, changes: { sortColumn: column, sortDirection: direction } }, state ); }) );
1.4 封装Feature Module
创建datatable.module.ts,把状态、组件、服务都封装进来,对外暴露必要的API:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { StoreModule } from '@ngrx/store'; import { datatableReducer } from './datatable.reducer'; import { DatatableComponent } from './datatable.component'; // 导入表格依赖的UI组件(比如分页器、排序按钮等) import { MatTableModule } from '@angular/material/table'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatSortModule } from '@angular/material/sort'; @NgModule({ declarations: [DatatableComponent], imports: [ CommonModule, MatTableModule, MatPaginatorModule, MatSortModule, // 注册Datatable的Feature State StoreModule.forFeature('datatable', datatableReducer), ], exports: [DatatableComponent] // 对外暴露组件,方便业务模块使用 }) export class DatatableModule {}
2. 封装有状态的Datatable组件
接下来编写datatable.component.ts,组件内部通过NgRx Store来管理状态,不再让业务组件处理状态逻辑:
import { Component, Input, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { select } from '@ngrx/store'; import { Observable } from 'rxjs'; import { DatatableInstanceState } from './datatable.state'; import { initDatatable, changePage, updateSort } from './datatable.actions'; import { datatableAdapter } from './datatable.state'; @Component({ selector: 'app-datatable', templateUrl: './datatable.component.html', styleUrls: ['./datatable.component.css'] }) export class DatatableComponent implements OnInit { @Input() tableId!: string; // 每个实例唯一标识,用于隔离状态 @Input() data!: any[]; // 业务数据由父组件传入 @Input() displayedColumns!: string[]; // 列配置由父组件传入 // 当前表格实例的状态Observable tableState$!: Observable<DatatableInstanceState | undefined>; constructor(private store: Store) {} ngOnInit(): void { // 初始化当前表格实例的状态 this.store.dispatch(initDatatable({ tableId: this.tableId })); // 订阅当前表格实例的状态 this.tableState$ = this.store.pipe( select(datatableAdapter.getSelectors().selectEntity(this.tableId)) ); } // 分页变化时触发Action onPageChange(pageIndex: number): void { this.store.dispatch(changePage({ tableId: this.tableId, pageIndex })); } // 排序变化时触发Action onSortChange(column: string, direction: 'asc' | 'desc'): void { this.store.dispatch(updateSort({ tableId: this.tableId, column, direction })); } // 更多核心行为方法(比如筛选、批量操作)在这里统一实现 }
对应的模板datatable.component.html示例:
<div class="datatable-container"> <table mat-table [dataSource]="data" matSort (matSortChange)="onSortChange($active, $direction)"> <!-- 列渲染逻辑,根据displayedColumns动态生成 --> <ng-container *ngFor="let column of displayedColumns" [matColumnDef]="column"> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{ column }}</th> <td mat-cell *matCellDef="let element">{{ element[column] }}</td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table> <mat-paginator [pageIndex]="(tableState$ | async)?.pageIndex || 0" [pageSize]="(tableState$ | async)?.pageSize || 10" (page)="onPageChange($event.pageIndex)" showFirstLastButtons ></mat-paginator> </div>
3. 在业务模块中复用
现在任何业务模块只需要导入DatatableModule,就可以直接使用带状态的表格组件,无需重复配置核心行为:
// 比如用户模块user.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DatatableModule } from '../datatable/datatable.module'; import { UserListComponent } from './user-list.component'; @NgModule({ declarations: [UserListComponent], imports: [CommonModule, DatatableModule] }) export class UserModule {}
在业务组件中使用:
<!-- user-list.component.html --> <app-datatable tableId="user-table" [data]="users$ | async" [displayedColumns]="['id', 'name', 'email']" ></app-datatable>
4. 统一维护核心行为
当你需要给所有表格实例新增核心功能时,只需要在DatatableModule内修改:
- 比如新增“批量删除”功能:
- 在
datatable.actions.ts中新增deleteSelectedRowsAction - 在
datatable.reducer.ts中处理该Action的状态变更 - 在
datatable.component.ts中添加批量删除的UI和方法 - 所有复用的表格实例会自动获得这个功能,无需在业务模块中做任何修改
- 在
关键注意点
- 状态隔离:通过
tableId区分不同表格实例的状态,避免互相干扰 - 业务扩展:如果某个业务表格需要自定义行为,可以通过
@Input、@Output或者让业务模块扩展NgRx Selectors来实现,但核心逻辑仍统一维护 - 性能优化:使用NgRx的
createSelector缓存状态选择结果,避免不必要的重渲染
内容的提问来源于stack exchange,提问作者ZackDeRose

