开篇
MobX是一款身经百战的状态管理库,它比Redux性能更优秀、功能更强大、使用更灵活、代码量更少!但使用率却不如Redux,我觉得有很大一部分原因是过于灵活,即完成一件事可以有多种方式。这使得不同人编写的MobX风格差异极大!也使得版本升级历史包袱重!MobX6与之前版本有不少差异!
最近我把官方文档从头到尾过了一遍,感觉MobX官方文档真的非常不错,里面有很多状态管理的知识值得细细咀嚼,强烈建议有时间的话还是要亲自啃一遍官方文档。
以下我将以todo案例模拟实战项目,写一篇关于使用typeScript 编写Mobx6的编码规范。在遇到一件事有多种实现方式时我只重点讲一种以及选择它的原因,其他的一笔带过。如有不妥之处,欢迎留言讨论!
引入MobX,如何选择mobx-react与mobx-react-lite?
mobx-react与mobx-react-lite优劣比较
- mobx-react支持类组件,mobx-react-lite不支持类组件。
- mobx-react支持Provider 和inject,mobx-react-lite可以用React.createContext替代。
- mobx-react支持特殊的观察对象 propTypes,mobx-react-lite可以用typeScript代替。
- mobx-react(7.3.0)的大小:压缩前16kb,压缩后5.5kb;mobx-react-lite(3.3.0)的大小:压缩前6.2kb,压缩后2.2kb。
结论
如果只写函数组件,建议用mobx-react-lite。如果需要写类组件,建议用mobx-react。
目前我写项目全部用函数组件,拥抱hooks,所以我选择mobx-react-lite。
只用函数组件
- 安装:
yarn add mobx mobx-react-lite
- 引入:
import { observer } from "mobx-react-lite"
需用类组件
- 安装:
yarn add mobx mobx-react
- 引入:
import { observer } from "mobx-react"
按功能划分状态
UI状态
常见内容有:
- Session 信息
- 应用加载阶段的信息
- 影响全局 UI 的信息
- Window 尺寸
- 当前语言
- 当前主题
- 会影响多个无关组件的界面状态
- 工具栏可见性等等
- 向导的状态
- 全局遮罩层的状态
- 其他和业务、用户无关的且跟界面强相关的信息
用户状态
常见内容有:
- 用户id、姓名、部门、头像等
- 用户权限表
- 用户登陆时间戳
- 用户空闲自动登出时限
- 用户密码过期时限
- 用户上一次更新密码时间戳
- 其他用户信息
业务状态
这个要视应用程序的功能而定。
小结
根据用途划分store,建议将强相关的数据集中在一个store中,方便处理。
例如:创建一个todo应用,通常store中要有ui、user、todo这3个store。
不建议像Redux那样所有状态集中在一个store里面。太过深奥的理由不谈,最直接的原因就是开发体验不好。在写redux时会遇到大量类似store.todo.todoitem.name
这样的代码。
定义数据存储
个人建议
- 不建议使用useLocalStore和useObserver,因为typescript对这种方式的类型提示支持不好。最推荐用class的形式写。
- 不要使用继承模式,继承模式会带来很多不必要的麻烦。关于使用继承模式时会遇到的哪些问题及解决方法在官方文档里有,内容真不少!真心不建议使用继承模式!!!
- 强烈建议使用类来定义数据存储。
使用类定义数据存储的好处
以下内容来自官方文档:
- 更容易被索引以实现自动补全等功能,例如使用 TypeScript。
- instanceof 检查对于类型推断来说非常强大,并且类实例不会被包装在 Proxy 对象中,这一点给了它们更好的调试体验。
- 使用类会从引擎优化中受益良多,因为它们的形态是可预测的并且方法在原型上是共享的。
容器选择
- 强烈建议使用createContext。
- 不建议使用useLocalStore、useObserver。
- 也不建议使用Provider 、inject以及装饰器。
代码示例及讲解
src/store/todos/todo.ts
import { makeAutoObservable } from "mobx"
export class Todo {
title = ""
finished = false
constructor(title: string) {
makeAutoObservable(this,
{ // 自定义各个类属性的mobx注解,如false(不注解)等
},
{ // options参数,autoBind是指自动绑定this
autoBind: true,
}
)
this.title = title
}
toggle() {
this.finished = !this.finished
}
}
讲解
- 以上是单条todo的state。建议定义数据存储的类时使用makeAutoObservable。关于注解类型及选项,请参考官方文档。
- 不建议用装饰器(旧版本的方式,兼容性差);也不建议用makeObservable(需要手动逐条指定注解,繁琐)。
- 这个Todo类不需要实例化并导出,因为它是用来作数据的!不是用来作store的!
src/store/todos/index.ts
import { makeAutoObservable } from "mobx"
import { Todo } from "./todo"
class TodoList {
todos: Todo[] = []
get unfinishedTodoCount() {
return this.todos.filter((todo) => todo.finished).length
}
constructor(todos: Todo[]) {
makeAutoObservable(this,
{ // 自定义各个类属性的mobx注解,如false(不注解)等
},
{ // options参数,autoBind是指自动绑定this
autoBind: true,
}
)
this.todos = todos
}
add(todo: Todo) {
this.todos.push(todo)
}
}
const todoStore = new TodoList([])
export default todoStore
讲解
- 这里的重点是类的组合!todoStore中主要的数据是todos,它是Todo[]类型,这个Todo即是上面的那个Todo类。
- 这个TodoList类定义完以后一定要创建一个实例并导出该实例,因为TodoList类是用来作store的!
- 在MobX中存储的是数据及数据相关动作。请牢记:MobX负责数据的状态管理!React负责数据的渲染展示!
- 数据的状态变更必须通过store里的action来实施。极其不建议在React组件中使用runInAction去直接更新state数据!
src/store/user/index.ts
import { makeAutoObservable } from "mobx"
class User {
name: string
get isLogin() {
return this.name.length>0
}
constructor(name:string) {
makeAutoObservable(this,
{ // 自定义各个类属性的mobx注解,如false(不注解)等
},
{ // options参数,autoBind是指自动绑定this
autoBind: true,
}
)
this.name = name
}
login(name: string) {
this.name = name
}
logout() {
this.name=""
}
}
const userStore = new User("")
export default userStore
讲解
- 这里做了一个非常简陋用户信息的store,只是演示如何在一个React.Context容器中存储多个store。
- 这个User类定义完以后一定也要创建一个实例并导出该实例,因为User类也是用来作store的!
src/store/index.tsx
import { createContext } from "react"
import todoStore from "./todos"
import userStore from "./user"
// 创建context用来保存各项数据store
export const Store = createContext({ todoStore,userStore })
// StoreProvide组件,用来给子组件传递store
const StoreProvide: React.FC<{
children: React.ReactNode[];
}> = (props) => {
return (
<Store.Provider value={{ todoStore,userStore }} >
{props.children}
</Store.Provider>
)
}
export { Todo } from "./todos/todo";
export default StoreProvide;
讲解
- 重点一是创建context用来保存各项数据store。
- 重点二是创建StoreProvide组件,用来给子组件传递store。
- 重点三是文件名是index.tsx,注意文件名后缀是tsx,如果名字是index.ts一定会报错!
src/app.tsx
import React, { useContext, useRef } from "react"
// import { observer } from "mobx-react"
import { observer } from "mobx-react-lite"
import StoreProvide, { Store, Todo } from './store'
const TodoListView: React.FC = observer(function TodoListView() {
const { todoStore } = useContext(Store)
console.log("渲染了TodoListView")
return (
<div>
<ul>
{todoStore.todos.map((todo, index) => (
<TodoView todo={todo} key={index} />
))}
</ul>
</div>
)
})
const TodoListLeft: React.FC = observer(function TodoListLeft() {
const { todoStore } = useContext(Store)
console.log("渲染了TodoListLeft")
return (
<>
Tasks left: {todoStore.unfinishedTodoCount}
</>
)
})
// const TodoListLeft = observer(
// class TodoListLeft extends React.Component {
// static contextType = Store;
// context!: React.ContextType<typeof Store>;
// render() {
// return (
// <>
// Tasks left: {this.context!.todoStore.unfinishedTodoCount}
// </>
// )
// }
// })
const AddTodo: React.FC = observer(function AddTodo() {
const { todoStore } = useContext(Store)
console.log("渲染了AddTodo")
const ref = useRef<HTMLInputElement>(null)
return (
<>
<input ref={ref} type="text" />
<button onClick={() => {
const item = new Todo(ref.current!.value)
todoStore.add(item)
ref.current!.value = ""
}} > 新增 </button>
</>
)
})
const TodoView: React.FC<{ todo: Todo }> = observer(function TodoView({ todo }) {
console.log("渲染了TodoView")
return (
<li>
<input
type="checkbox"
checked={todo.finished}
onChange={() => todo.toggle()}
/>
{todo.title}
</li>
)
})
const LoginUser: React.FC = observer(function LoginUser() {
console.log("LoginUser")
const { userStore } = useContext(Store)
const ref = useRef<HTMLInputElement>(null)
return (
<div>
<p>
{userStore.isLogin?`当前登录的用户是:${userStore.name}`:"当前没有登录用户!"}
</p>
<p>
<input ref={ref} type="text" />
<button onClick={() => userStore.login(ref.current!.value)} > 登录 </button>
<button onClick={() => userStore.logout()} > 退出 </button>
</p>
</div>
)
})
const App = () => {
return (
<StoreProvide>
<LoginUser />
<hr/>
<AddTodo />
<TodoListView />
<TodoListLeft />
</StoreProvide>
)
}
export default App
代码讲解
- App组件返回的内容最外层是StoreProvide组件。这是用来将context中的MobX store传递到所有子组件。
- 凡是使用MobX store数据的组件,外面都要用observer函数包围,这样MobX就可以根据被观察数据决定被包围的组件是否要做重新渲染。
- 请留意TodoListLeft组件,我写了2个版本,一个是函数组件的版本,被注释的部分是类组件的版本。请留意函数组件与类组件使用context中的MobX store用法区别。
- 请留意顶部还注释了一行代码,类组件必须使用
mobx-react
库,函数组件mobx-react
库和mobx-react-lite
两种库都可以使用。有关这2个库的差异在文章第二大段讲过了。 - 请细看LoginUser组件开头部分
const LoginUser: React.FC = observer(function LoginUser() {
,这里的`function LoginUser() {``是为了让组件在React Developer Tools中可以正常显示名字。以下是名字显示正常的截图:
- 如果将上面的代码替换成这样
const LoginUser: React.FC = observer(()=> {
,组件虽然可以正常工作,但在React Developer Tools中不能正常显示名字。以下是名字显示异常的截图:
最后
MobX的内容真的很多,小小的一篇博客没法讲完。以上只是选最常用最基本的内容。后续我还会出一篇有关reactions的博客。
再次建议有时间的朋友一定要细读官方文档,这里不光有很多MobX的使用细则,还有很多怎么做好状态管理的建议。
另外附以上代码的CodeSandbox在线体验。