Mobx-react-lite —— Lightweight React bindings for MobX based on React 16.8 and Hooks
最近项目开发使用到了mobx-react-lite这个轻量级的状态管理库,在使用的过程中碰到了不少的问题,这里做一个小小的总结。
mobx介绍
Mobx是一个状态管理库,与Redux不同,它更加简单且可扩展性强,推崇任何源自应用状态的东西都应该自动地获得。它的核心原理是通过action触发state的变化,进而触发state的衍生对象(computed value & Reactions)。
Mobx的几个核心概念:
- Observable state
可观察的状态,存放应用数据。默认情况下,observable 是递归应用的,所以如果状态的某个值是一个对象或数组,那么该值也会变成可观察的。 - Computed values
计算值,在相关state数据发生变化时自动更新的值。 - Reactions
Reactions 和计算值很像,但它不是产生一个新的值,而是会产生一些副作用,比如打印到控制台、网络请求、递增地更新 React 组件树以修补DOM、等等。 - Actions
动作是任何用来修改状态的东西。在严格模式下,必须通过action来更改状态。
mobx-react-lite介绍
直至2018年底,React项目中统一使用mobx的方式都是mobx-react。然而随着react hooks的诞生,mobx-react-lite也出现了,它是专门服务于react hooks的mobx-react轻量级版本。虽然mobx-react@6已经包含了mobx-react-lite,但官方是推荐在没有类组件的react项目中直接使用mobx-react-lite的。
如何管理状态?
Mobx中的状态用observable object表示,而在mobx-react-lite中,有一个新的API用于创建可观察状态—— useLocalStore。这可以看作是一个自定义的react hook,它运行其初始化函数一次以创建可观察的store并在组件的生命周期内维护store。
初始化函数返回对象的所有属性将自动可观察化,getter将转换为计算属性,方法将绑定到store并自动封装成mobx transaction。什么是transaction呢?我们知道,观察者(react组件)会监听Observable变量的改变从而触发视图渲染,如果action中改变了多个状态,那么是不是会触发多次渲染呢?为了避免这种情况,mobx提供了transaction功能,可以将对多个应用状态(Observable)的更新封装成一个事务,只有在事务执行完成后,才会触发一次对应的监听者(Reactions),减少组件渲染次数。如果初始化函数返回的是类实例,那么也是同样的操作。
在mobx中,状态是可以修改的,但一般推荐通过action来修改状态,便于跟踪状态变化,同时减少相关依赖的更新。
1 | import React from 'react' |
useLocalStore源码:
1 | export function useLocalStore<TStore extends Record<string, any>, TSource extends object = any>( |
从源码可以看出,useLocalStore的逻辑其实是比较简单的,它先是通过observable(initializer())将初始化函数返回的对象中的属性转换成可观察的,为计算属性创建代理。接着遍历对象中的方法,将其封装成事务,再使用useState返回处理后的store,而这个store是不可变的。
如何观察状态变更?
有三种方式可以让组件监听到状态变化:
- observer HOC
- observer component
- useObserver hook
组件正确监听后,就会被通知组件有关更改的信息(只要状态可以被观察到),并且在父级不知道的情况下进行重新渲染。这种观察者模式正是mobx的一大特征。
1 | import { observable } from 'mobx' |
这三个的区别是什么呢?
- observer HOC:
observer<P>(baseComponent: React.FC<P>, options?: IObserverOptions): React.FC<P>
使用了高阶组件的模式,并在内部封装了React.memo,将传入的函数组件转换为监听者。 - observer component
<Observer>{renderFn}</Observer>
只跟踪函数中的渲染并在需要时自动渲染,更颗粒化地管控变化, 通常为了优化性能而使用。 - useObserver hook
useObserver<T>(fn: () => T, baseComponentName = "observed", options?: IUseObserverOptions): T
使用这个钩子去包裹return的内容,它相对更加自由,可以让我们以多种方式优化组件(e.g. using memo with a custom areEqual, using forwardRef, etc.), 并准确声明观察的部分。
关于这一点的一个好处是,如果任何钩子由于某种原因改变了一个observable,那么该组件将不会不必要地重新渲染两次。
与React Context协作
useLocalStore的虽然是从名称上看只创建组件本地的store,但是借助react context,就可以建立全局的store。
先来设计store
1 | export function createStore() { |
然后创建一个context,使用useLocalStore传入刚刚的函数创建本地的store,通过context将store挂载上去
1 | import React from 'react' |
接着将StoreProvider作为父组件放在最外层,这样我们就可以在页面组件中获取store了
1 | import React from 'react' |
如何处理异步请求和副作用?
这也是我在项目中遇到的一个问题。首先,mobx与redux不同,它的异步请求是可以写在action里面的,因此在实际项目中,就产生了两种写法:
将请求写在action中。这样写的好处是在组件中的逻辑更加清晰,只需直接调用action执行即可。但是无法根据请求状态返回loading给组件。
1
2
3
4
5
6
7getTitle() {
this.pendingRequestCount++;
fetch(url).then(action(resp => {
this.title = resp.title;
this.pendingRequestCount--;
}))
}将请求写在组件中,在action中只做修改状态的事情。这样写的好处是可以直接在组件内部做异步请求,更方便地管理请求进度并处理结果。缺点是组件内部请求逻辑变重,没有在store中统一维护请求,会因此增加一些额外的代码来维护该请求。
1
2
3
4
5
6
7
8
9
10useEffect(
() => {
if (!store.finishFetch) {
fetch(url).then(action(resp => {
store.getTitle(resp.title)
}))
}
},
[store]
)
目前采用的是第二种写法,是否有更好的方法来优化呢?
在项目中,我们经常会用到useEffect去处理一次副作用(如首次进入页面做异步请求),如果其中依赖到了observable value那怎么办呢?这里也是有2种写法:
借助autorun。这是官方推荐的写法,这里注意永远不需要指定useEffect的依赖项,因为我们只希望运行一次且运行结果不依赖于store,但这样写会引起eslint报warning,所以需要让eslint去ignore这个警告。
1
2
3
4
5
6
7
8
9
10
11
12
13import React from 'react'
import { autorun } from 'mobx'
function useDocumentTitle(store: TStore) {
React.useEffect(
() =>
autorun(() => {
document.title = `${store.title} - ${store.sectionName}`
}),
// eslint-disable-next-line
[], // note empty dependencies
)
}直接在useEffect中编写副作用。但是这样组件重新渲染可能会执行多次副作用,需要额外的代码去控制。
1
2
3
4
5
6
7
8
9
10useEffect(
() => {
setLoading(true)
requestTeamList().then((res) => {
store.setList(res.teams)
setLoading(false)
})
},
[store]
)
mobx-react-lite目前还是比较新的一个库,需要更多的探索。