NgRx状态管理实战经验与常见问题解决方法
我的写法,亲测靠谱
先说结论:NgRx 的状态管理虽然强大,但写得不好会把自己折腾死。我一般喜欢用一种比较清晰的目录结构来组织代码,比如把 actions、reducers 和 selectors 分开到不同文件夹里,而不是全堆在一个 feature 文件夹下。这样虽然看着文件多,但维护起来特别方便。
核心的 reducer 我会这样写:
import { createReducer, on } from '@ngrx/store';
import * as UserActions from './user.actions';
export interface UserState {
loading: boolean;
data: any;
error: string | null;
}
export const initialState: UserState = {
loading: false,
data: null,
error: null
};
export const userReducer = createReducer(
initialState,
on(UserActions.loadUser, (state) => ({
...state,
loading: true,
error: null
})),
on(UserActions.loadUserSuccess, (state, { user }) => ({
...state,
loading: false,
data: user
})),
on(UserActions.loadUserFailure, (state, { error }) => ({
...state,
loading: false,
error: error
}))
);
这种写法的好处是逻辑清晰,每个 action 对应的状态变化一目了然。特别是当项目大了以后,别人接手也能快速看懂。
这几种错误写法,别再踩坑了
最常见的一种错误就是直接在组件里调用 store.dispatch,然后还混着一堆业务逻辑。比如:
ngOnInit() {
this.store.dispatch(loadUser());
this.store.select('user').subscribe(user => {
if (user.loading) {
// 显示loading动画
}
if (user.error) {
// 处理错误
}
// 其他业务逻辑...
});
}
这种写法看着简单,但有几个大问题:一是组件和 store 耦合太紧,测试的时候很难 mock;二是 subscribe 没有及时 unsubscribe,容易造成内存泄漏;三是业务逻辑都堆在组件里,后期改需求会很痛苦。
还有个常见的坑是滥用 selectors。有人喜欢在 selector 里写一大堆计算逻辑,比如:
export const getUserFullName = createSelector(
getUser,
user => ${user.firstName} ${user.lastName}
);
看起来没问题,但如果这个 selector 在多个地方被调用,每次都会重新计算。我建议复杂计算还是放在 effect 或者 service 里处理比较好。
实际项目中的坑
在实际项目中,最容易出问题的就是异步操作的处理。比如接口请求失败后的重试机制,很多人会在 effect 里直接写 retry:
loadUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUser),
switchMap(() =>
this.userService.getUser().pipe(
map(user => UserActions.loadUserSuccess({ user })),
catchError(error => of(UserActions.loadUserFailure({ error }))),
retry(3) // 这里有问题
)
)
)
);
看起来很美好,但实际运行时你会发现 retry 会从 switchMap 开始重试,导致重复触发 action。正确的做法是把 retry 放在具体的 HTTP 请求里:
this.userService.getUser().pipe(
retry(3),
map(user => UserActions.loadUserSuccess({ user })),
catchError(error => of(UserActions.loadUserFailure({ error })))
)
另一个要注意的是状态的初始化问题。我之前遇到过一个坑,在 lazy load 的模块里使用 NgRx,结果发现状态没重置。后来才发现需要在 module 的 forFeature 里显式设置初始状态:
StoreModule.forFeature('user', userReducer, { initialState })
其他注意事项
几点小经验分享给大家:
- 不要在 reducer 里做任何副作用操作,比如调接口、修改 DOM 等
- effect 里记得 return action,否则会导致后续 effect 无法触发
- selector 尽量保持纯净,不要依赖外部变量
- 调试工具一定要装,@ngrx/store-devtools 是必备神器
还有个我经常用的小技巧:在开发环境可以开启 store freeze 来防止状态被意外修改:
import { storeFreeze } from 'ngrx-store-freeze';
@NgModule({
imports: [
StoreModule.forRoot(reducers, {
metaReducers: [storeFreeze]
})
]
})
总结一下
以上是我个人在使用 NgRx 过程中总结的一些经验。总的来说,NgRx 确实能很好地管理复杂应用的状态,但前提是你要用对方式。合理的目录结构、清晰的职责划分、注意性能优化,这些都是很重要的。
最后提醒大家:状态管理方案没有银弹,NgRx 也不是万能药。如果项目比较简单,可能用简单的 service 就够了。选择合适的技术才是最重要的。
有更好的实践或者不同见解,欢迎在评论区交流!

暂无评论