从头开始搭建一个React 项目(二)mobx + hooks

5,233 阅读6分钟

React 的状态管理工具有 Redux 和 mobx,听说他们各有千秋。作为一个俩都没用过的菜鸟,经过一系列不是很深层次的了解之后后,发现 Redux 学习成本稍高,样板代码稍多,短暂的思考了一下,决定使用 mobx 作为下个项目的状态管理工具。

但是。。。由于了解的没有那么深刻,在搭建这个项目的时候,果不其然又踩了很多的坑,下面,我将记录下我的学习(踩坑)历程,方便后来者及时规避,谨慎敲码。

第一步:mobx 基础知识学习

@observable 装饰器

mobx 使用类定义语法,将需要管理的状态放在类中,使用@observable 装饰器将对象转化为可观察的对象。 例如:

class store {
    @observable title = ''
}

@observable 可修饰的数据类型有:JS基本数据类型、引用类型、普通对象、类实例、数组和映射。

需要注意的是,使用 @observable 修饰过后的数组,对象,map等复杂的数据结构,类型就是(observable)对象,比如:

class store {
    @observable todos = [1,2,3]
    console.log(Array.isArray(this.todos))
}

得到的结果将是 false,虽然它的类型不再是数组,但是它可以使用所有数组的方法,也可以正常的访问所有数组的元素。如果你想将 observable 对象转换为原始类型,可以使用 toJS函数。

toJS 是 mobx 提供的一个工具函数。他可以递归地将一个(observable)对象转换为 javascript 结构,使用方法如下:

import { toJS } from  'mobx'
class store {
    @observable todos = [1,2,3]
    console.log(Array.isArray(toJS(this.todos)))
}

这样得到的结果就将是 true 了

@computed

@computed 计算属性,即根据当前现有的值或者状态得到其他衍生的值,例如:

class store {
    @observable firstName = ''
    @observable lastName = ''
    @computed get fullName () {
        return this.firstName + '·' + this.lastName
    }
}

autorun

当你想创建一个响应式函数,而该函数本身永远不会有观察者时,可以使用 mobx.autorun。当可观察数据发生变化时,autorun 会自动去执行依赖可观察数据的行为。 例如:

class Store {
    @observable firstName = ''
    @observable lastName = ''
}
const store = new Store()
autorun(()=>{
    console.log(store.firstName + '·' + store.lastName)
})
store.firstName = 'lily'
store.lastName = 'martin'

执行后会发现 autorun 会执行 3 次,每次更改 firstName 或 lastName 它都会执行

autorun 与 computed: 如果你想响应式的产生一个可以被其它 observer 使用的值,请使用 @computed,如果你不想产生一个新值,而想要达到一个效果,请使用 autorun

@action

@action 是任何用来修改状态的东西,@action.bound 则还可以用来自动地将动作绑定到目标对象。 例如:

class Ticker {
    @observable tick = 0

    @action.bound
    increment() {
        this.tick++ //this 永远是正确的
    }
}

const ticker = new Ticker()
setInterval(ticker.increment, 1000)

mobx 注入

学习完基本的 mobx 知识点后, 我创建了两个 store 文件,分别用来存储 user 相关的数据和 订单相关的数据,以下是示例:

// store/user.js
class userStoreClass {
    @observable userId = ''
    @observable phone = ''
    ...
}
const userStore = new userStoreClass()
export default userStore
// store/order.js
class orderStoreClass {
    @observable orderId = ''
    @obervable orderTime = ''
    ...
}
const orderStore = new orderStoreClass()
export defalut orderStore

然后将两个 store 注入到 index 中

// index.js
import { Provider } from 'mobx-react';
import orderStore from './stores/order'
import userStore from './stores/user'

const stores = {
  orderStore, userStore
};

ReactDOM.render(
  <Provider {...stores}>
    <App />
  </Provider>, document.getElementById('root'))

mobx 使用

首先引入

import { observer, inject } from 'mobx-react'

@inject('userStore')
@observer
class ViewBox extends React.Component {
}

