React ---Mobx 6 的使用及案例
MobX61. MobX 概述MobX 是一个简单的可扩展的状态管理库,无样板代码风格简约。目前最新版本为 6,版本 4 和版本 5 已不再支持。在 MobX 6 中不推荐使用装饰器语法,因为它不是 ES 标准,并且标准化过程要花费很长时间,但是通过配置仍然可以启用装饰器语法。MobX 可以运行在任何支持 ES5 的环境中,包含浏览器和 Node。MobX 通常和 React 配合使用,但是在 A
MobX6
1. MobX 概述
MobX 是一个简单的可扩展的状态管理库,无样板代码风格简约。
目前最新版本为 6,版本 4 和版本 5 已不再支持。
在 MobX 6 中不推荐使用装饰器语法,因为它不是 ES 标准,并且标准化过程要花费很长时间,但是通过配置仍然可以启用装饰器语法。
MobX 可以运行在任何支持 ES5 的环境中,包含浏览器和 Node。
MobX 通常和 React 配合使用,但是在 Angular 和 Vue 中也可以使用 MobX。
2. MobX 光速入门
2.1 下载
- mobx:MobX 核心库
- mobx-react-lite:仅支持函数组件
- mobx-react:既支持函数组件也支持类组件
yarn add mobx@6.1.8 mobx-react-lite@3.2.0
2.2 核心概念
- observable state:被 MobX 跟踪的状态。
- action:允许修改状态的方法,在严格模式下只有 action 方法被允许修改状态。
- computed:根据应用程序状态派生的新值,计算值。
2.3 工作流程
2.4 入门案例
2.4.1 案例
计数器:在组件中显示数值状态,单击按钮使数值加一,单击按钮使数值重置。
// Counter Component -> Counter 组件
// observer: 监控当前组件使用到的由 MobX 跟踪的 observable state, 当状态发生变化时通知 React 更新视图
import { observer } from "mobx-react-lite"
function Counter({ counterStore }) {
return (
<div>
<p className="paragraph">{counterStore.count}</p>
<button onClick={() => counterStore.increment()} className="button">加 1</button>
<button onClick={() => counterStore.reset()} className="button">重置</button>
</div>
)
}
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 <Counter counterStore={counterStore} />
}
export default App
2.4.2 makeAutoObservable
// target: 将目标对象中的属性和方法设置为 observable state 和 action
// overrides: 覆盖默认设置, 将 target 对象中的某些属性或者方法设置为普通属性
// options: 配置对象, autoBind, 使 action 方法始终拥有正确的 this 指向
makeAutoObservable(target, overrides?, options?)
makeAutoObservable(this, {reset: false}, {autoBind: true})
2.4.3 总结
状态变化更新视图的必要条件
- 状态需要被标记为
observable state
- 更改状态的方法需要被标记为
action
方法 - 组件视图必须通过
observer
方法包裹
可以使用 makeAutoObservable
方法将对象属性设置为 observable state
,将对象方法设置为 action
方法
可以使用 observer
方法监控当前组件使用到的由 MobX 跟踪的 observable state
,当状态发生变化时通知 React 更新视图
3. TodoList
3.1 创建初始 TodoListStore
-
创建 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 TodoListStore={todoListStore} /> }
-
渲染初始待办事项列表
// TodoListView.js function TodoListView({ TodoListStore }) { return ( <ul className="todo-list"> {TodoListStore.todos.map(todo => ( <TodoView key={todo.id} todo={todo} /> ))} </ul> ) }
// TodoView.js function TodoView({ todo }) { return <label>{todo.title}</label> }
3.2 创建待办事项
-
在 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 <TodoHeader createTodo={title => TodoListStore.createTodo(title)} /> }
// TodoHeader.js import { useState } from "react" function TodoHeader({ createTodo }) { const [title, setTitle] = useState("") return ( <header className="header"> <input value={title} onChange={event => setTitle(event.target.value)} onKeyUp={event => { if (event.key === "Enter") { createTodo(title) setTitle("") } }} /> </header> ) }
-
设置状态变化更新视图的必要条件
// 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)
3.3 创建 Store 上下文
我们希望在每个组件中都能够直接获取到 TodoListStore,而不是通过 props 属性传递的方式。
// TodoListStore.js
import { createContext, useContext } from "react"
const TodoListStoreContext = createContext()
const TodoListStoreProvider = ({ store, children }) => {
return (
<TodoListStoreContext.Provider value={store}>
{children}
</TodoListStoreContext.Provider>
)
}
const useTodoStore = () => {
return useContext(TodoListStoreContext)
}
export { TodoListStore, TodoListStoreProvider, useTodoStore }
// App.js
import { TodoListStore, TodoListStoreProvider } from "./stores/Todos/TodoListStore"
function App() {
return (
<TodoListStoreProvider store={todoListStore}>
<TodoListView/>
<Counter counterStore={counterStore} />
</TodoListStoreProvider>
)
}
// 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)
}
3.3 创建 Store 上下文
我们希望在每个组件中都能够直接获取到 TodoListStore,而不是通过 props 属性传递的方式。
// TodoListStore.js
import { createContext, useContext } from "react"
const TodoListStoreContext = createContext()
const TodoListStoreProvider = ({ store, children }) => {
return (
<TodoListStoreContext.Provider value={store}>
{children}
</TodoListStoreContext.Provider>
)
}
const useTodoStore = () => {
return useContext(TodoListStoreContext)
}
export { TodoListStore, TodoListStoreProvider, useTodoStore }
// App.js
import { TodoListStore, TodoListStoreProvider } from "./stores/Todos/TodoListStore"
function App() {
return (
<TodoListStoreProvider store={todoListStore}>
<TodoListView/>
<Counter counterStore={counterStore} />
</TodoListStoreProvider>
)
}
// 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)
}
3.4 删除待办事项
// 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 <button onClick={() => todoListStore.removeTodo(todo.id)} className="destroy" />
}
3.5 更改任务状态
// 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 (
<li className={todo.completed ? "completed" : ""}>
<input checked={todo.completed} onChange={() => todo.toggle()} className="toggle" type="checkbox" />
</li>
)
}
export default observer(TodoView)
3.6 更正 this 指向
<input onChange={() => todo.toggle()} />
<input onChange={todo.toggle}/> // this 指向会发生错误
makeObservable(this, {
toggle: action.bound
})
3.7 计算待办事项数量
待办事项数量属于派生状态,即该状态依赖现有状态 (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 <strong>{todoListStore.unCompletedTodoCount}</strong> 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 只会输出一次, 说明计算属性是被缓存的.
3.8 待办事项过滤
待办事项过滤为计算值的练习任务。
// 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 (
<footer className="footer">
<ul className="filters">
<li>
<button
onClick={() => todoListStore.changeFilter("all")}
className={todoListStore.filter === "all" ? "selected" : ""}
>
All
</button>
</li>
<li>
<button
onClick={() => todoListStore.changeFilter("active")}
className={todoListStore.filter === "active" ? "selected" : ""}
>
Active
</button>
</li>
<li>
<button
onClick={() => todoListStore.changeFilter("completed")}
className={todoListStore.filter === "completed" ? "selected" : ""}
>
Completed
</button>
</li>
</ul>
</footer>
)
}
// TodoListView.js
function TodoListView() {
const todoListStore = useTodoListStore()
return (
<ul className="todo-list">
{todoListStore.filterTodos.map(todo => <TodoView key={todo.id} todo={todo} />)}
</ul>
)
}
3.9 加载远端任务
-
下载并启动 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)))) } }
3.9 创建 RootStore
通过创建 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 (
<RootStoreContext.Provider value={store}>
{children}
</RootStoreContext.Provider>
)
}
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 (
<RootStoreProvider store={rootStore}>
<TodoListView />
<Counter />
</RootStoreProvider>
)
}
export default App
// 消费 RootStore 的其他组件
import { useRootStore } from "../../stores/RootStore"
function TodoHeader() {
const { todoListStore } = useRootStore()
}
function Counter() {
const { counterStore } = useRootStore()
}
4. 数据监测
4.1 autorun 方法
-
监控数据变化执行副作用,接收一个函数作为参数,参数函数用来执行副作用,当参数函数内部使用的 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 ( <div> <p>{counterStore.person.name}</p> <button onClick={() => runInAction(() => (counterStore.person.name = "李四"))}>李四</button> <button onClick={() => runInAction(() => (counterStore.person = { name: "王五" }))}>王五</button> </div> ) }
-
去除控制台中和 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
脚手架工具内部已经内置。配置完成后重新启动应用。
4.2 reaction 方法
监控状态变化执行副作用,接收两个函数作为参数,第一个函数返回要监控的状态,第二个函数用来执行副作用,只有当第一个函数返回的状态发生变化时,第二个函数才会执行。reaction 方法提供了更加细颗粒度的状态控制。
和 autorun 不同,reaction 初始时不会执行副作用。
import { reaction } from "mobx"
function Counter() {
useEffect(() => {
reaction(
() => counterStore.count,
(current, previous) => {
console.log(current)
console.log(previous)
}
)
}, [])
}
更多推荐
所有评论(0)