MobX实战指南:状态管理的高效写法与常见陷阱
先看效果,再看代码
我第一次用 MobX 是在重构一个老项目的状态管理模块。当时 Redux 写得又臭又长,改个状态要改七八个文件,烦死了。后来一咬牙切了 MobX,结果发现:真香。
核心就一点:你不用管“怎么变”,只管“变成什么样”。MobX 会自动追踪哪些组件用了哪些数据,数据一变,用到的地方自动更新。亲测有效,比手动写 shouldComponentUpdate 省心多了。
来看个最简单的例子:
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';
class CounterStore {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++;
}
}
const counterStore = new CounterStore();
function Counter() {
return (
<div>
<p>{counterStore.count}</p>
<button onClick={() => counterStore.increment()}>
+1
</button>
</div>
);
}
export default observer(Counter);
注意:observer 包裹组件,makeAutoObservable 让类变成响应式。就这么简单,别被文档里一堆 @observable、action 的装饰器吓到,现在官方推荐用 makeAutoObservable,更清爽。
这个场景最好用:表单状态管理
表单是前端最头疼的场景之一,尤其是动态表单、嵌套字段、联动校验。我之前用 Redux 写表单,光 reducer 就写了 200 行,还经常漏掉某个字段的更新逻辑。
换成 MobX 后,直接把表单状态塞进 store,每个字段都是可观察的,UI 自动同步。而且因为是 mutable(可变)的,写起来像操作普通对象,不用到处 spread。
class FormStore {
fields = {
name: '',
email: '',
phone: ''
};
errors = {};
constructor() {
makeAutoObservable(this);
}
updateField(field, value) {
this.fields[field] = value;
// 清除对应字段的错误
delete this.errors[field];
}
validate() {
this.errors = {};
if (!this.fields.name) this.errors.name = '姓名不能为空';
if (!this.fields.email) this.errors.email = '邮箱不能为空';
return Object.keys(this.errors).length === 0;
}
submit() {
if (this.validate()) {
console.log('提交数据:', this.fields);
// 实际项目中这里会发请求
}
}
}
配合 React 组件,几乎不用写 useEffect 监听变化,store 一变,UI 就跟着变。我试过在复杂表单里嵌套三层结构,照样丝滑。建议直接用这种方式,比那些 formik + yup 的组合轻量太多。
踩坑提醒:这三点一定注意
虽然 MobX 好用,但有几个坑我踩过好几次,必须提醒你:
- 不要在 render 里创建新对象或数组:MobX 通过引用追踪依赖。如果你在组件里写
const list = store.items.map(...),每次 render 都会生成新数组,导致 observer 误判依赖变化,疯狂重渲染。正确做法是把计算逻辑移到 store 里,用computed缓存。 - 异步操作记得用 action:虽然
makeAutoObservable默认会把方法包装成 action,但如果你在异步回调里直接修改状态,比如:fetchData() { api.get('/data').then(res => { this.data = res; // 这里会报错! }); }MobX 在严格模式下会禁止在非 action 中修改状态。解决办法是用
runInAction包裹,或者显式声明为 action:import { runInAction } from 'mobx'; fetchData() { api.get('/data').then(res => { runInAction(() => { this.data = res; }); }); } - 避免在 store 里存 DOM 或函数引用:MobX 会尝试追踪所有属性,如果 store 里有
onSubmit回调函数,而这个函数在每次 render 时都重新定义(比如父组件传下来的),那 observer 会认为依赖变了,导致不必要的重渲染。解决方案是把回调抽离,或者用useCallback固定引用。
高级技巧:computed 和 reaction 的妙用
很多人只知道 observable 和 observer,其实 computed 和 reaction 才是 MobX 的精华。
computed 适合做派生数据,比如过滤列表、计算总价。它会自动缓存,只有依赖变了才重新计算:
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this, {
completedCount: computed,
activeTodos: computed
});
}
get completedCount() {
return this.todos.filter(t => t.completed).length;
}
get activeTodos() {
return this.todos.filter(t => !t.completed);
}
}
而 reaction 适合做副作用,比如监听某个状态变化后自动保存到 localStorage:
import { reaction } from 'mobx';
// 在 store 初始化后调用
reaction(
() => this.todos,
(todos) => {
localStorage.setItem('todos', JSON.stringify(todos));
},
{ fireImmediately: true }
);
注意:reaction 默认不会立即执行,除非你加 fireImmediately: true。我一开始漏了这个,调试半天发现初始化数据没存进去。
还有一个冷门但实用的:when。它会在条件满足时执行一次回调,适合做“等数据加载完再干某事”的场景:
when(
() => this.isLoading === false && this.data.length > 0,
() => {
console.log('数据准备好了');
// 比如初始化图表
}
);
和 Context 配合?其实没必要
很多人习惯把 store 丢进 React Context,然后用 useContext 取。但 MobX 官方其实不推荐这么做——因为 store 本身就是一个全局单例,直接 import 用就行,省去一层 Provider 包裹。
当然,如果你的项目需要 SSR(服务端渲染),或者要支持多个 store 实例(比如多租户),那还是得用 Context。但普通项目真没必要折腾。
我的做法是:在 stores/index.js 里统一导出所有 store 实例:
// stores/index.js
import CounterStore from './CounterStore';
import FormStore from './FormStore';
export const counterStore = new CounterStore();
export const formStore = new FormStore();
然后在组件里直接 import 用。简单粗暴,还少写一堆 Context 代码。
最后说两句
MobX 不是银弹,但在中小型项目里,它真的能让你少写 50% 的样板代码。如果你还在被 Redux 的 action/type/reducer 折磨,不妨试试 MobX。上手成本低,心智负担小,关键是——写起来爽。
以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如和 React Native 配合、自定义 interceptor 等),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论