React MobX 基础教程:告别繁琐状态管理
在现代前端应用的开发中,状态管理无疑是核心且复杂的议题之一。随着应用规模的增长,数据流动变得错综复杂,如何高效、可维护地管理共享状态成为了开发者面临的巨大挑战。React 本身提供了 useState
和 useContext
等 Hooks 来处理组件内部和跨组件的状态,但在面对全局或复杂应用状态时,它们往往显得力不从心。
此时,专业的状态管理库便应运而生。在众多选择中,Redux 以其严格的单向数据流和强大的生态系统占据了一席之地。然而,Redux 的学习曲线陡峭,且伴随着大量的样板代码(action creators, reducers, selectors 等),常常让开发者望而却步,感叹其“繁琐”。
今天,我们将深入探讨另一个强大而优雅的解决方案——MobX。MobX 以其直观的响应式编程模型,承诺让状态管理变得简单、高效,并真正帮助开发者“告别繁琐状态管理”。
第一章:状态管理的困境与 MobX 的登场
1.1 为什么状态管理如此重要?
在 React 应用中,状态(State)是驱动 UI 渲染和逻辑执行的核心数据。
* 数据共享: 多个组件需要访问或修改同一份数据。
* 数据一致性: 确保所有依赖同一份数据的组件都能实时反映最新的数据状态。
* 复杂逻辑: 应用中可能存在异步操作、副作用、数据缓存等复杂逻辑,需要一个清晰的机制来管理这些过程。
* 可维护性: 随着项目规模扩大,混乱的状态管理会使代码难以理解、调试和扩展。
React 提供的 useState
和 useReducer
适用于组件内部状态或局部状态树,useContext
可以用于传递不常变动的数据或注入服务,但当需要跨越多个层级、多个组件共享和修改复杂状态时,它们就显得力不从心了。强行使用 Context API 往往导致 Context Provider 的层层嵌套,以及不必要的全量重渲染。
1.2 Redux 的“爱恨交织”
Redux 凭借其“单一数据源”、“状态只读”和“纯函数修改”三大原则,提供了严格且可预测的状态管理范式。这对于大型、高复杂度的应用来说,提供了强大的可调试性和可预测性。
然而,Redux 的缺点也同样显著:
* 样板代码过多: Action types, action creators, reducers, middleware 配置等,每增加一个功能都需要大量重复性代码。
* 学习曲线陡峭: 引入了大量新概念(纯函数、不可变性、中间件、高阶组件等)。
* 心智负担: 需要时刻注意不可变性,更新深层嵌套状态时尤其麻烦(尽管有 immer
等库辅助)。
* 性能优化复杂: 虽然 Redux 有 reselect
等工具,但仍需手动优化组件的重新渲染。
1.3 MobX:另一条康庄大道
与 Redux 严格的函数式、不可变性范式不同,MobX 拥抱的是面向对象和响应式编程。它的核心理念是:任何从应用状态中衍生的东西都应该自动更新。
MobX 的优势在于:
* 极简的样板代码: 大多数情况下,你只需要用 @observable
装饰器或 makeObservable
函数标记可观察的状态,用 @action
标记修改状态的方法,剩下的 MobX 都会自动处理。
* 直观的心智模型: 你可以直接修改状态(可变性),而不是创建新的状态副本。这更符合我们日常编程的直觉。
* 自动的细粒度更新: MobX 会追踪哪些组件实际使用了哪些可观察状态,并仅在这些状态发生变化时,精确地重新渲染受影响的组件,而非整个组件树,从而带来优秀的性能。
* 自由的架构: MobX 不强制你遵循特定的应用架构(如单一 Store)。你可以根据业务领域创建多个 Stores,使代码组织更加灵活。
简单来说,如果你厌倦了 Redux 的繁琐和心智负担,但又需要一个强大的状态管理方案,那么 MobX 绝对值得你一试。它能让你将精力更多地集中在业务逻辑本身,而不是状态管理的“八股文”。
第二章:MobX 的核心概念——基石的构建
理解 Mob MobX 的核心概念是掌握其强大威力的关键。这主要包括:Observable(可观察状态)、Action(动作)、Computed(计算值) 和 Reaction(反应)。
2.1 Observable (可观察状态)
定义: Observable 是 MobX 的核心。它们是应用中那些 MobX 能够追踪其变化并做出响应的数据。当你将一个属性、对象、数组或 Map 标记为 observable 后,MobX 会在幕后对其进行“增强”,使其在被读取时通知 MobX “我被读取了”,在被修改时通知 MobX “我被修改了”。
如何定义:
-
makeObservable(target, annotations?, options?)
(推荐,函数式 API)
这是 MobX 6 以后推荐的用法,更利于 Tree-shaking 和 TypeScript 类型推断,也更灵活。“`javascript
import { makeObservable, observable, action, computed } from “mobx”;class Todo {
id = Math.random();
title = “”;
completed = false;constructor(title) { makeObservable(this, { title: observable, // 标记 title 为可观察属性 completed: observable, // 标记 completed 为可观察属性 toggle: action, // 标记 toggle 为动作 getStatus: computed // 标记 getStatus 为计算值 }); this.title = title; } toggle() { this.completed = !this.completed; } get getStatus() { return this.completed ? "已完成" : "进行中"; }
}
class TodoStore {
todos = [];
filter = “”;constructor() { makeObservable(this, { todos: observable, // 标记 todos 数组为可观察 filter: observable, // 标记 filter 为可观察 addTodo: action, // 标记 addTodo 为动作 setFilter: action, // 标记 setFilter 为动作 filteredTodos: computed, // 标记 filteredTodos 为计算值 remainingCount: computed // 标记 remainingCount 为计算值 }); } addTodo(title) { this.todos.push(new Todo(title)); // 直接修改数组 } setFilter(filter) { this.filter = filter; } get filteredTodos() { const matchFilter = new RegExp(this.filter, "i"); return this.todos.filter(todo => !this.filter || matchFilter.test(todo.title) ); } get remainingCount() { return this.todos.filter(todo => !todo.completed).length; }
}
const todoStore = new TodoStore();
“` -
@observable
(装饰器,需要配置 Babel/TypeScript)
这种方式更简洁,但需要构建工具支持。“`javascript
import { observable, action, computed } from “mobx”;
import { observer } from “mobx-react-lite”; // 注意导入observerclass Todo {
id = Math.random();
@observable title = “”;
@observable completed = false;constructor(title) { this.title = title; } @action toggle() { this.completed = !this.completed; } @computed get getStatus() { return this.completed ? "已完成" : "进行中"; }
}
class TodoStore {
@observable todos = [];
@observable filter = “”;@action addTodo(title) { this.todos.push(new Todo(title)); } @action setFilter(filter) { this.filter = filter; } @computed get filteredTodos() { const matchFilter = new RegExp(this.filter, "i"); return this.todos.filter(todo => !this.filter || matchFilter.test(todo.title) ); } @computed get remainingCount() { return this.todos.filter(todo => !todo.completed).length; }
}
``
makeObservable` 方式。
本教程后续示例主要使用
注意事项:
* 数组/Map/Set: 当将数组、Map 或 Set 标记为 observable
时,MobX 会将它们转换为内部的响应式版本,以便追踪元素的增删改。直接修改这些集合(如 push
, splice
, delete
)都会触发响应。
* 深层嵌套: 默认情况下,observable
会递归地将对象和数组内部的属性也变为可观察的。如果你不需要深层观察或想手动控制,可以使用 observable.shallow
或 observable.ref
。
2.2 Action (动作)
定义: Action 是任何修改可观察状态的代码块。MobX 鼓励你将所有状态修改都封装在 Action 中。这有助于 MobX 批量更新,优化性能,并使其追踪状态变化变得更加清晰。在严格模式下(MobX 默认开启),非 Action 的状态修改会抛出错误。
如何定义:
* action(fn)
函数:action(() => { this.count++; })
* @action
装饰器(用于类方法):@action increment() { this.count++; }
* 在 makeObservable
中标记:makeObservable(this, { prop: observable, method: action });
示例:
在上面的 TodoStore
中,addTodo
和 setFilter
方法都被标记为 action
。
“`javascript
class TodoStore {
// …
@action
addTodo(title) {
this.todos.push(new Todo(title)); // 状态修改
}
@action
setFilter(filter) {
this.filter = filter; // 状态修改
}
}
“`
为什么需要 Action?
1. 原子性更新: MobX 会在 Action 执行期间批量处理所有状态更新,并在 Action 结束后一次性通知所有观察者,避免多次不必要的重渲染。
2. 调试友好: 在开发工具中,Action 可以被追踪和记录,有助于理解状态是如何变化的。
3. 严格模式: 默认情况下,MobX 处于严格模式,强制所有状态修改必须通过 Action 进行。这有助于防止意外的状态修改,提高代码可预测性。
2.3 Computed (计算值)
定义: Computed 是从现有的可观察状态或其他计算值中派生出来的值。它们类似于电子表格中的公式,只有当其依赖的可观察状态发生变化时,才会重新计算。否则,它们会返回缓存的结果,从而提供优秀的性能优化。
如何定义:
* computed(fn)
函数:computed(() => this.remainingTodos.length)
* @computed
装饰器(用于类 getter):@computed get remainingCount() { ... }
* 在 makeObservable
中标记:makeObservable(this, { computedProp: computed });
示例:
在 TodoStore
中,filteredTodos
和 remainingCount
都是计算值。
“`javascript
class TodoStore {
// …
@computed
get filteredTodos() {
console.log(“重新计算 filteredTodos”); // 调试用
const matchFilter = new RegExp(this.filter, “i”);
return this.todos.filter(todo =>
!this.filter || matchFilter.test(todo.title)
);
}
@computed
get remainingCount() {
console.log("重新计算 remainingCount"); // 调试用
return this.todos.filter(todo => !todo.completed).length;
}
}
``
this.filter
当或
this.todos发生变化时,
filteredTodos才会重新计算。当
this.todos中任意 Todo 的
completed状态变化时,
remainingCount才会重新计算。如果这些依赖没有变化,即使多次访问
filteredTodos或
remainingCount`,它们也只会返回上次缓存的结果。
2.4 Reaction (反应)
定义: Reaction 是 MobX 中一种独立于 UI 的副作用。它们是当可观察状态发生变化时,自动执行的函数。与 Computed
的区别在于:Computed
会产生一个值,而 Reaction
会产生一个副作用(如打印日志、网络请求、更新 DOM 等)。
MobX 提供了三种主要的 Reaction:
-
autorun(effect: () => void)
:- 接收一个函数作为参数。
- 该函数中使用的所有可观察状态都会被自动追踪。
- 在
autorun
定义时立即执行一次,之后每当其依赖的任何可观察状态变化时,都会重新执行。 - 适合: 打印日志、发送分析数据、同步本地存储等,无需返回值且依赖相对简单的场景。
“`javascript
import { autorun } from “mobx”;autorun(() => {
console.log(当前过滤条件: ${todoStore.filter}
);
console.log(待办事项数量: ${todoStore.remainingCount}
);
});
// 初始执行一次
// 之后每次 filter 或 remainingCount 变化时都会执行
“` -
reaction(expression: () => T, effect: (value: T, prevValue: T) => void)
:- 接收两个函数:
expression
:一个函数,它返回你想要观察的值。这个函数中使用的可观察状态会被追踪。effect
:一个副作用函数,当expression
的返回值发生变化时执行。
- 在
reaction
定义时不立即执行effect
函数。 effect
函数只会响应expression
函数返回值的变化。- 适合: 当你只关心特定数据的变化,并且想在变化时执行副作用,且副作用依赖于这个变化后的值时。
“`javascript
import { reaction } from “mobx”;reaction(
() => todoStore.remainingCount, // 观察这个值
(count, prevCount) => { // 当值变化时执行这个副作用
if (count === 0 && prevCount > 0) {
alert(“恭喜!所有待办事项已完成!”);
}
}
);
“` - 接收两个函数:
-
when(predicate: () => boolean, effect: () => void)
:- 接收两个函数:
predicate
:一个返回布尔值的函数,用于定义一个条件。effect
:当predicate
返回true
时执行的副作用函数。
effect
函数只会执行一次,执行后这个when
反应会自动销毁。- 适合: 满足特定条件后执行一次性操作的场景,如加载数据、导航等。
“`javascript
import { when } from “mobx”;when(
() => todoStore.todos.length > 5, // 当待办事项数量超过5个时
() => {
console.log(“待办事项有点多了,该清理一下了!”);
// 可以在这里触发一个清理动作或显示一个提示
}
);
“` - 接收两个函数:
清理 Reaction:
autorun
和 reaction
都返回一个清理函数。当组件卸载或不再需要监听时,调用这个清理函数可以停止 Reaction,避免内存泄漏。
javascript
const dispose = autorun(() => { /* ... */ });
// 稍后
dispose(); // 停止这个 reaction
在 React 组件中,通常在 useEffect
的清理函数中调用 dispose
。
第三章:MobX 与 React 的集成——连接UI与状态
MobX 的核心是数据层,而 React 是视图层。要让两者协同工作,我们需要一个桥梁,那就是 mobx-react-lite
(推荐用于函数组件) 或 mobx-react
(支持类组件和函数组件)。
3.1 安装依赖
“`bash
npm install mobx mobx-react-lite
或者 yarn add mobx mobx-react-lite
“`
如果你使用装饰器(@observable
, @action
等),并且使用的是 Babel,你可能还需要:
bash
npm install @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties --save-dev
并在 .babelrc
或 babel.config.js
中配置:
json
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}
3.2 observer
高阶组件/Hook
observer
是 mobx-react-lite
或 mobx-react
提供的核心工具。它的作用是:将 React 组件转换为一个“观察者”。当这个组件渲染时,MobX 会追踪它所使用的所有可观察数据。一旦这些数据发生变化,observer
就会通知 React 重新渲染这个组件。
为什么需要 observer
?
React 的渲染机制是基于 Props 或 State 的变化。MobX 的状态是可变的,当你在 MobX Store 中直接修改 todo.completed = true
时,React 组件并不知道底层数据发生了变化,它不会自动重渲染。observer
HOC/Hook 解决了这个问题,它拦截了 MobX 状态的变化通知,并将其转化为 React 的 forceUpdate
或类似的更新机制,从而触发组件重渲染。
-
对于函数式组件(推荐使用
mobx-react-lite
):“`jsx
import React from ‘react’;
import { observer } from ‘mobx-react-lite’; // 从 mobx-react-lite 导入// 假设你有以下 MobX Store
class CounterStore {
constructor() {
makeObservable(this, {
count: observable,
increment: action,
decrement: action
});
}
count = 0;
increment() { this.count++; }
decrement() { this.count–; }
}
const counterStore = new CounterStore();// 将组件用 observer 包裹
const CounterComponent = observer(() => {
console.log(‘CounterComponent 渲染了!’); // 观察重渲染次数
return (计数器: {counterStore.count}
);
});export default CounterComponent;
``
counterStore.count
当发生变化时,只有
CounterComponent` 会重新渲染。 -
对于类组件(使用
mobx-react
):“`jsx
import React from ‘react’;
import { observer } from ‘mobx-react’; // 从 mobx-react 导入class CounterStore { / …同上… / }
const counterStore = new CounterStore();@observer // 用装饰器包裹类组件
class CounterComponent extends React.Component {
render() {
console.log(‘CounterComponent 渲染了!’);
return (计数器: {counterStore.count}
);
}
}export default CounterComponent;
``
@observer` 装饰器确保了类组件的响应式更新。
同样的,
重要提示:
* 只包裹消费可观察数据的组件: 只有那些直接或间接从 MobX Store 中读取可观察状态的 React 组件才需要被 observer
包裹。如果一个组件只是简单地传递 props 或渲染静态内容,而没有直接访问 MobX Store,那么它不需要被 observer
包裹,否则会造成不必要的开销。
* 细粒度更新: observer
的神奇之处在于,它只会订阅组件实际用到的可观察属性。例如,如果 CounterComponent
只使用了 counterStore.count
,那么只有当 count
变化时它才会重渲染,即使 counterStore
中有其他属性变化也不会触发它重渲染。
3.3 共享 MobX Stores (Context API)
在实际应用中,你通常会创建多个 MobX Store 来管理不同领域的状态。为了避免手动将每个 Store 作为 props 传递给每个组件,最佳实践是使用 React 的 Context API 来提供 Store。
-
创建 Store Context:
“`javascript
// stores/index.js
import React from ‘react’;
import { makeObservable, observable, action, computed } from ‘mobx’;class TodoStore {
constructor() {
makeObservable(this, {
todos: observable,
filter: observable,
addTodo: action,
toggleTodo: action,
setFilter: action,
filteredTodos: computed,
remainingCount: computed
});
}
todos = [];
filter = “”;addTodo(title) { this.todos.push(new Todo(title)); } toggleTodo(id) { const todo = this.todos.find(t => t.id === id); if (todo) { todo.completed = !todo.completed; // 直接修改 observable } } setFilter(filter) { this.filter = filter; } get filteredTodos() { const matchFilter = new RegExp(this.filter, "i"); return this.todos.filter(todo => !this.filter || matchFilter.test(todo.title) ); } get remainingCount() { return this.todos.filter(todo => !todo.completed).length; }
}
class Todo {
constructor(title) {
makeObservable(this, {
title: observable,
completed: observable,
});
this.id = Math.random();
this.title = title;
this.completed = false;
}
}// 创建 MobX Stores 实例
const todoStore = new TodoStore();// 创建 React Context
export const TodoStoreContext = React.createContext(todoStore); // 默认值,也可以是 null// 创建一个 Hook 方便消费
export const useTodoStore = () => React.useContext(TodoStoreContext);
“` -
在应用根部提供 Store:
“`jsx
// App.js
import React from ‘react’;
import { TodoStoreContext, todoStore } from ‘./stores’; // 导入 Context 和 Store 实例// 导入你的组件
import TodoInput from ‘./components/TodoInput’;
import TodoList from ‘./components/TodoList’;
import TodoFooter from ‘./components/TodoFooter’;function App() {
return (
MobX Todo 应用
);
}export default App;
“` -
在组件中消费 Store:
“`jsx
// components/TodoInput.jsx
import React, { useState } from ‘react’;
import { observer } from ‘mobx-react-lite’;
import { useTodoStore } from ‘../stores’; // 导入自定义 Hookconst TodoInput = observer(() => {
const todoStore = useTodoStore(); // 获取 store 实例
const [newTodoTitle, setNewTodoTitle] = useState(”);const handleSubmit = (e) => { e.preventDefault(); if (newTodoTitle.trim()) { todoStore.addTodo(newTodoTitle.trim()); setNewTodoTitle(''); } }; return ( <form onSubmit={handleSubmit}> <input type="text" value={newTodoTitle} onChange={(e) => setNewTodoTitle(e.target.value)} placeholder="添加新待办事项..." /> <button type="submit">添加</button> </form> );
});
export default TodoInput;
“`
这种模式使得你的 MobX Store 可以在任何需要它的 React 组件中被方便地访问到,而无需手动传递 props。
第四章:构建一个完整的 Todo 应用示例
现在,我们将把以上概念整合起来,构建一个功能完整的 Todo 列表应用。
4.1 项目结构
my-mobx-todo/
├── public/
├── src/
│ ├── components/
│ │ ├── TodoInput.jsx
│ │ ├── TodoItem.jsx
│ │ ├── TodoList.jsx
│ │ └── TodoFooter.jsx
│ ├── stores/
│ │ └── todoStore.js # 包含 TodoStore 和 Todo 类定义以及 Context
│ ├── App.js
│ ├── index.js
│ └── index.css # 简单样式
└── package.json
4.2 src/stores/todoStore.js
这是我们定义核心状态逻辑的地方。
“`javascript
// src/stores/todoStore.js
import { makeObservable, observable, action, computed } from ‘mobx’;
import React from ‘react’;
// Todo 模型
class Todo {
id = Math.random();
title = “”;
completed = false;
constructor(title) {
makeObservable(this, {
title: observable,
completed: observable,
toggle: action, // 虽然在 TodoItem 组件中直接修改了,但为了严谨性也可以定义action
});
this.title = title;
}
// 可以在这里定义 toggle action,使得业务逻辑内聚
toggle() {
this.completed = !this.completed;
}
}
// TodoStore
class TodoStore {
todos = [];
filter = “”; // ‘all’, ‘active’, ‘completed’
constructor() {
makeObservable(this, {
todos: observable,
filter: observable,
addTodo: action,
toggleTodo: action,
removeTodo: action,
setFilter: action,
clearCompleted: action,
filteredTodos: computed,
remainingCount: computed,
completedCount: computed,
totalCount: computed,
isAllCompleted: computed,
});
// 初始数据(可选)
this.addTodo("学习 MobX");
this.addTodo("掌握 React Hooks");
this.todos[0].completed = true;
}
addTodo(title) {
this.todos.push(new Todo(title));
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
// 直接修改 observable 对象的属性
// 注意:Todo 内部的 completed 属性也必须是 observable
todo.completed = !todo.completed;
}
}
removeTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
setFilter(filter) {
this.filter = filter;
}
clearCompleted() {
this.todos = this.todos.filter(todo => !todo.completed);
}
get filteredTodos() {
switch (this.filter) {
case 'active':
return this.todos.filter(todo => !todo.completed);
case 'completed':
return this.todos.filter(todo => todo.completed);
default: // 'all'
return this.todos;
}
}
get remainingCount() {
return this.todos.filter(todo => !todo.completed).length;
}
get completedCount() {
return this.todos.filter(todo => todo.completed).length;
}
get totalCount() {
return this.todos.length;
}
get isAllCompleted() {
return this.totalCount > 0 && this.remainingCount === 0;
}
}
// 创建 MobX Store 实例
const todoStore = new TodoStore();
// 创建 React Context
export const TodoStoreContext = React.createContext(null); // 初始值可以设为 null
// 创建一个自定义 Hook 来方便组件消费 Store
export const useTodoStore = () => {
const store = React.useContext(TodoStoreContext);
if (!store) {
// 严格模式,确保 Provider 被正确设置
throw new Error(‘useTodoStore must be used within a TodoStoreContext.Provider’);
}
return store;
};
// 导出 store 实例(方便在 App.js 中提供)
export default todoStore;
“`
4.3 src/App.js
应用程序的根组件,负责提供 MobX Store。
“`javascript
// src/App.js
import React from ‘react’;
import { TodoStoreContext } from ‘./stores/todoStore’; // 导入 Context
import todoStoreInstance from ‘./stores/todoStore’; // 导入 Store 实例
import TodoInput from ‘./components/TodoInput’;
import TodoList from ‘./components/TodoList’;
import TodoFooter from ‘./components/TodoFooter’;
import ‘./index.css’; // 导入样式
function App() {
return (
MobX Todo List
);
}
export default App;
“`
4.4 src/index.js
应用的入口文件。
“`javascript
// src/index.js
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import App from ‘./App’;
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
);
“`
4.5 src/components/TodoInput.jsx
添加新待办事项的输入框。
“`javascript
// src/components/TodoInput.jsx
import React, { useState } from ‘react’;
import { observer } from ‘mobx-react-lite’;
import { useTodoStore } from ‘../stores/todoStore’;
const TodoInput = observer(() => {
const todoStore = useTodoStore();
const [newTodoTitle, setNewTodoTitle] = useState(”);
const handleSubmit = (e) => {
e.preventDefault();
if (newTodoTitle.trim()) {
todoStore.addTodo(newTodoTitle.trim());
setNewTodoTitle('');
}
};
return (
<form className="todo-input" onSubmit={handleSubmit}>
<input
type="text"
value={newTodoTitle}
onChange={(e) => setNewTodoTitle(e.target.value)}
placeholder="你想做什么?"
/>
<button type="submit">添加</button>
</form>
);
});
export default TodoInput;
“`
4.6 src/components/TodoItem.jsx
单个待办事项的组件。
“`javascript
// src/components/TodoItem.jsx
import React from ‘react’;
import { observer } from ‘mobx-react-lite’;
import { useTodoStore } from ‘../stores/todoStore’;
const TodoItem = observer(({ todo }) => {
const todoStore = useTodoStore();
const handleToggle = () => {
// 直接在组件中调用 store 上的 action
todoStore.toggleTodo(todo.id);
};
const handleRemove = () => {
todoStore.removeTodo(todo.id);
};
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={handleToggle}
/>
<span>{todo.title}</span>
<button className="remove-btn" onClick={handleRemove}>×</button>
</li>
);
});
export default TodoItem;
“`
4.7 src/components/TodoList.jsx
待办事项列表的容器。
“`javascript
// src/components/TodoList.jsx
import React from ‘react’;
import { observer } from ‘mobx-react-lite’;
import { useTodoStore } from ‘../stores/todoStore’;
import TodoItem from ‘./TodoItem’;
const TodoList = observer(() => {
const todoStore = useTodoStore();
// 直接使用 MobX Store 中的 computed 属性
const { filteredTodos, toggleTodo } = todoStore;
// 可以在这里添加一个全选/反选的逻辑
const handleToggleAll = () => {
const allCompleted = todoStore.isAllCompleted;
// 遍历所有待办,根据当前全选状态来设置
todoStore.todos.forEach(todo => {
if (todo.completed === allCompleted) {
todo.completed = !allCompleted; // 直接修改 observable
}
});
};
return (
<div className="todo-list">
{todoStore.totalCount > 0 && (
<div className="toggle-all-container">
<input
type="checkbox"
id="toggle-all"
checked={todoStore.isAllCompleted}
onChange={handleToggleAll}
/>
<label htmlFor="toggle-all">全选</label>
</div>
)}
<ul>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
{filteredTodos.length === 0 && todoStore.totalCount > 0 && (
<p className="no-items-message">当前筛选条件下没有待办事项。</p>
)}
{todoStore.totalCount === 0 && (
<p className="no-items-message">还没有待办事项,快添加一个吧!</p>
)}
</div>
);
});
export default TodoList;
“`
4.8 src/components/TodoFooter.jsx
底部状态栏,显示待办数量和筛选按钮。
“`javascript
// src/components/TodoFooter.jsx
import React from ‘react’;
import { observer } from ‘mobx-react-lite’;
import { useTodoStore } from ‘../stores/todoStore’;
const TodoFooter = observer(() => {
const todoStore = useTodoStore();
const { filter, remainingCount, setFilter, clearCompleted, completedCount } = todoStore;
return (
<footer className="todo-footer">
<span className="todo-count">
<strong>{remainingCount}</strong> 项待办
</span>
<ul className="filters">
<li>
<button
className={filter === 'all' ? 'selected' : ''}
onClick={() => setFilter('all')}
>
全部
</button>
</li>
<li>
<button
className={filter === 'active' ? 'selected' : ''}
onClick={() => setFilter('active')}
>
进行中
</button>
</li>
<li>
<button
className={filter === 'completed' ? 'selected' : ''}
onClick={() => setFilter('completed')}
>
已完成
</button>
</li>
</ul>
{completedCount > 0 && (
<button
className="clear-completed"
onClick={clearCompleted}
>
清除已完成 ({completedCount})
</button>
)}
</footer>
);
});
export default TodoFooter;
“`
4.9 src/index.css
(可选,提供基础样式)
“`css
/ src/index.css /
body {
font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, ‘Roboto’, ‘Oxygen’,
‘Ubuntu’, ‘Cantarell’, ‘Fira Sans’, ‘Droid Sans’, ‘Helvetica Neue’,
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
background-color: #f5f5f5;
margin: 0;
padding-top: 50px;
}
root {
width: 100%;
max-width: 600px;
background: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
.App {
padding: 20px;
}
h1 {
text-align: center;
font-size: 48px;
color: #333;
margin-bottom: 30px;
}
/ TodoInput /
.todo-input {
display: flex;
margin-bottom: 20px;
}
.todo-input input {
flex-grow: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
outline: none;
}
.todo-input input:focus {
border-color: #4CAF50;
}
.todo-input button {
background-color: #4CAF50;
color: white;
border: none;
padding: 12px 20px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
font-size: 16px;
transition: background-color 0.2s;
}
.todo-input button:hover {
background-color: #45a049;
}
/ TodoList /
.todo-list ul {
list-style: none;
padding: 0;
margin: 0;
}
.todo-list .toggle-all-container {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 5px 0;
border-bottom: 1px solid #eee;
}
.todo-list .toggle-all-container input[type=”checkbox”] {
margin-right: 10px;
transform: scale(1.2);
cursor: pointer;
}
.todo-list .toggle-all-container label {
font-size: 16px;
color: #555;
cursor: pointer;
}
/ TodoItem /
.todo-item {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.todo-item:last-child {
border-bottom: none;
}
.todo-item input[type=”checkbox”] {
margin-right: 15px;
transform: scale(1.2);
cursor: pointer;
}
.todo-item span {
flex-grow: 1;
font-size: 18px;
color: #333;
}
.todo-item.completed span {
text-decoration: line-through;
color: #aaa;
}
.todo-item .remove-btn {
background: none;
border: none;
color: #cc0000;
font-size: 24px;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.todo-item .remove-btn:hover {
opacity: 1;
}
.no-items-message {
text-align: center;
color: #888;
margin-top: 20px;
font-size: 16px;
}
/ TodoFooter /
.todo-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
border-top: 1px solid #eee;
margin-top: 20px;
}
.todo-footer .todo-count {
font-size: 14px;
color: #555;
}
.todo-footer .filters {
list-style: none;
padding: 0;
margin: 0;
display: flex;
}
.todo-footer .filters li {
margin: 0 5px;
}
.todo-footer .filters button {
background: none;
border: 1px solid transparent;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
color: #555;
transition: all 0.2s;
}
.todo-footer .filters button:hover {
border-color: #ddd;
}
.todo-footer .filters button.selected {
border-color: #4CAF50;
color: #4CAF50;
}
.todo-footer .clear-completed {
background: none;
border: none;
color: #cc0000;
cursor: pointer;
font-size: 14px;
opacity: 0.7;
transition: opacity 0.2s;
}
.todo-footer .clear-completed:hover {
text-decoration: underline;
opacity: 1;
}
“`
至此,一个功能齐全的 MobX + React Todo 应用就构建完成了。你可以运行 npm start
来查看效果。
4.10 运行与观察
在浏览器中打开应用,尝试添加、完成、删除待办事项,并切换筛选条件。你会发现 UI 响应非常迅速,并且是精确地更新了。这就是 MobX 细粒度响应式更新的魅力。
第五章:MobX 高级概念与最佳实践
5.1 严格模式 (Strict Mode)
MobX 默认开启严格模式,这意味着所有可观察状态的修改都必须在 action
中进行。这是 MobX 推荐的最佳实践,它确保了状态变化的意图明确,并且可以在开发工具中被追踪。
如果你尝试在非 action
中修改一个可观察属性,MobX 会抛出错误:[MobX] In strict mode, all mutations to observable state must be wrapped in an action. Wrap the code in 'action()' if this is intended.
如何关闭(不推荐):
如果你确实想关闭严格模式,可以在应用启动时配置:
“`javascript
import { configure } from “mobx”;
configure({
enforceActions: “never” // “always” (默认), “never”, “observed”
});
``
“observed”` 模式允许在 Action 之外修改未被观察的属性,一旦属性被观察,就必须通过 Action 修改。
5.2 调试 MobX
MobX 提供了多种调试手段:
- MobX DevTools: Chrome/Firefox 浏览器扩展,可以可视化 MobX 状态树,追踪 Action,查看依赖关系。这是最强大的调试工具。
-
trace()
: 用于追踪某个 observable 或 computed 是如何被观察到的,或者某个 action 是如何被调用的。
“`javascript
import { autorun, trace } from “mobx”;autorun(() => {
trace(); // 打印这个 autorun 的所有依赖
console.log(todoStore.remainingCount);
});// 追踪某个特定的 observable
trace(todoStore, ‘remainingCount’);
3. **日志:** 在 `action` 或 `computed` 中添加 `console.log`。
javascript
4. **`spy()`:** 监听所有 MobX 事件(action, reaction, computed 计算等)。适合在需要全面了解 MobX 内部行为时使用。
import { spy } from “mobx”;spy(event => {
if (event.type === ‘action’) {
console.log(${event.name} with args: ${JSON.stringify(event.arguments)}
);
}
// 也可以监听 ‘update’, ‘add’, ‘delete’ 等事件
});
“`
5.3 异步操作
在 MobX 中处理异步操作非常简单,你可以在 action
中直接使用 async/await
。
“`javascript
import { makeObservable, observable, action } from “mobx”;
class UserStore {
@observable user = null;
@observable loading = false;
@observable error = null;
constructor() {
makeObservable(this, {
user: observable,
loading: observable,
error: observable,
fetchUser: action, // 异步 action 仍然需要被标记为 action
});
}
@action
async fetchUser(userId) {
this.loading = true;
this.error = null;
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data = await response.json();
this.user = data;
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
}
}
const userStore = new UserStore();
``
fetchUser
在这个例子中,方法内部的
this.loading,
this.error,
this.user` 的修改都会被 MobX 追踪并触发相应的更新。
5.4 性能优化
虽然 MobX 默认提供了细粒度的更新,但在某些情况下,你仍然可以进一步优化性能:
- 正确使用
observer
: 确保只有需要响应式更新的组件才被observer
包裹。 - 粒度控制: 如果一个对象包含大量属性,并且只有少数属性需要被观察,可以考虑使用
observable.ref
或将对象拆分为更小的可观察对象。observable.ref
: 只有引用变化时才触发,不观察对象内部。observable.shallow
: 只有第一层属性被观察。
- 计算值(Computed)的效率: 确保
computed
属性的 getter 函数是纯粹的,没有副作用,并且其计算成本不要过高。如果计算成本高,确保其依赖尽可能少地变化。 - 避免在循环中创建
observable
: 在循环中频繁创建 MobX 的 observable 实例可能会有开销。如果数组中的对象都是一样的结构,可以预先将它们定义为可观察的类。 - React
memo
与observer
结合: 如果一个observer
组件接收到 props,并且这些 props 不来自 MobX Store,而是来自父组件,你可以将它与React.memo
结合使用来避免不必要的重渲染:
jsx
const MyComponent = observer(React.memo(({ propFromParent }) => {
// ... 使用 propFromParent 和 MobX Store
}));
但请注意,当propFromParent
是一个对象或数组时,如果父组件在每次渲染时都创建新引用,React.memo
可能不会阻止重渲染。
5.5 测试 MobX Stores
测试 MobX Stores 通常比测试 Redux 的 Reducers 简单得多,因为它们是普通的 JavaScript 类和方法。你可以直接实例化 Store,调用其 Action,并断言其属性和计算值是否符合预期。
“`javascript
// store.test.js
import { TodoStore, Todo } from ‘./stores/todoStore’; // 假设你的 store 是这样导出的
describe(‘TodoStore’, () => {
let store;
beforeEach(() => {
store = new TodoStore();
});
test('should add a todo', () => {
expect(store.totalCount).toBe(2); // 初始数据
store.addTodo('测试任务');
expect(store.totalCount).toBe(3);
expect(store.todos[2].title).toBe('测试任务');
});
test('should toggle todo completion status', () => {
const initialTodo = store.todos[0];
const initialStatus = initialTodo.completed;
store.toggleTodo(initialTodo.id);
expect(initialTodo.completed).toBe(!initialStatus);
store.toggleTodo(initialTodo.id); // Toggle back
expect(initialTodo.completed).toBe(initialStatus);
});
test('should filter todos correctly', () => {
store.setFilter('active');
expect(store.filteredTodos.length).toBe(store.remainingCount);
expect(store.filteredTodos.every(todo => !todo.completed)).toBe(true);
store.setFilter('completed');
expect(store.filteredTodos.length).toBe(store.completedCount);
expect(store.filteredTodos.every(todo => todo.completed)).toBe(true);
store.setFilter('all');
expect(store.filteredTodos.length).toBe(store.totalCount);
});
test('should clear completed todos', () => {
store.todos[0].completed = true; // Ensure some are completed
store.todos[1].completed = false;
const initialRemaining = store.remainingCount;
store.clearCompleted();
expect(store.totalCount).toBe(initialRemaining);
expect(store.completedCount).toBe(0);
});
});
“`
第六章:MobX 生态与总结
6.1 MobX 生态
除了核心的 mobx
和 mobx-react-lite
,MobX 还有一个强大的生态系统:
- MobX-State-Tree (MST): 这是一个基于 MobX 的高级状态管理库,它将 MobX 的响应式能力与 Redux 的结构化、可预测性结合起来。MST 提供了强大的类型系统(模型定义)、快照、时间旅行调试、引用等特性,适用于非常大型和复杂的应用。如果你觉得纯 MobX 过于自由,需要更多结构和约束,可以考虑 MST。
- MobX-React-Form: 简化表单处理。
- MobX Utils: 提供一些 MobX 相关的实用工具。
6.2 MobX 与其他状态管理方案的抉择
- 小型应用或局部状态: React
useState
,useReducer
,useContext
通常足够。 - 中大型应用,追求简洁高效: MobX 是一个非常好的选择,尤其适合那些喜欢面向对象、直观可变性、且注重开发效率的团队。
- 中大型应用,追求严格可预测、时间旅行调试、函数式编程: Redux 或 Redux Toolkit 依然是强大的选择。
- 需要强类型和模型定义: MobX-State-Tree 可能更适合。
MobX 并不仅仅是 Redux 的替代品,它提供了一种完全不同的心智模型。对于许多开发者来说,MobX 的直观性可以显著降低状态管理的认知负担,让他们更专注于业务逻辑的实现。
总结
至此,我们已经全面而深入地探讨了 React MobX 的基础教程。从理解状态管理的痛点开始,我们学习了 MobX 的核心概念:observable
(可观察状态)、action
(动作)、computed
(计算值)和 reaction
(反应)。我们还详细讲解了如何将 MobX 与 React 组件集成,通过 observer
和 Context API 实现了 UI 与状态的无缝连接。最后,我们构建了一个完整的 Todo 应用示例,并讨论了 MobX 的高级概念和最佳实践,包括严格模式、调试、异步处理、性能优化和测试。
MobX 以其独特的响应式编程模型,为 React 开发者提供了一条“告别繁琐状态管理”的康庄大道。它减少了大量的样板代码,让状态管理变得更加直观和高效。如果你厌倦了繁琐的配置和严格的不可变性约束,MobX 绝对值得你投入时间去学习和实践。它将帮助你构建更加简洁、高性能且易于维护的 React 应用,让你真正专注于创造价值,而不是被状态管理的复杂性所困扰。
开始你的 MobX 之旅吧!