如何在NGRX中更新一对多关联数据?
NgRx中一对多模型关联关系同步更新的解决方案
问题背景
我有两个存在一对多关系的模型:
export interface GroupModel { id: number; name: string; userIds?: number[]; } export interface UserModel { id: number; name: string; groupId?: number; }
当前的问题是:使用NgRx Effect更新任意一个模型时,对应的关联关系不会自动同步更新。
现有Effect实现
groups.effects.ts
updateGroup$ = createEffect(() => this.actions$.pipe( ofType(GroupsActions.updateGroup), concatMap(({ group }) => this.groupsService.update(group).pipe( map(() => GroupsAPIActions.groupUpdatedSuccess({ update: { id: group.id, changes: group }, }) ), catchError((error) => of(GroupsAPIActions.groupUpdatedFail({ message: error })) ) ) ) ) );
users.effects.ts
updateUser$ = createEffect(() => this.actions$.pipe( ofType(UsersActions.updateUser), concatMap(({ user }) => this.usersService.update(user).pipe( map(() => UsersAPIActions.userUpdatedSuccess({ update: { id: user.id, changes: user }, }) ), catchError((error) => of(UsersAPIActions.userUpdatedFail({ message: error })) ) ) ) ) );
现有Reducer实现
on( GroupsActions.groupStation, (state) => ({ ...state, loading: true, errorMessage: '', }) ), on(GroupsAPIActions.groupUpdatedSuccess, (state, { update }) => groupsAdapter.updateOne(update, { ...state, loading: false, }) ), ( GroupsAPIActions.groupUpdatedFail, (state, { message }) => ({ ...state, loading: false, errorMessage: message, }) )
需求场景示例
现有初始数据:
groups: [ { id: 1, name: "Group 1", userIds: [1, 2] } ]; users: [ { id: 1, name: "User 1", groupId: 1 }, { id: 2, name: "User 2", groupId: 1 } ];
当执行以下dispatch操作将用户2移出组时:
this.store.dispatch(UserActions.updateUser({ user: { id: 2, name: "User 2", groupId: undefined } }));
希望Group 1的userIds数组同步移除用户2的ID,变为:
{ id: 1, name: "Group 1", userIds: [1] }
当前痛点
我已经手动在groupsReducer中编写了关联同步代码,但代码量过大,对于这种简单的同步需求来说不够简洁:
on(UsersAPIActions.userUpdatedSuccess, (state, { update }) => { const userId = +update.id; const newGroupId = update.changes.groupId; const allGroups = selectAll(state); // 查找包含该用户ID的组 const groupToUpdate = allGroups.find((group: GroupModel) => group.userIds.includes(userId) ); const updateGroupChanges = {} as Partial<GroupModel>; // 如果找到用户所属的旧组 if (groupToUpdate) { // 更新后的用户有新组 if (newGroupId) { // 情况A:新旧组相同,无需操作 if (groupToUpdate.id === newGroupId) return state; // 情况B:新旧组不同,从旧组移除用户,添加到新组 const oldGroupChanges = { userIds: groupToUpdate.userIds.filter((id) => id !== userId), } as Partial<GroupModel>; const newGroup = allGroups.find(g => g.id === newGroupId); const newGroupChanges = { userIds: newGroup ? [...newGroup.userIds, userId] : [userId], } as Partial<GroupModel>; console.log(`Group ${groupToUpdate.id}: 将用户${userId}移动到组${newGroupId}`); // 更新两个组 return groupsAdapter.updateMany( [ { id: groupToUpdate.id, changes: oldGroupChanges }, { id: newGroupId, changes: newGroupChanges }, ], { ...state } ); } // 更新后的用户没有组,从旧组移除用户 console.log(`Group ${groupToUpdate.id}: 移除用户${userId}`); updateGroupChanges.userIds = groupToUpdate.userIds.filter( (id) => id !== userId ); } // 未找到用户所属的旧组 else { // 如果用户也没有设置新组,无需操作 if (!newGroupId) return state; // 如果用户设置了新组,将用户添加到该组 const newGroup = allGroups.find(g => g.id === newGroupId); if (!newGroup) { console.warn(`Group ${newGroupId}: 未找到!无法添加用户${userId}`); return state; } console.log(`Group ${newGroupId}: 添加用户${userId}`); updateGroupChanges.userIds = [...newGroup.userIds, userId]; } return groupsAdapter.updateOne( { id: groupToUpdate?.id ?? newGroupId, changes: updateGroupChanges, } as Update<GroupModel>, { ...state } satisfies GroupsEntityState ); }),
解决方案建议
1. 标准化做法:单一数据源+派生状态
NgRx的核心原则是单一数据源,对于关联关系,更推荐只存储单向关联,另一方向的关联通过**选择器(Selector)**动态派生,而非双向存储:
- 只在
UserModel中存储groupId(记录用户属于哪个组) GroupModel不再存储userIds,而是通过Selector从用户列表中筛选出属于该组的用户ID
示例Selector:
export const selectUsersByGroupId = createSelector( selectUsersState, (usersState, props: { groupId: number }) => selectAll(usersState).filter(user => user.groupId === props.groupId).map(user => user.id) );
这种方案的优势:
- 彻底避免双向同步的维护成本,只需要维护用户的
groupId字段 - 状态结构更简洁,没有冗余数据
- 关联关系始终保持一致,不会出现不同步问题
2. 优化现有双向存储的同步逻辑
如果必须保持双向存储的结构,可以通过以下方式简化代码:
提取通用同步工具函数
将关联同步逻辑提取为独立的工具函数,避免在Reducer中写大量重复代码:
// group.utils.ts export function syncUserGroupChange( groups: GroupModel[], userId: number, oldGroupId: number | undefined, newGroupId: number | undefined ): Update<GroupModel>[] { const updates: Update<GroupModel>[] = []; // 处理旧组:移除用户 if (oldGroupId) { const oldGroup = groups.find(g => g.id === oldGroupId); if (oldGroup) { updates.push({ id: oldGroupId, changes: { userIds: oldGroup.userIds.filter(id => id !== userId) } }); } } // 处理新组:添加用户 if (newGroupId && newGroupId !== oldGroupId) { const newGroup = groups.find(g => g.id === newGroupId); if (newGroup) { updates.push({ id: newGroupId, changes: { userIds: [...newGroup.userIds, userId] } }); } } return updates; }
然后在Reducer中调用该函数:
on(UsersAPIActions.userUpdatedSuccess, (state, { update }) => { const userId = +update.id; const newGroupId = update.changes.groupId; // 从用户状态中获取旧的groupId(或在Action中直接携带) const oldUser = selectUserById(userId)(usersState); const oldGroupId = oldUser?.groupId; const groupUpdates = syncUserGroupChange(selectAll(state), userId, oldGroupId, newGroupId); if (groupUpdates.length === 0) return state; return groupsAdapter.updateMany(groupUpdates, { ...state }); }),
优化Action设计
在发起更新用户的Action时,直接携带用户的旧groupId,避免在Reducer中查找旧数据:
// 更新UserAction定义 export const updateUser = createAction( '[Users] Update User', props<{ user: UserModel, oldGroupId?: number }>() );
组件dispatch时先获取用户原始groupId,再传递给Action,Reducer中直接使用即可。
总结
- 优先选择派生状态方案,从根本上避免双向同步问题,符合NgRx设计原则
- 若必须保持双向存储,通过提取工具函数、优化Action设计来简化同步逻辑,减少冗余代码
内容的提问来源于stack exchange,提问作者Starfish
相关产品推荐
相关产品推荐