@observer: observer 函数/装饰器可以用来将 React 组件转变成响应式组件。 它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。 observer 是由单独的 mobx-react 包提供的。

@inject: 从mobx-react4 开始,要连接到这些 stores,必须要传递一个 stores 名称的列表给 inject,这使得 stores 可以作为组件的 props 使用。

引用完成后,我们可以直接在 prop 中使用 store 中的数据和方法。 例如:

import { observer, inject } from 'mobx-react'

@inject('userStore')
@observer
class ViewBox extends React.Component {
    render() {
        return <div>{this.props.userStore.phone}</div>;
    }
}

阶段总结:

这样一系列流程下来, mobx 算是可以搭好了,可以愉快的在上面码代码了,但是码着码着,突然想到试试React hooks。

众所周知 hooks 是 react 近年来比较重大的一次更新,看完文档之后也是心痒痒。但是 hooks 必须要在函数组件中使用,但是函数组件要怎么注入 mobx 呢?试了一下直接跟 class 组件相同的引用方式,发现果然 想的美。。。怎么办呢?翻看 mobx 的中文文档,没有找到与 hooks 相关的一点点蛛丝马迹(怀念Vue...)只能忍着脑壳疼去翻官方文档。。。

mobx + hooks 探索

一顿搜索猛如虎,终于我找到了~~

首先上链接:mobx-react.js.org/libraries#c…

首先,官方给出了 hooks 相关的几个版本推荐:

  • 如果你还没有使用 hooks,安心使用mobx-react @ 5 吧~
  • 如果你想将项目迁移到 hooks,你的项目中既有函数式组件又有class组件,你可以使用包含mobx-react-litemobx-react @ 6,并将其自动用于函数式组件。
  • 如果你的项目完全使用函数式组件,直接使用 mobx-react-lite,它支持hooks并且更快更小!

那么我们要怎么结合hooks使用mobx呢?上链接:mobx-react.js.org/recipes-con…

由于我们的项目基本上都会涉及到多个store,我就直接看 Multiple global stores这一节啦 结合我们上面的例子,流程与官方例子一致。

先创建两个仓库

// store/user.js
export default class userStoreClass {
    @observable userId = ''
    @observable phone = ''
    ...
}
// store/order.js
export default class orderStoreClass {
    @observable orderId = ''
    @obervable orderTime = ''
    ...
}

注意:与上面的例子不同的是,我这里导出的是 class 而不是 class 的实例,因为官方文档中指出:

全局保存 class 实例被认为是一种不好的做法,因为在一些应用程序的管理单元和集成测试时,会引起问题。

既然没有导出实例,那我们在index.js 引用的时候也要注意了,不能直接将 class 注入到项目中,必须先实例化

// index.js
import { Provider } from 'mobx-react';
import orderStoreClass from './stores/order'
import userStoreClass from './stores/user'

const stores = {
  orderStore:new orderStoreClass()
  userStore:new userStoreClass()
};

ReactDOM.render(
  <Provider {...stores}>
    <App />
  </Provider>, document.getElementById('root'))

这样我们才能继续在 class 组件中使用 mobx

继续 hooks 的兼容,接下来我们要创建一个 storesContext,来管理我们的store:

// src/contexts/index.js
import React from 'react'
import orderStoreClass from './stores/order'
import userStoreClass from './stores/user'

export const storesContext = React.createContext({
  orderStore:new orderStoreClass()
  userStore:new userStoreClass()
})

最后,让我们使用自定义useStores hook 来访问导出的storeContext值。

// src/hooks/use-stores.js
import React from 'react'
import { storesContext } from '../contexts'

export const useStores = () => React.useContext(storesContext)

现在我们就可以在函数式组件中使用 store 啦

import React from 'react'
import { observer } from 'mobx-react'
import { useStores } from '../hooks/use-stores'

// src/components/Counter.js
export const Counter = observer(() => {
  const { userStoreClass } = useStores()

  return (
    <>
      <div>{userStoreClass.userId}</div>
      <button onClick={() => userStoreClass.add()}>++</button>
      <button onClick={() => userStoreClass.toggle()}>--</button>
    </>
  )
})

以上