前言
目前,react hooks 大行其到,在这里,咱不比较 Class Componets 和 Function Component 孰优孰劣,官网有很好的描述,在这里,主要讲述 mobx 最新版本,如何在 react hooks 项目中,优雅地使用,合理地组织你的前端数据流
随着react 从 v16.8 开始,加入 FC 写法,持续升级至如今的 v17,mobx 也又 mobx v4/v5 -> v6,当前 mobx v16 版本可以很好的支持 react hooks 方式,市面上同样存在着其他优秀的数据管理工具,比如 recoil。
- 有兴趣可以阅读往期文章:Mobx vs Recoil application
希望通过阅读本文,可以帮助到你了解:如何优雅地使用 mobx
参考文献
环境准备
安装最新版的 mobx 和 mobx-react
npm install mobx mobx-react --save`
基本使用
全局mobx 配置
- 在 store/index.ts (store入口文件)中,配置mobx 选项
import { configure } from 'mobx';
// 全局mobx配置
configure({
enforceActions: 'always', // 始终需要通过action来改变状态
computedRequiresReaction: true // 禁止从动作或反应外部直接访问任何未观察到的计算值
});
创建 Store 的两种方式
- makeAutoObservable 自动观察所有数据模式,建议使用此模式
- makeObservable 自定义可观察属性
自动观察所有数据
- makeAutoObservable
import { makeAutoObservable, runInAction } from 'mobx';
class TestStore {
isReady = false; // observable state
amount = 1; // observable state
data; // observable state
constructor() {
// 自动监听所有属性
makeAutoObservable(this);
}
// computed
get price(): string {
return `$${this.amount * 100}`;
}
// action
increment(): void {
this.amount++;
}
// async action,
async asyncAction(): Promise<void> {
try {
const data = await geAsyncData();
// 异步action中,改变状态时,外层要包裹 runInAction(() => {/** ...*/})
runInAction(() => {
this.data = data;
this.isReady = true;
});
} catch (e) {
throw new Error(e);
}
}
}
export default new TestStore();
自定义可观察属性
- makeObservable,程序自己控制可观察数据 注意:一个store,只能存在 makeAutoObservable 或 makeObservable 其中的一种
import { makeObservable, observable, runInAction } from 'mobx';
class TestStore {
isReady = false; // state
amount = 1; // state
data;
constructor() {
// 自定义可观察
makeObservable(this, {
isReady: observable,
amount: observable,
price: computed,
data: observable,
increment: action,
asyncAction: action
});
}
// computed
get price(): string {
return `$${this.amount * 100}`;
}
// action,改变状态,不需要return,mobx,规范也不允许使用 return
increment(): void {
this.amount++;
}
// async action,使用 runInAction
async asyncAction(): Promise<void> {
try {
const data = await geAsyncData();
// 异步action中,改变状态时,外层要包裹 runInAction(() => {/** ...*/})
runInAction(() => {
this.data = data;
this.isReady = true;
});
} catch (e) {
throw new Error(e);
}
}
}
export default new TestStore();
Use In React
- 需要结合 mox-react,达到store可观察数据变化,引用store的componet自动更新
- 业务组件按需引用自己的store,"即插即用"
import { FC } from 'react';
import { observer } from 'mobx-react';
import store from './store';
const Test: FC = () => {
return (
<div>
<h1>About</h1>
<div>count from main: {store.amount}</div>
<div>price: {store.price}</div>
<button type="button" onClick={() => store.increment()}>add +1</button>
</div>
);
};
// 监听 Component
export default observer(Test);
高级用法
状态感应
不建议使用 when,逻辑容易混乱,文档后面有更好使用方式 when、reaction 需定义在 constructor 中,不要放在action中,容易造成监听混乱
import { makeAutoObservable, reaction, when } from 'mobx';
class TestStore {
isReady = false;
count = 0;
constructor() {
makeAutoObservable(this);
// 监听isReady,当 isReady 变为 true 时,执行 doSomething(一次性行为)
when(() = this.isReady, () => this.doSomething());
// 监听amount,当 amount 每次变化后,都会输出 value, previousValue
reaction(
() => this.amount,
(value, previousValue) => {
console.log(value, previousValue);
}
);
}
doReady(): void {
this.isReady = true;
}
doSomething(): void {
...
}
increment(): void {
this.amount++;
}
}
export default new TestStore();
多store间相互感应
不建议使用,逻辑容易混乱,文档后面有更好使用方式
// userStore.ts
import { makeAutoObservable, runInAction } from 'mobx';
import * as userService from '@service/user.ts';
class UserStore {
userInfo = {};
isReady = false;
constructor() {
makeAutoObservable(this);
}
async init(): Promise<void> {
await this.getUserInfo();
}
async getUserInfo(): Promise<void> {
try {
const data = await userService.getUserData();
runInAction(() => {
this.userInfo = data;
this.isReady = true;
});
} catch (e) {
throw new Error(e);
}
}
}
export default new UserStore();
// testStore.ts
import { makeAutoObservable, when } from 'mobx';
import userStore from './userStore.ts';
class TestStore {
amount = 1;
constructor() {
makeAutoObservable(this);
// 当 userStore.isReady 变为 true时,立即执行自己的 init 方法
when(
() => userStore.isReady,
() => this.init()
);
}
get price(): string {
return `$${this.amount * 100}`;
}
init(): void {
console.log('testStore init...')
}
increment(): void {
this.amount++;
}
}
export default new TestStore();
inject 公共 store 模式
- 如果项目中存在共享数据,并且对于多层级共用的情况(props一层层传递较麻烦),
-
- mobx-react Provider 模式,已经不在适用于 react hooks,会引起 error
-
- 通过 createContext -> useContext 建立上下文,即可使用 inject 模式
// 以 userStore 为例
class UserStore {/** ... */}
export const userStore = new UserStore();
// loadStores.ts 直接导出所有store
export { userStore } from './modules/userStore';
export { testStore } from './modules/testStore';
// store/index.ts
import { useContext, createContext } from 'react';
import * as stores from './loadStores';
// 创建上下文
const storesContext = createContext(stores);
// react 结合 store context
const useStores = (): any => useContext(storesContext);
export { stores, useStores }
// commponets/Counter.ts
import { FC } from 'react';
import { observer } from 'mobx-react';
import { useStores } from '@store/index';
const Counter: FC = () => {
// const { testStore } = useStores();
// 解构方式,注意,不能解构action,会引起 this 问题
const { testStore: { amount, price } } = useStores();
return (
<div>
<!--
<div>{testStore.amount}</div>
<div>price: {testStore.price}</div>
-->
<div>{amount}</div>
<div>price: {price}</div>
<button type="button" onClick={() => testStore.increment()}>add +1</button>
</div>
);
};
// 监听 Component
export default observer(Counter);
store 调度任务
- A -> B -> C
- 不建议使用when(when会存在一定风险,可能造成多个store间互相引用,维护成本高)
- 通过创建一个系统store调用任务Store
// store/index.ts
import { useContext, createContext } from 'react';
import { configure, makeAutoObservable, runInAction } from 'mobx';
import * as stores from './installStores';
// 全局mobx配置
configure({
enforceActions: 'always', // 始终需要通过行动来改变状态
computedRequiresReaction: true // 禁止从动作或反应外部直接访问任何未观察到的计算值
});
/**
* store 数据流控制
*/
class ScheduleStore {
isRunning = false;
constructor() {
makeAutoObservable(this);
}
// 调度任务开始
async run(): Promise<void> {
try {
/** ---- 系统初始化数据 --- */
// 用户信息初始化
await stores.userStore.init();
// test信息初始化
await stores.testStore.init();
// ...
// 数据就绪,准备渲染页面
runInAction(() => {
this.isRunning = true;
});
} catch (e) {
console.log(e);
}
}
}
const storesContext = createContext(stores);
const scheduleContext = createContext({
schedule: new ScheduleStore()
});
const useStores = (): any => useContext(storesContext);
const useSchedule = (): any => useContext(scheduleContext);
export { stores, useStores, useSchedule };
// App.ts
import { FC, useEffect, useContext, useState } from 'react';
import { observer } from 'mobx-react';
import { useSchedule } from '@store/index';
const App: FC<RouteComponentProps> = () => {
const { schedule } = useSchedule();
useEffect(() => {
if (!schedule.isRunning) {
console.log('schedule to run');
schedule.run();
} else {
console.log('init gar');
// doSomething
}
}, [schedule]);
return schedule.isRunning ? <Home /> : <Loadding />
}
export default observer(App);
mobx instantiation instead of react hooks fetch data
- 业务页面中,react hooks 请求初始数据,一般是这样实现
useEffect(() => {
store.getAsynData();
}, [])
- 利用 mobx store 的实例化,实现 store 的数据初始化
- 业务页面,直接 import 该store 即可,页面中无需 useEffect[() => {}, []]
// productionStore.ts
import { makeAutoObservable, runInAction } from 'mobx';
class ProductionStore {
list = [];
constructor() {
makeAutoObservable(this);
this.init();
}
async init(): Promise<void> {
await this.getList();
}
async getList(): Promise<void> {
try {
const data = await getAsyncList();
runInAction(() => {
this.list = data;
});
} catch (e) {
throw new Error(e);
}
}
}
export default new ProductionStore();
外部实例化 store
- 业务页面,直接导出 Class,在外部实例化(可传参) 1.定义 store Class
export default class ProductStore {
detailInfo = {};
constructor(id: string) {
makeAutoObservable(this);
this.init(id);
}
async init(id: string): Promise<void> {
await this.getProductDetail();
}
async getProductDetail(id: string): Promise<void> {
try {
const data = await getProDetail(id);
runInAction(() => {
this.detailInfo = data;
});
} catch (e) {
throw new Error(e);
}
}
}
2.在外部实例化
import ProductStore from './ProductStore';
const ProDetail: FC<RouteComponentProps> = ({ id }) => {
const productStore = new ProductStore(id);
// 卡卡地,就是干...
return ();
}
多实例 Store 共存
- 业务页面一般不会涉及多实例共存,这里以 tabsStore 多tab卡片共存为例子
type TabType = {
key: string;
title?: string;
};
// 单例 tab
class Tab {
active = false;
key: string; // 主键
title: string;
constructor(options: TabType) {
makeAutoObservable(this);
this.init(options);
}
init(options): void {
Object.assign(this, options);
}
setActive(val: boolean): void {
if (val !== this.active) {
this.active = val;
}
}
}
// tabs 管理
class Tabse {
tabList = [];
constructor() {
makeAutoObservable(this);
}
get activeTabKey(): string {
const activeTab = this.tabList.find(x => x.active);
if (activeTab) {
return activeTab.key;
}
return this.tabList.length ? this.tabList[0].key : undefined;
}
get activeTab() {
return this.tabList.find(x => x.active);
}
/**
* 增加一个 tab
* 如果存在就切换,不存在则新建
* @param tab
*/
addTabIfNotExist = (tab: TabType): void => {
const { key } = tab;
// 检查是否存在
let target = this.tabList.find(x => x.key === key);
// 激活路由
history.push(tab.key);
if (!target) {
target = new Tab(tab);
this.tabList.push(target);
}
// 切换tab
this.switchTab(target, false);
}
/**
* 增加一个tab页面,关闭当前页面
* @param tab
*/
addTabRemoveCurrent = tab => {
const activeTab = this.activeTab.key;
this.addTabIfNotExist(tab);
this.removeTab(activeTab, true);
}
/**
* 切换tab
* @param tab
* @param pushHistory
*/
switchTab(tab, pushHistory: boolean): void {
if (!tab?.active) {
// 关闭当前active状态后,激活目标值
this.tabList.find(x => x.active).active = false;
tab.active = true;
if (pushHistory) {
history.push(tab.key)
}
if(location.pathname !== '/' + tab.key) {
const url = location.origin + '/' + tab.ke
window.history.pushState({}, 0, url);
}
}
}
/**
* 关闭 tab
* @param key
* @param state
*/
removeTab(key, ): void {
// 仅有一个tab的时候不能关闭
if (this.tabList.length === 1) {
return;
}
// ....
}
}
export const tabsStore = new Tabse();
尾声
mobx的基本使用与高级用法,到此基本已经结束,相信通过阅读本文,可以对你可以更好地组织管理前端项目的数据
❤️ 加入我们
字节跳动 · 幸福里团队
Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者
期待您的加入,一起用技术改变生活!!!