通过ngrx actions传递回调是否属于不良开发实践?
现有方案的问题分析
第一个方案(将回调塞入Action payload)不可行
- 违反NgRx核心设计原则:Action要求是可序列化的纯JSON对象,传入函数会直接导致Redux DevTools的时间旅行、状态快照等调试功能失效,也无法做状态回溯。
- 副作用逻辑散落在组件中,不符合全局副作用统一由Effect管理的约定,后续维护成本高。
第二个方案(全局订阅Action流)的缺陷
- 缺少上下文过滤:如果同时打开多个删除用户弹窗、或者其他页面也触发了用户删除操作,当前弹窗会被误关闭。
- 全局订阅即使加了
takeUntil销毁逻辑,依然存在不必要的全局事件监听,逻辑内聚性差。
更合理的实现方案
方案1:改进全局订阅,增加上下文过滤(改动最小)
在删除成功的Action中返回被删除的用户ID,订阅时仅匹配当前弹窗对应的用户ID,避免误触发:
[...] import { Actions } from '@ngrx/effects'; [...] @Component(...) class UserDeleteDialogComponent implements OnDestroy { destroyed$ = new Subject<boolean>(); constructor(updates$: Actions, @Inject(MAT_DIALOG_DATA) public data: {user: User}) { updates$.pipe( ofType(UserActions.DELETE_SUCCESS), // 仅匹配当前弹窗要删除的用户ID的成功事件 filter((action) => action.deletedUserId === this.data.user.id), takeUntil(this.destroyed$) ) .subscribe(() => { this.dialog.close(); }); } ngOnDestroy() { this.destroyed$.next(true); this.destroyed$.complete(); } confirm() { this.store.dispatch(deleteUser({ id: this.data.user.id })); } }
优点:改造成本极低,符合NgRx全局状态流的设计规范。
适用场景:删除操作的用户ID本身就是天然唯一标识,没有同一个用户同时触发多次删除的特殊场景。
方案2:增加关联ID适配复杂场景
如果存在同一个用户同时触发多个删除请求的特殊场景,可以在dispatch Action时生成唯一关联ID,成功/失败的Action携带该ID,订阅时仅匹配对应关联ID的事件即可:
// Action定义 export const deleteUser = createAction( '[User] Delete User', props<{ id: string; correlationId: string }>() ); export const deleteUserSuccess = createAction( '[User] Delete User Success', props<{ deletedUserId: string; correlationId: string }>() ); // 组件代码 confirm() { // 生成唯一关联ID const correlationId = uuidv4(); this.store.dispatch(deleteUser({ id: this.user.id, correlationId })); this.updates$.pipe( ofType(UserActions.DELETE_SUCCESS), filter(action => action.correlationId === correlationId), take(1), takeUntil(this.destroyed$) ).subscribe(() => this.dialog.close()); }
优点:完全避免同类型操作的冲突,适配复杂多实例操作场景。
方案3:使用Component Store做组件级状态管理(最推荐)
弹窗属于临时的、上下文独立的组件,用@ngrx/component-store做组件内的状态和副作用管理,完全和全局Store解耦,逻辑更内聚:
// 弹窗对应的Component Store @Injectable() export class UserDeleteDialogStore extends ComponentStore<{}> { constructor(private userService: UserService, private globalStore: Store) { super({}); } readonly deleteUser = this.effect<{userId: string}>((req$) => req$.pipe( switchMap(({userId}) => this.userService.delete(userId).pipe( tap(() => this.globalStore.dispatch(deleteUserSuccess({id: userId}))), map(() => ({success: true})), catchError(() => of({success: false})) ) ) ) ); } // 组件代码 @Component({ ..., providers: [UserDeleteDialogStore] // 组件级提供,销毁时自动回收 }) class UserDeleteDialogComponent { constructor( private dialogStore: UserDeleteDialogStore, public dialog: NgbActiveModal, @Inject(MAT_DIALOG_DATA) public data: {user: User} ) {} confirm() { this.dialogStore.deleteUser({userId: this.data.user.id}).pipe( take(1) ).subscribe(res => { if (res.success) { this.dialog.close(); } else { // 展示错误提示 } }) } }
优点:逻辑完全内聚在弹窗内部,不会污染全局状态流,不需要全局订阅,符合单一职责原则,调试和维护成本最低。
内容的提问来源于stack exchange,提问作者Thomas
相关产品推荐
相关产品推荐

