接手过一个用 Redux 写的后台系统,一个订单详情页要改三个字段,我得在 actionTypes.js、actions/order.js、reducers/order.js 三个文件之间来回跳,写完还要回组件里补 mapStateToProps。四十多行样板代码,换来的实际逻辑就是 state.detail = payload。那阵子我特别理解为什么会有人转向 MobX。
这篇把 MobX 的核心 API 逐个拆一遍,配一个能跑的计数器和一套 Store 组织方式。文章原本是 2024 年写的,这次补了一节 2026 年的现状:MobX 在 React 18 并发渲染和 React Server Components 下会遇到什么,以及什么情况下我会建议换成别的方案。老代码怎么写的我照原样留着,新的判断另起一节说,两边不混。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- MobX 想解决 Redux 的哪几个具体痛点,以及它付出了什么代价
observable、computed、action三块基石的职责边界observer是怎么让组件跟着数据自动重渲的autorun、reaction、flow各自的适用场景和常见误用- 一个完整可跑的计数器示例,把上面所有概念串起来
- Store 组织、严格模式配置、异步处理的工程化做法
- React 18 并发渲染和 RSC 之下 MobX 的现状,以及 2026 年的替代选择
# 一、MobX 想解决什么
# 1.1 Redux 的三个痛点
在 MobX 出现之前,React 项目的状态管理默认就是 Redux。它采用单一数据源,所有状态存在一个只读的 store 里,只能通过 action 和 reducer 更新。逻辑清晰,但用久了有三个地方磨人。
第一个是状态冗余更新。Redux 的状态是不可变的,每次更新都要创建新的状态对象,结果是与 store 连接的 UI 组件都会重新渲染,即使它们只依赖状态里的一小块数据。你可以用 PureRenderMixin 或者 reselect 做缓存优化,但这是额外的开发成本,而且优化对不对得靠自己盯。
第二个是样板代码。写 action types、action creators、reducers,一个再简单的功能也得把这套模板走一遍。
第三个是学习曲线。要理解 Redux 的数据流,得先搞明白 action、reducer、middleware 这几层怎么串,对新人不太友好。
# 1.2 MobX 的三块基石
MobX 走的是完全不同的路子。它用 JavaScript 的代理(Proxy)机制自动追踪状态的读写,谁读了哪个字段它记着,那个字段变了就通知谁。这套模式叫响应式编程。
拆开看是三件事。可观察的状态(Observable State),把普通 JS 对象转成可观察对象,任何修改都能被追踪。自动推导(Computed Values),根据现有状态算出衍生值,和 Excel 的公式一个意思,改了 A1 单元格,依赖它的公式自动重算。响应式副作用(Reactions),状态变化时自动执行副作用,比如更新 UI、打日志、发请求。
第三件里最重要的那个 reaction,就是 React 组件本身。
# 1.3 和 Redux 摆在一起看
| 特性 | MobX | Redux |
|---|---|---|
| 数据结构 | 多 store,支持普通对象 |
单一 store,不可变数据 |
| 更新方式 | 直接修改,可选严格模式 | 只能通过 action 和 reducer |
| 依赖追踪 | 自动追踪,按需更新 | 手动订阅,可能过度渲染 |
| 代码量 | 简洁,样板代码少 | 较多模板代码 |
| 学习成本 | 较低 | 较高 |
| 调试工具 | 友好的时间旅行调试 | 强大的 DevTools |
从数据流的覆盖范围看,Redux 管的是 STORE -> VIEW -> ACTION 一整圈,MobX 更关注 STORE -> VIEW 这一段。action 在 MobX 里是可选的,你完全可以直接改状态。方便归方便,代价是状态的修改入口不统一,出了问题不好回溯改的是谁。
# 1.4 优点和缺点
优点有三条。
第一是基于运行时的数据订阅。MobX 的数据依赖始终保持最小,而且是运行时自动追踪的。用 Redux 的时候你可能一不小心就多订阅或者少订阅了数据,多了性能差,少了不更新,都得靠 PureRenderMixin 和 reselect 给 selector 做缓存来补。
第二是可以用面向对象的方式组织领域模型。能抽出清晰 domain model 的场景下,OOP 组织起来是真的顺手。MobX 支持用引用的方式引用数据,很容易形成模型图(model graph),一个订单能直接拿到它的用户对象,而不是拿一个 userId 再去别的 slice 里查。
第三是改数据自然。MobX 基于原生的 JS 对象、数组和 Class 实现,改数据没有额外语法成本,不用每次都返回一个新对象,直接操作就行。
缺点也有三条。
第一是最佳实践和社区积累相对少。MobX 出现得晚,你遇到的问题可能社区还没人遇到过,而且它没有很好的扩展和插件机制。
第二是随意修改 store 的风险。Redux 里唯一能改数据的地方是 reducer,这保证了应用的可预测性;MobX 可以在任何地方改数据触发更新,用起来会有点不踏实。这个问题后来是有解的,MobX 从 2.2 版本开始加入了 action 支持,开启 strict mode 之后只有 action 能改数据,修改入口就收敛回来了。
第三是逻辑层的限制。如果更新逻辑没法很好地封装进 domain class,用 Redux 会更合适。另外 MobX 缺少类似 redux-saga 那样的库,复杂的业务流程编排不知道放哪合适。
# 二、核心 API 逐个拆
# 2.1 observable
observable 负责把普通属性变成可观察属性。被标记的属性暴露给观察者,值一变,所有依赖它的地方都会收到通知。可观察的值可以是基本类型、引用类型、普通对象、类实例、数组和 Map。
现在推荐的入口是 makeAutoObservable,在构造函数里调一次,它会自动推断每个成员该是什么:
import { makeAutoObservable } from 'mobx'
class Counter {
// 基本类型
count = 0
name = '计数器'
// 引用类型
user = {
name: '张三',
age: 25
}
// 数组
items = []
constructor() {
// makeAutoObservable 会自动推断类型并添加相应的装饰器
makeAutoObservable(this)
}
increment() {
this.count++
}
decrement() {
this.count--
}
setName(name) {
this.name = name
}
}
const counter = new Counter()
推断规则很直白,普通属性变成 observable,getter 变成 computed,普通方法变成 action,generator 方法变成 flow。
老项目里更常见的是装饰器写法,需要配 babel 才能用:
// 使用装饰器写法
import { observable, computed, action } from 'mobx'
class Counter {
@observable count = 0
@observable title = 'this is about page'
@observable num = 0
// computed - 计算值
@computed get getUserInfo() {
return `我是computed经过计算的getter, current num: ${this.num}`
}
// action - 动作
@action.bound add() {
this.num++
}
@action.bound reduce() {
this.num--
}
}
这套写法在 MobX 4/5 时代是主流,现在仍然支持,但从 MobX 6 开始它不再是默认路径。原因是装饰器提案长期没定稿,MobX 6 把 makeObservable / makeAutoObservable 提为主入口,装饰器降级成可选语法糖,而且用装饰器时仍然要在构造函数里调一次 makeObservable(this)。新写的代码建议直接用 makeAutoObservable,能省掉整套 babel 装饰器配置。
@action.bound 里的 .bound 是把方法绑到实例上,这样你可以直接 onClick={counter.add} 而不用 () => counter.add()。makeAutoObservable 默认不做这个绑定,需要的话得显式写 { add: action.bound }。
# 2.2 computed
计算值是根据现有状态或其它计算值衍生出来的值。它自带缓存,基础值没变的话,重复读取直接返回缓存,不会引起虚拟 DOM 的重新渲染。
import { makeAutoObservable, computed } from 'mobx'
class OrderStore {
items = []
taxRate = 0.1
constructor() {
makeAutoObservable(this, {
// 显式声明 computed 属性
subtotal: computed,
tax: computed,
total: computed
})
}
get subtotal() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
get tax() {
return this.subtotal * this.taxRate
}
get total() {
return this.subtotal + this.tax
}
addItem(item) {
this.items.push(item)
}
}
注意 total 依赖 tax,tax 依赖 subtotal,MobX 会自己把这条依赖链排好,items 一变,三个值按顺序失效重算。这条链你不用手写任何订阅代码。
有个坑要注意。computed 的缓存只在它被某个 reaction 观察时才生效,如果你在组件外面直接读一个没人观察的 computed,MobX 会每次都重新计算。这在写单元测试或者在事件回调里直接读 store 时容易踩到,表现是「明明加了 computed 却没变快」。
computed 还支持 setter 做逆向推导:
class Foo {
@observable length = 2
@computed get squared() {
return this.length * this.length
}
set squared(value) {
// 这是一个自动的动作,不需要注解
// 设置 squared 时,自动推导出 length
this.length = Math.sqrt(value)
}
}
这个特性用得不多,但在做「双向绑定的换算单元」时挺好使,比如摄氏度和华氏度两个输入框互相推导。
# 2.3 action
只有在 action 里才能修改 state。这条规则默认是不强制的,要靠严格模式打开: