Angular 11与NGRX 11 同Store下按类型隔离分页用户列表实现
方案结论
不需要为两类用户拆分独立Store,最优实现是在现有用户Store内做按用户类型的状态分区隔离,复用现有NGRX实体适配器逻辑,兼顾数据隔离、分页状态独立、低维护成本的要求,完全适配Angular 11 + NGRX 11的版本特性。
为什么不推荐拆分独立Store
- 两类数据本质都是
User实体,拆分独立Store会重复定义实体适配器、Reducer、Effect、通用CRUD逻辑,后续如果新增用户类型(如平台运营、服务商),需要重复拷贝整套逻辑,维护冗余度极高 - 现有全局Store本身支持嵌套分片状态,无需为了隔离单独拆分Feature模块
具体改造步骤(完全基于现有代码结构调整)
1. 调整全局State结构
把原本扁平的单列表状态,改为按用户类型做键的字典结构,每个字典值独立持有自己的实体数据、分页信息、加载状态,天然实现隔离:
// 定义用户类型枚举,避免硬编码字符串 export type UserType = 'seller' | 'buyer'; // 单个用户分类的状态(复用原有字段定义) export interface UserListState extends EntityState<User> { isLoading: boolean; totalElements: number; totalPages: number; empty: boolean; page: number; filters: UserFilter; } // 全局UserState 结构调整 export interface UserState { lists: Record<UserType, UserListState>; }
2. 初始化状态与实体适配器
保留原有实体适配器的配置,为每个用户类型的列表初始化独立的空状态:
// 复用原有实体适配器配置 export const userListAdapter: EntityAdapter<User> = createEntityAdapter<User>({ selectId: user => user.userId }); // 生成单个列表的初始状态 const getInitialListState = (): UserListState => userListAdapter.getInitialState({ isLoading: false, totalElements: null, totalPages: null, page: 0, empty: true, filters: {} as UserFilter }); // 全局初始状态,为两类用户分别初始化独立空间 export const initialState: UserState = { lists: { seller: getInitialListState(), buyer: getInitialListState() } };
3. 调整Action定义
在原有Action基础上补充userType标识,让Reducer可以精准定位需要更新的状态分区,不需要新增重复的Action:
export const loadUsers = createAction( '[Users List] Load Users via Service', props<{ filters: UserFilter, userType: UserType }>() ); export const usersLoadedDone = createAction( '[Users Effect] Users Loaded Successfully', props<{ users: Page<User>, userType: UserType, filters: UserFilter }>() ); // 其余原有CRUD Action保持不变,后续按需在参数中补充userType即可
4. 调整Reducer更新逻辑
所有状态更新操作都只定位到对应userType的分区内修改,完全不影响另一个分区的状态、分页、缓存:
export const userReducer = createReducer( initialState, // 加载状态更新:仅修改目标列表的loading标识 on(loadUsers, (state, { filters, userType }) => ({ ...state, lists: { ...state.lists, [userType]: { ...state.lists[userType], isLoading: true, filters } } })), // 加载成功:仅替换目标列表的实体与分页数据 on(usersLoadedDone, (state, { users, userType, filters }) => { const targetList = state.lists[userType]; const updatedList = userListAdapter.setAll(users.content, { ...targetList, isLoading: false, totalElements: users.totalElements, totalPages: users.totalPages, empty: users.empty, page: users.number, filters }); return { ...state, lists: { ...state.lists, [userType]: updatedList } }; }) );
5. 调整Selector逻辑
基于NGRX的参数化选择器,为每个用户类型生成独立的Selector,组件可以直接选取对应列表的数据、加载状态、分页信息:
export const userFeatureSelector = createFeatureSelector<UserState>('users'); // 通用工厂方法:根据传入的userType返回对应列表的根状态 export const getUserListState = (userType: UserType) => createSelector( userFeatureSelector, state => state.lists[userType] ); // 卖家列表的独立选择器 export const selectSellerUsers = createSelector( getUserListState('seller'), userListAdapter.getSelectors().selectAll ); export const selectSellerLoading = createSelector( getUserListState('seller'), state => state.isLoading ); export const selectSellerPagination = createSelector( getUserListState('seller'), state => ({ totalElements: state.totalElements, totalPages: state.totalPages, page: state.page }) ); // 买家列表的独立选择器 export const selectBuyerUsers = createSelector( getUserListState('buyer'), userListAdapter.getSelectors().selectAll ); export const selectBuyerLoading = createSelector( getUserListState('buyer'), state => state.isLoading ); // 其余通用CRUD逻辑的选择器可按同样模式扩展
6. Effect层适配
原有Effect逻辑仅需在请求时携带userType对应的接口参数,返回时把userType透传给usersLoadedDone Action即可:
loadUsers$ = createEffect(() => this.actions$.pipe( ofType(loadUsers), switchMap(({ filters, userType }) => // 透传参数请求对应类型的用户分页数据 this.userService.getUsers({...filters, type: userType}).pipe( map(pageData => usersLoadedDone({ users: pageData, userType, filters })) ) ) ) );
方案优势
- 改动成本极低:仅在原有状态结构外层加一层按类型的字典嵌套,原有实体适配器、通用CRUD逻辑完全复用
- 天然数据隔离:两类用户的实体、分页参数、加载状态、缓存数据完全独立存储,不会出现读写串数据的问题
- 可扩展性强:后续如果需要新增其他类型的用户下拉,只需要在
UserType枚举中新增值,在初始状态的lists中补充对应键值即可,不需要新增整套Store逻辑 - 完全兼容现有分页缓存逻辑:每个列表独立持有当前分页的数据缓存,符合“仅缓存当前请求分页数据”的业务要求
补充说明:现有代码中存在一处小的字段不匹配问题:State定义中字段为
isLoading,但Reducer中赋值使用的是isLoadingList,改造时可一并修正,避免状态读取异常。
内容的提问来源于stack exchange,提问作者Steven Dieu
相关产品推荐
相关产品推荐

