MobX 是一个简单的可扩展的状态管理库,无样板代码风格简约。
目前最新版本为 6,版本 4 和版本 5 已不再支持。
在 MobX 6 中不推荐使用装饰器语法,因为它不是 ES 标准,并且标准化过程要花费很长时间,但是通过配置仍然可以启用装饰器语法。
MobX 可以运行在任何支持 ES5 的环境中,包含浏览器和 Node。
MobX 通常和 React 配合使用,但是在 Angular 和 Vue 中也可以使用 MobX。
yarn add [email protected] [email protected]
计数器:在组件中显示数值状态,单击按钮使数值加一,单击按钮使数值重置。
// Counter Component -> Counter 组件
// observer: 监控当前组件使用到的由 MobX 跟踪的 observable state, 当状态发生变化时通知 React 更新视图
import { observer } from "mobx-react-lite"
function Counter({ counterStore }) {
return (
{counterStore.count}
)
}
export default observer(Counter)
// Counter Store -> 管理 Counter 组件的 Store
import { makeAutoObservable } from "mobx"
class CounterStore {
// 数值状态
count = 10
constructor() {
// 将参数对象中的属性设置为 observable state
// 将参数对象中的方法设置为 action
makeAutoObservable(this)
}
// 使数值状态加一
increment() {
this.count += 1
}
// 重置数值状态
reset() {
this.count = 0
}
}
export default CounterStore
// App Component -> 根组件
// 导入 Counter 组件
import Counter from "./components/Counter/Counter"
// 导入管理 Counter 组件的 Store
import CounterStore from "./stores/Counter/CounterStore"
// 创建管理 Counter 组件的 Store 实例对象
const counterStore = new CounterStore()
function App() {
// 调用 Counter 组件并传入管理其状态的 Store
return
}
export default App
// target: 将目标对象中的属性和方法设置为 observable state 和 action
// overrides: 覆盖默认设置, 将 target 对象中的某些属性或者方法设置为普通属性
// options: 配置对象, autoBind, 使 action 方法始终拥有正确的 this 指向
makeAutoObservable(target, overrides?, options?)
makeAutoObservable(this, {reset: false}, {autoBind: true})
状态变化更新视图的必要条件
observable state
action
方法observer
方法包裹可以使用 makeAutoObservable
方法将对象属性设置为 observable state
,将对象方法设置为 action
方法
可以使用 observer
方法监控当前组件使用到的由 MobX 跟踪的 observable state
,当状态发生变化时通知 React 更新视图
创建 TodoListStore,用于管理待办事项列表状态。
// TodoListStore.js
class TodoListStore {
todos = []
constructor(todos) {
if (todos) this.todos = todos
}
}
export default TodoListStore
创建 TodoViewStore,用于管理待办事项状态。
// TodoViewStore.js
class TodoViewStore {
id = Math.random()
title = ""
completed = false
constructor(title) {
this.title = title
}
}
export default TodoViewStore
创建 TodoListStore 实例对象并将其传入到 TodoListView 组件
// App.js
import TodoListView from "./components/Todos/TodoListView"
import TodoListStore from "./stores/Todos/TodoListStore"
import TodoStore from "./stores/Todos/TodoStore"
const counterStore = new CounterStore()
const todoListStore = new TodoListStore([
new TodoStore("Hello MobX"),
new TodoStore("Hello React")
])
function App() {
return
}
渲染初始待办事项列表
// TodoListView.js
function TodoListView({ TodoListStore }) {
return (
{TodoListStore.todos.map(todo => (
))}
)
}
// TodoView.js
function TodoView({ todo }) {
return
}
在 TodoListStore 类中创建 createTodo 方法,用于向 todos 数组中添加待办事项
// TodoListStore.js
import TodoStore from "./TodoStore"
class TodoListStore {
createTodo(title) {
this.todos.push(new TodoStore(title))
}
}
在 TodoHeader 组件中调用 createTodo 方法创建待办事项
// TodoListView.js
function TodoListView({ TodoListStore }) {
return TodoListStore.createTodo(title)} />
}
// TodoHeader.js
import { useState } from "react"
function TodoHeader({ createTodo }) {
const [title, setTitle] = useState("")
return (
setTitle(event.target.value)}
onKeyUp={event => {
if (event.key === "Enter") {
createTodo(title)
setTitle("")
}
}}
/>
)
}
设置状态变化更新视图的必要条件
// TodoListStore.js
import { action, makeObservable, observable } from "mobx"
class TodoListStore {
constructor() {
makeObservable(this, {
todos: observable,
createTodo: action
})
}
}
// TodoListView.js
import { observer } from "mobx-react-lite"
function TodoListView (){}
export default observer(TodoListView)
我们希望在每个组件中都能够直接获取到 TodoListStore,而不是通过 props 属性传递的方式。
// TodoListStore.js
import { createContext, useContext } from "react"
const TodoListStoreContext = createContext()
const TodoListStoreProvider = ({ store, children }) => {
return (
{children}
)
}
const useTodoStore = () => {
return useContext(TodoListStoreContext)
}
export { TodoListStore, TodoListStoreProvider, useTodoStore }
// App.js
import { TodoListStore, TodoListStoreProvider } from "./stores/Todos/TodoListStore"
function App() {
return (
)
}
// TodoListView.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore"
function TodoListView() {
const todoListStore = useTodoListStore()
}
import { useTodoListStore } from "../../stores/Todos/TodoListStore"
function TodoHeader() {
const todoListStore = useTodoListStore()
todoListStore.createTodo(title)
}
我们希望在每个组件中都能够直接获取到 TodoListStore,而不是通过 props 属性传递的方式。
// TodoListStore.js
import { createContext, useContext } from "react"
const TodoListStoreContext = createContext()
const TodoListStoreProvider = ({ store, children }) => {
return (
{children}
)
}
const useTodoStore = () => {
return useContext(TodoListStoreContext)
}
export { TodoListStore, TodoListStoreProvider, useTodoStore }
// App.js
import { TodoListStore, TodoListStoreProvider } from "./stores/Todos/TodoListStore"
function App() {
return (
)
}
// TodoListView.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore"
function TodoListView() {
const todoListStore = useTodoListStore()
}
import { useTodoListStore } from "../../stores/Todos/TodoListStore"
function TodoHeader() {
const todoListStore = useTodoListStore()
todoListStore.createTodo(title)
}
// TodoListStore.js
class TodoListStore {
constructor() {
makeObservable(this, {
removeTodo: action
})
}
removeTodo(id) {
const index = this.todos.findIndex(todo => todo.id === id)
this.todos.splice(index, 1)
}
}
// TodoView.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore"
function TodoView({ todo }) {
const todoListStore = useTodoListStore()
return
// TodoStore.js
import { makeObservable, observable, action } from "mobx"
class TodoStore {
completed = false
constructor() {
makeObservable(this, {
completed: observable,
toggle: action
})
}
toggle() {
this.completed = !this.completed
}
}
// TodoView.js
import { observer } from "mobx-react-lite"
function TodoView({ todo }) {
return (
todo.toggle()} className="toggle" type="checkbox" />
)
}
export default observer(TodoView)
todo.toggle()} />
// this 指向会发生错误
makeObservable(this, {
toggle: action.bound
})
待办事项数量属于派生状态,即该状态依赖现有状态 (todos) 生成。
派生状态可以使用计算值实现,当依赖状态发生变化后,计算值自动更新。
// TodoListStore.js
import { computed } from "mobx"
class TodoListStore {
constructor() {
makeObservable(this, {
unCompletedTodoCount: computed
})
}
get unCompletedTodoCount() {
return this.todos.filter(todo => !todo.completed).length
}
}
// TodoFooter.js
import { useTodoListStore } from "../../stores/Todos/TodoListStore"
import { observer } from "mobx-react-lite"
function TodoFooter() {
const todoListStore = useTodoListStore()
return {todoListStore.unCompletedTodoCount} item left
}
export default observer(TodoFooter)
注意:计算值是被缓存的。
get unCompletedTodoCount() {
console.log("unCompletedTodoCount")
return this.todos.filter(todo => !todo.completed).length
}
{todoListStore.unCompletedTodoCount}
{todoListStore.unCompletedTodoCount}
{todoListStore.unCompletedTodoCount}
// 计算属性被调用多次, 但是方法内部的console.log 只会输出一次, 说明计算属性是被缓存的.
待办事项过滤为计算值的练习任务。
// TodoListStore.js
class TodoListStore {
todos = []
filter = "all"
constructor() {
makeObservable(this, {
filter: observable,
changeFilter: action,
filterTodos: computed
})
}
get filterTodos() {
switch (this.filter) {
case "all":
return this.todos
case "active":
return this.todos.filter(todo => !todo.completed)
case "completed":
return this.todos.filter(todo => todo.completed)
default:
return this.todos
}
}
changeFilter(filter) {
this.filter = filter
}
}
// TodoFooter.js
function TodoFooter() {
const todoListStore = useTodoListStore()
return (
)
}
// TodoListView.js
function TodoListView() {
const todoListStore = useTodoListStore()
return (
{todoListStore.filterTodos.map(todo => )}
)
}
下载并启动 json-server
npm install -g json-server
json-server ./src/todo.json --port 3005
yarn add axios
创建 loadTodos 方法用于加载初始任务
// TodoListStore.js
import axios from "axios"
import { runInAction } from "mobx"
class TodoListStore {
constructor(todos) {
this.loadTodos()
}
async loadTodos() {
let todos = await axios.get("http://localhost:3005/todos").then(response => response.data)
runInAction(() => todos.forEach(todo => this.todos.push(todo)))
}
}
解决远端加载待办事项无法切换状态的问题
原因是远端加载待办事项的任务对象的原型对象上没有 toggle 方法。
import TodoStore from "./TodoStore"
class TodoListStore {
async loadTodos() {
runInAction(() => todos.forEach(todo => this.todos.push(new TodoStore(todo.title))))
}
}
通过创建 RootStore 可以将 CounterStore 和 TodoListStore 进行合并,实现在任何组件中都可以访问任何状态,方便全局状态共享。
// RootStore.js
import CounterStore from "./Counter/CounterStore"
import TodoListStore from "./Todos/TodoListStore"
import { createContext, useContext } from "react"
class RootStore {
constructor() {
this.counterStore = new CounterStore()
this.todoListStore = new TodoListStore()
}
}
const RootStoreContext = createContext()
const RootStoreProvider = ({ store, children }) => {
return (
{children}
)
}
const useRootStore = () => {
return useContext(RootStoreContext)
}
export { RootStore, RootStoreProvider, useRootStore }
// App.js
import Counter from "./components/Counter/Counter"
import TodoListView from "./components/Todos/TodoListView"
import { RootStore, RootStoreProvider } from "./stores/RootStore"
const rootStore = new RootStore()
function App() {
return (
)
}
export default App
// 消费 RootStore 的其他组件
import { useRootStore } from "../../stores/RootStore"
function TodoHeader() {
const { todoListStore } = useRootStore()
}
function Counter() {
const { counterStore } = useRootStore()
}
监控数据变化执行副作用,接收一个函数作为参数,参数函数用来执行副作用,当参数函数内部使用的 observable state, computed 发生变化时函数会运行,初始运行 autorun 方法时参数函数也会运行一次。
import { autorun } from "mobx"
import { useEffect } from "react"
function Counter() {
const { counterStore } = useRootStore()
useEffect(() => {
// 确保 autorun 方法只被初始化一次
autorun(() => {
console.log(counterStore.count)
})
}, [])
}
对于基本数据类型,属于值传递,mobx 只能跟踪到原始属性,跟踪不到复制后的值。
useEffect(() => {
let count = counterStore.count
autorun(() => {
// 错误写法, mobx 跟踪不到变量 count
console.log(count)
})
}, [])
对于引用数据类型,只要引用地址不发生变化,mobx 就可以进行跟踪
// CounterStore.js
class CounterStore {
person = { name: "张三" }
}
// Counter 组件
function Counter() {
const { counterStore } = useRootStore()
useEffect(() => {
const person = counterStore.person
autorun(() => {
console.log(person.name)
})
}, [])
return (
{counterStore.person.name}
)
}
去除控制台中和 useEffect 相关的 ⚠️
在 react 17 版本中,官方团队修改了脚手架工具,允许直接在外部声明 .eslintrc 文件覆盖 eslint 配置,不需要使用 package.json, react-app-rewired 和 customize-cra 就可用实现 eslint 配置
在项目的根目录下新建 .eslintrc.js
文件并加入如下内容
module.exports = {
plugins: ["react-hooks"],
rules: {
"react-hooks/exhaustive-deps": 0
}
}
注意此处不需要下载 eslint-plugin-react-hooks 插件,create-react-app
脚手架工具内部已经内置。
配置完成后重新启动应用。
监控状态变化执行副作用,接收两个函数作为参数,第一个函数返回要监控的状态,第二个函数用来执行副作用,只有当第一个函数返回的状态发生变化时,第二个函数才会执行。reaction 方法提供了更加细颗粒度的状态控制。
和 autorun 不同,reaction 初始时不会执行副作用。
import { reaction } from "mobx"
function Counter() {
useEffect(() => {
reaction(
() => counterStore.count,
(current, previous) => {
console.log(current)
console.log(previous)
}
)
}, [])
}