加载远端任务
加入请求服务端数据的异步任务。
Redux 中通过 saga 或 thunk 中间件完成。
MobX 通过 runInAction
创建一个立即触发的临时 Action,用于在异步操作完成之后调用,更新状态。
配置远端请求
配置远端请求
# 使用 json 文件模拟 restapi
npm install -g json-server
json-server ./src/todo.json --port 3005
./src/todo.json
:
{
"todos": [
{
"id": 1,
"title": "React",
"completed": false
},
{
"id": 2,
"title": "Angular",
"completed": false
},
{
"id": 3,
"title": "Vue",
"completed": false
}
]
}
安装 axios
npm install axios
创建 loadTodos 加载初始数据
// src\stores\TodoStore\TodoListStore.js
import { makeObservable, observable, action, computed } from "mobx";
import TodoViewStore from "./TodoViewStore";
import { createContext, useContext } from 'react'
import axios from 'axios'
class TodoListStore {
// 待办列表
todos = []
filter = 'all'
constructor(todos) {
if (todos) this.todos = todos
// 转化状态和方法
makeObservable(this, {
todos: observable,
filter: observable,
createTodo: action,
deleteTodo: action,
clear: action.bound,
changeFilter: action.bound,
unCompletedTodoCount: computed,
filterTodos: computed,
})
// 初始化数据
this.loadTodos()
}
// ...
// 加载初始数据
async loadTodos() {
const todos = await axios.get('http://localhost:3005/todos').then(response => response.data)
this.todos.push(...todos.map(todo => new TodoViewStore(todo.title)))
}
}
// ...
此时控制台会发出警告:[MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed.
这是因为,异步方法是不能作为 Action 方法的。
使用 runInAction
方法,它表示在 Action 中执行操作。
它接收的回调函数内执行的内容就是要执行的操作。
// src\stores\TodoStore\TodoListStore.js
import { makeObservable, observable, action, computed, runInAction } from "mobx";
import TodoViewStore from "./TodoViewStore";
import { createContext, useContext } from 'react'
import axios from 'axios'
class TodoListStore {
// 待办列表
todos = []
filter = 'all'
constructor(todos) {
if (todos) this.todos = todos
// 转化状态和方法
makeObservable(this, {
todos: observable,
filter: observable,
createTodo: action,
deleteTodo: action,
clear: action.bound,
changeFilter: action.bound,
unCompletedTodoCount: computed,
filterTodos: computed,
})
// 初始化数据
this.loadTodos()
}
// ...
// 加载初始数据
async loadTodos() {
const todos = await axios.get('http://localhost:3005/todos').then(response => response.data)
runInAction(() => this.todos.push(...todos.map(todo => new TodoViewStore(todo.title))))
}
}
// ...
创建 RootStore
应用中同时使用 CounterStore 和 TodoListStore。
在一个应用中有多个 Store 是非常混乱的,如果要使用 context 上下文,那每个 Store 都要通过一层 Provider 去共享状态。
最好将它们合并成一个 RootStore,方便在全局共享状态,实现在任何组件中都可以访问任何状态。
创建 RootStore,合并多个 Store:
// src\stores\RootStore.js
import TodoListStore from './TodoStore/TodoListStore'
import CounterStore from './CounterStore/CounterStore'
import { createContext, useContext } from 'react'
class RootStore {
constructor() {
this.counterStore = new CounterStore()
this.todoListStore = new TodoListStore()
}
}
// 创建上下文
const RootStoreContext = createContext()
// 新建组件:通过 Provider 传递上下文中的数据
const RootStoreProvider = ({ store, children }) => {
return (
<RootStoreContext.Provider value={store}>
{children}
</RootStoreContext.Provider>
)
}
// 新建钩子函数:获取上下文中的数据(Store 对象)
const useRootStore = () => {
return useContext(RootStoreContext)
}
export { RootStore, RootStoreProvider, useRootStore }
修改 TodoListStore
// src\stores\TodoStore\TodoListStore.js
import { makeObservable, observable, action, computed, runInAction } from "mobx";
import TodoViewStore from "./TodoViewStore";
import axios from 'axios'
class TodoListStore {
// 待办列表
todos = []
filter = 'all'
constructor(todos) {
if (todos) this.todos = todos
// 转化状态和方法
makeObservable(this, {
todos: observable,
filter: observable,
createTodo: action,
deleteTodo: action,
clear: action.bound,
changeFilter: action.bound,
unCompletedTodoCount: computed,
filterTodos: computed,
})
// 初始化数据
this.loadTodos()
}
// 未完成事件
get unCompletedTodoCount() {
return this.todos.filter(({completed}) => !completed).length
}
// 事件列表
get filterTodos() {
switch (this.filter) {
case 'all':
return this.todos
case 'active':
return this.todos.filter(({completed}) => !completed)
case 'completed':
return this.todos.filter(({completed}) => completed)
default:
return this.todos
}
}
// 新增待办
createTodo(title) {
this.todos.push(new TodoViewStore(title))
}
// 删除待办
deleteTodo(todoId) {
const todoIndex = this.todos.findIndex(({id}) => id === todoId)
this.todos.splice(todoIndex, 1)
}
// 清空待办
clear() {
this.todos = []
}
// 过滤
changeFilter(filter) {
this.filter = filter
}
// 加载初始数据
async loadTodos() {
const todos = await axios.get('http://localhost:3005/todos').then(response => response.data)
runInAction(() => this.todos.push(...todos.map(todo => new TodoViewStore(todo.title))))
}
}
export default TodoListStore
修改 App.js
// src\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}>
<Counter />
<TodoListView />
</RootStoreProvider>
)
}
export default App
修改 Counter 组件
// src\components\Counter.js
import { observer } from 'mobx-react-lite'
+ import { useRootStore } from '../../stores/RootStore'
- function Counter({counterStore}) {
+ function Counter() {
const { counterStore } = useRootStore()
return (
<div>
<span>{counterStore.count}</span>
<button onClick={() => counterStore.increment()}>+</button>
<button onClick={() => counterStore.reset()}>重置</button>
</div>
)
}
export default observer(Counter)
修改 TodoList 相关组件
// src\components\Todos\TodoFooter.js
import { observer } from "mobx-react-lite"
- import { useTodoListStore } from "../../stores/TodoStore/TodoListStore"
+ import { useRootStore } from '../../stores/RootStore'
function TodoFooter() {
- const todoListStore = useTodoListStore()
+ const { todoListStore } = useRootStore()
const { filter, changeFilter } = todoListStore
return (
<footer className="footer">
<span className="todo-count">
<strong>{todoListStore.unCompletedTodoCount}</strong> item left
</span>
<ul className="filters">
<li>
<button className={filter === 'all' ? 'selected' : ''} onClick={() => changeFilter('all')}>All</button>
</li>
<li>
<button className={filter === 'active' ? 'selected' : ''} onClick={() => changeFilter('active')}>Active</button>
</li>
<li>
<button className={filter === 'completed' ? 'selected' : ''} onClick={() => changeFilter('completed')}>Completed</button>
</li>
</ul>
<button className="clear-completed" onClick={todoListStore.clear}>Clear completed</button>
</footer>
)
}
export default observer(TodoFooter)
// src\components\Todos\TodoHeader.js
import { useState } from 'react'
- import { useTodoListStore } from '../../stores/TodoStore/TodoListStore'
+ import { useRootStore } from '../../stores/RootStore'
function TodoHeader() {
// 使用 `useState` 钩子函数管理 input 的 value 和 onChange
const [title, setTitle] = useState('')
- const todoListStore = useTodoListStore()
+ const { todoListStore } = useRootStore()
return (
<header className="header">
<h1>todos</h1>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
value={title}
onChange={event => setTitle(event.target.value)}
onKeyUp={event => {
if (event.key === 'Enter') {
// 新增待办
todoListStore.createTodo(event.target.value)
// 清空输入框
setTitle('')
}
}}
/>
</header>
)
}
export default TodoHeader
// src\components\Todos\TodoListView.js
import TodoHeader from "./TodoHeader"
import TodoFooter from "./TodoFooter"
import TodoView from "./TodoView"
import { observer } from 'mobx-react-lite'
- import { useTodoListStore } from '../../stores/TodoStore/TodoListStore'
+ import { useRootStore } from '../../stores/RootStore'
function TodoListView() {
- const todoListStore = useTodoListStore()
+ const { todoListStore } = useRootStore()
return (
<section className="todoapp">
{/* 将方法传递给组件 */}
<TodoHeader />
<section className="main">
<input className="toggle-all" type="checkbox" />
<ul className="todo-list">
{
todoListStore.filterTodos.map(todo => (
<TodoView todo={todo} key={todo.id} />
))
}
</ul>
</section>
<TodoFooter />
</section>
)
}
// 使用 `observer` 包裹组件监听状态变化
export default observer(TodoListView)
// src\components\Todos\TodoView.js
import { observer } from 'mobx-react-lite'
- import { useTodoListStore } from '../../stores/TodoStore/TodoListStore'
+ import { useRootStore } from '../../stores/RootStore'
function TodoView({ todo }) {
- const todoListStore = useTodoListStore()
+ const todoListStore = useTodoListStore()
return (
<li className={todo.completed ? 'completed' : ''}>
<div className="view">
<input className="toggle" type="checkbox" checked={todo.completed} onChange={todo.toggle} />
<label>{todo.title}</label>
<button className="destroy" onClick={() => todoListStore.deleteTodo(todo.id)} />
</div>
<input className="edit" />
</li>
)
}
export default observer(TodoView)
数据监测
类似 useEffect
,当数据发生变化时执行一些操作(副作用)。
MobX 提供了一些方法,下面介绍其中的 autorun
和 reaction
方法。
autorun
监控数据变化执行副作用,接收一个函数作为参数,这个函数用来执行副作用。
- 当参数函数内部使用的 Observable State、Computed 发生变化时函数就会执行。
- 当初始运行 autorun 方法时,参数函数也会运行一次。
使用 useEffect 确保 autorun 只执行一次
当前使用的是函数式组件,当状态发生变化,函数组件会被重新调用。
而 autorun 在初始调用的时候会执行传入的函数。
所以状态一经变化,autorun 传入的函数将被执行两次。
使用 useEffect 可以确保 autorun 只执行一次:
// src\components\Counter.js
import { observer } from 'mobx-react-lite'
import { useRootStore } from '../../stores/RootStore'
import { autorun } from 'mobx'
import { useEffect } from 'react'
function Counter() {
const { counterStore } = useRootStore()
// 使用 useEffect 并且第二个参数传入 [] 确保 autorun 方法只执行一次
useEffect(() => {
autorun(() => {
console.log(counterStore.count)
})
}, [])
return (
<div>
<span>{counterStore.count}</span>
<button onClick={() => counterStore.increment()}>+</button>
<button onClick={() => counterStore.reset()}>重置</button>
</div>
)
}
export default observer(Counter)
跟踪的数据类型
对于基本数据类型,属于值传递,MobX 只能跟踪到原始属性,跟踪不到复制后的值:
useEffect(() => {
const count = counterStore.count
autorun(() => {
// 错误写法, MobX 跟踪不到变量 count
console.log(count)
})
}, [])
对于引用数据类型,只要引用地址不发生变化,MobX 就可以进行跟踪:
// src\stores\CounterStore\CounterStore.js
import { makeAutoObservable } from "mobx"
class CounterStore {
// 数值状态
count = 10
person = { name: 'Tom' }
constructor() {
// 将参数对象的属性设置为 Observable State
// 将参数对象的方法设置为 Action
makeAutoObservable(this, {}, {autoBind: true})
}
// 使数值+1
increment() {
this.count += 1
}
// 重置数值状态
reset() {
this.count = 0
}
}
export default CounterStore
// src\components\Counter.js
import { observer } from 'mobx-react-lite'
import { useRootStore } from '../../stores/RootStore'
import { autorun, runInAction } from 'mobx'
import { useEffect } from 'react'
function Counter() {
const { counterStore } = useRootStore()
// 使用 useEffect 并且第二个参数传入 [] 确保 autorun 方法只执行一次
useEffect(() => {
var person = counterStore.person
autorun(() => {
console.log(person.name)
})
}, [])
return (
<div>
<span>{counterStore.person.name}</span>
<button onClick={() => runInAction(() => counterStore.person.name = 'Jone')}>Jone</button>
<button onClick={() => runInAction(() => counterStore.person = { name: 'Lucy' })}>Lucy(点击后将不再执行副作用)</button>
</div>
)
}
export default observer(Counter)
使用 runInAction 执行操作省略在 Store 中定义 Action。
去除 ESLint 中 useEffect 相关的警告⚠️
当 useEffect 中传入了第二个参数,ESLint 会建议传入副作用中使用的状态。如:
useEffect(() => {
var person = counterStore.person
autorun(() => {
console.log(person.name)
})
}, [])
// 警告: React Hook useEffect has a missing dependency: 'counterStore.person'. Either include it or remove the dependency array react-hooks/exhaustive-deps
// 应传入 [counterStore.person]
可以通过修改 eslint 配置关闭该项规则校验。
在 React 17 版本中,官方团队修改了脚手架工具,允许直接在外部声明 .eslintrc 文件覆盖 eslint 配置,不需要使用 package.json、react-app-rewired 和 customize-cra 就可以实现 eslint 配置。
在项目根目录下新建 .eslintrc.js
文件:
// .eslintrc.js
module.exports = {
plugins: ['react-hooks'],
rules: {
'react-hooks/exhaustive-deps': 0
}
}
重启应用 npm start
reaction
reaction 和 autorun 的使用类似,区别是 reaction 提供了更加细颗粒度的状态控制。
- autorun 只能提供当前状态,reaction 还可以提供上一次的状态
- reaction 接收两个函数作为参数
- 第一个函数返回要监控的状态
- 第二个函数用来执行副作用
- 只有当第一个函数返回的状态发生变化时,第二个函数才会执行
- reaction 初始时不会执行副作用
useEffect(() => {
reaction(
() => counterStore.count,
(current, previous) => {
console.log(current + ' - ' + previous)
}
)
}, [])