React MobX 基础教程:告别繁琐状态管理 – wiki基地


React MobX 基础教程:告别繁琐状态管理

在现代前端应用的开发中,状态管理无疑是核心且复杂的议题之一。随着应用规模的增长,数据流动变得错综复杂,如何高效、可维护地管理共享状态成为了开发者面临的巨大挑战。React 本身提供了 useStateuseContext 等 Hooks 来处理组件内部和跨组件的状态,但在面对全局或复杂应用状态时,它们往往显得力不从心。

此时,专业的状态管理库便应运而生。在众多选择中,Redux 以其严格的单向数据流和强大的生态系统占据了一席之地。然而,Redux 的学习曲线陡峭,且伴随着大量的样板代码(action creators, reducers, selectors 等),常常让开发者望而却步,感叹其“繁琐”。

今天,我们将深入探讨另一个强大而优雅的解决方案——MobX。MobX 以其直观的响应式编程模型,承诺让状态管理变得简单、高效,并真正帮助开发者“告别繁琐状态管理”。

第一章:状态管理的困境与 MobX 的登场

1.1 为什么状态管理如此重要?

在 React 应用中,状态(State)是驱动 UI 渲染和逻辑执行的核心数据。
* 数据共享: 多个组件需要访问或修改同一份数据。
* 数据一致性: 确保所有依赖同一份数据的组件都能实时反映最新的数据状态。
* 复杂逻辑: 应用中可能存在异步操作、副作用、数据缓存等复杂逻辑,需要一个清晰的机制来管理这些过程。
* 可维护性: 随着项目规模扩大,混乱的状态管理会使代码难以理解、调试和扩展。

React 提供的 useStateuseReducer 适用于组件内部状态或局部状态树,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 “我被修改了”。

如何定义:

  1. 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();
    “`

  2. @observable (装饰器,需要配置 Babel/TypeScript)
    这种方式更简洁,但需要构建工具支持。

    “`javascript
    import { observable, action, computed } from “mobx”;
    import { observer } from “mobx-react-lite”; // 注意导入observer

    class 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.shallowobservable.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 中,addTodosetFilter 方法都被标记为 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 中,filteredTodosremainingCount 都是计算值。

“`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.filterthis.todos发生变化时,filteredTodos才会重新计算。当this.todos中任意 Todo 的completed状态变化时,remainingCount才会重新计算。如果这些依赖没有变化,即使多次访问filteredTodosremainingCount`,它们也只会返回上次缓存的结果。

2.4 Reaction (反应)

定义: Reaction 是 MobX 中一种独立于 UI 的副作用。它们是当可观察状态发生变化时,自动执行的函数。与 Computed 的区别在于:Computed产生一个值,而 Reaction产生一个副作用(如打印日志、网络请求、更新 DOM 等)。

MobX 提供了三种主要的 Reaction:

  1. autorun(effect: () => void)

    • 接收一个函数作为参数。
    • 该函数中使用的所有可观察状态都会被自动追踪。
    • autorun 定义时立即执行一次,之后每当其依赖的任何可观察状态变化时,都会重新执行。
    • 适合: 打印日志、发送分析数据、同步本地存储等,无需返回值且依赖相对简单的场景。

    “`javascript
    import { autorun } from “mobx”;

    autorun(() => {
    console.log(当前过滤条件: ${todoStore.filter});
    console.log(待办事项数量: ${todoStore.remainingCount});
    });
    // 初始执行一次
    // 之后每次 filter 或 remainingCount 变化时都会执行
    “`

  2. 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(“恭喜!所有待办事项已完成!”);
    }
    }
    );
    “`

  3. 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:
autorunreaction 都返回一个清理函数。当组件卸载或不再需要监听时,调用这个清理函数可以停止 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

并在 .babelrcbabel.config.js 中配置:
json
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}

3.2 observer 高阶组件/Hook

observermobx-react-litemobx-react 提供的核心工具。它的作用是:将 React 组件转换为一个“观察者”。当这个组件渲染时,MobX 会追踪它所使用的所有可观察数据。一旦这些数据发生变化,observer 就会通知 React 重新渲染这个组件。

为什么需要 observer
React 的渲染机制是基于 Props 或 State 的变化。MobX 的状态是可变的,当你在 MobX Store 中直接修改 todo.completed = true 时,React 组件并不知道底层数据发生了变化,它不会自动重渲染。observer HOC/Hook 解决了这个问题,它拦截了 MobX 状态的变化通知,并将其转化为 React 的 forceUpdate 或类似的更新机制,从而触发组件重渲染。

  1. 对于函数式组件(推荐使用 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` 会重新渲染。

  2. 对于类组件(使用 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。

  1. 创建 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);
    “`

  2. 在应用根部提供 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;
    “`

  3. 在组件中消费 Store:

    “`jsx
    // components/TodoInput.jsx
    import React, { useState } from ‘react’;
    import { observer } from ‘mobx-react-lite’;
    import { useTodoStore } from ‘../stores’; // 导入自定义 Hook

    const 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}>&times;</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 提供了多种调试手段:

  1. MobX DevTools: Chrome/Firefox 浏览器扩展,可以可视化 MobX 状态树,追踪 Action,查看依赖关系。这是最强大的调试工具。
  2. 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`。
    4. **`spy()`:** 监听所有 MobX 事件(action, reaction, computed 计算等)。适合在需要全面了解 MobX 内部行为时使用。
    javascript
    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 默认提供了细粒度的更新,但在某些情况下,你仍然可以进一步优化性能:

  1. 正确使用 observer 确保只有需要响应式更新的组件才被 observer 包裹。
  2. 粒度控制: 如果一个对象包含大量属性,并且只有少数属性需要被观察,可以考虑使用 observable.ref 或将对象拆分为更小的可观察对象。
    • observable.ref: 只有引用变化时才触发,不观察对象内部。
    • observable.shallow: 只有第一层属性被观察。
  3. 计算值(Computed)的效率: 确保 computed 属性的 getter 函数是纯粹的,没有副作用,并且其计算成本不要过高。如果计算成本高,确保其依赖尽可能少地变化。
  4. 避免在循环中创建 observable 在循环中频繁创建 MobX 的 observable 实例可能会有开销。如果数组中的对象都是一样的结构,可以预先将它们定义为可观察的类。
  5. React memoobserver 结合: 如果一个 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 生态

除了核心的 mobxmobx-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 之旅吧!

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部