MobX 学习 - 06 异步任务、rootStore、数据监测

加载远端任务

加入请求服务端数据的异步任务。

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 提供了一些方法,下面介绍其中的 autorunreaction 方法。

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)
    }
  )
}, [])
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值