react hooks + mobx usage of summary

2,099 阅读6分钟

前言

目前,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

参考文献

环境准备

安装最新版的 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等多个知名开源项目作者

期待您的加入,一起用技术改变生活!!!

招聘链接: job.toutiao.com/s/JHjRX8B