preface

Now, react Hooks are working on this project. They are not comparing Class Componets and Function Component. Use gracefully and organize your front-end data flow

React hooks are supported in the current MOBx V16 version, which includes v17, V16.8, and FC. There are other great data management tools out there, too, like Recoil.

  • Read the previous article: Mobx vs Recoil Application

Hopefully reading this article has helped you understand: How to use Mobx elegantly

reference

  • mobx.js.org/
  • mobx-react.js.org/

Environment to prepare

Install the latest versions of MOBx and Mox-React

npm install mobx mobx-react --save`
Copy the code

The basic use

Global MOBX configuration

  • In store/index.ts (Store entry file), configure the mobx option
import { configure } from 'mobx';

// Global MOBx configuration
configure({
  enforceActions: 'always'.// You always need action to change the state
  computedRequiresReaction: true // Disallow direct access to any unobserved computed values from outside the action or reaction
});
Copy the code

Two ways to create a Store

  • The makeAutoObservable automatically observes all data schemas and is recommended
  • MakeObservable customizes observable properties

Automatically observe all data

  • makeAutoObservable
import { makeAutoObservable, runInAction } from 'mobx';

class TestStore {
  isReady = false; // observable state
  amount = 1; // observable state
  data; // observable state

  constructor() {
    // Automatically listen on all attributes
    makeAutoObservable(this);
  }
  
  // computed
  get price() :string {
    return ` $The ${this.amount * 100}`;
  }
  
  // action
  increment(): void {
    this.amount++;
  }
  
  / / async action,
  async asyncAction(): Promise<void> {
    try {
      const data = await geAsyncData();
      // In an asynchronous action, the code contains the runInAction(() => {/**... * /})
      runInAction(() = > {
        this.data = data;
        this.isReady = true;
      });
    } catch (e) {
      throw new Error(e); }}}export default new TestStore();
Copy the code

Custom observable properties

  • Makeobservables, programs that control observable data themselves

Note that a store can only exist in one of makeAutoObservable or makeObservable

import { makeObservable, observable, runInAction } from 'mobx';

class TestStore {
  isReady = false; // state
  amount = 1; // state
  data;
  
  constructor() {
    // Custom observable
    makeObservable(this, {
      isReady: observable,
      amount: observable,
      price: computed,
      data: observable,
      increment: action,
      asyncAction: action
    });
  }
  
  // computed
  get price() :string {
    return ` $The ${this.amount * 100}`;
  }
  
  // action, change the state, do not need return, mobx specification does not allow return
  increment(): void {
    this.amount++;
  }
  
  // async action, using the runInAction
  async asyncAction(): Promise<void> {
    try {
      const data = await geAsyncData();
      // In an asynchronous action, the code contains the runInAction(() => {/**... * /})
      runInAction(() = > {
        this.data = data;
        this.isReady = true;
      });
    } catch (e) {
      throw new Error(e); }}}export default new TestStore();
Copy the code

Use In React

  • Mox-react needs to be combined to achieve store observable data changes, and reference store componet to update automatically
  • Business components reference their own stores on demand, “plug and play”
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>
  );
};

/ / listening Component
export default observer(Test);
Copy the code

Advanced usage

State induction

The use of when is not recommended because the logic is confusing and there are better ways to use it later in the document

When and reaction should be defined in constructor and not in action, which may cause monitoring confusion

import { makeAutoObservable, reaction, when } from 'mobx';

class TestStore {
  isReady = false;
  count = 0;

  constructor() {
    makeAutoObservable(this);
    // listen for isReady, and when isReady becomes true, doSomething (one-time behavior)
    when(() = this.isReady, () = > this.doSomething());
    // Listen for amount, and output value, previousValue after each amount change
    reaction(
      () = > this.amount,
      (value, previousValue) = > {
        console.log(value, previousValue); }); } doReady():void {
    this.isReady = true;
  }
  
  doSomething(): void{... } increment():void {
    this.amount++; }}export default new TestStore();
Copy the code

Multiple stores sense each other

It is not recommended to use it because the logic is easily confused. There are better ways to use it in the document

// 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();
Copy the code
// testStore.ts
import { makeAutoObservable, when } from 'mobx';
import userStore from './userStore.ts';

class TestStore {
  amount = 1;

  constructor() {
    makeAutoObservable(this);
    // When userStore.isReady becomes true, execute your init method immediately
    when(
      () = > userStore.isReady,
      () = > this.init()
    );
  }

  get price() :string {
    return ` $The ${this.amount * 100}`;
  }

  init(): void {
    console.log('testStore init... ')
  }

  increment(): void {
    this.amount++; }}export default new TestStore();
Copy the code

Inject Public Store mode

  • If there is shared data in the project and for multi-layer sharing (it is cumbersome to pass layers of props),
    • The mobx-React Provider mode is no longer applicable to react hooks, which raise errors
    • Use inject mode to establish the context by createContext -> useContext
// Take userStore as an example
class UserStore {/ * *... * /}
export const userStore = new UserStore();
Copy the code
// loadStores. Ts directly exports all stores
export { userStore } from './modules/userStore';
export { testStore } from './modules/testStore';
Copy the code
// store/index.ts
import { useContext, createContext } from 'react';
import * as stores from './loadStores';

// Create context
const storesContext = createContext(stores);

// react with store context
const useStores = (): any= > useContext(storesContext);

export { stores, useStores }
Copy the code
// commponets/Counter.ts
import { FC } from 'react';
import { observer } from 'mobx-react';
import { useStores } from '@store/index';

const Counter: FC = () = > {
  // const { testStore } = useStores();
  // Destruct the action. Note that the action cannot be destructed, which causes this problem
  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>
  );
};

/ / listening Component
export default observer(Counter);
Copy the code

Store Scheduling Task

  • A -> B -> C
  • It is not recommended to use when (When may cause cross-reference between multiple stores and high maintenance cost).
  • Call the task store by creating a system store
// store/index.ts
import { useContext, createContext } from 'react';
import { configure, makeAutoObservable, runInAction } from 'mobx';
import * as stores from './installStores';

// Global MOBx configuration
configure({
  enforceActions: 'always'.// Action is always needed to change the state
  computedRequiresReaction: true // Disallow direct access to any unobserved computed values from outside the action or reaction
});

/** * store data flow control */
class ScheduleStore {
  isRunning = false;

  constructor() {
    makeAutoObservable(this);
  }
  
  // The scheduling task starts
  async run(): Promise<void> {
    try {
       /** ---- System initialization data -- */
      // User information is initialized
      await stores.userStore.init();
      // test Information initialization
      await stores.testStore.init();
      // ...
      // The data is ready to render the page
      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 };
Copy the code
// 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);
Copy the code

mobx instantiation instead of react hooks fetch data

  • React hooks request initial data in a business page, typically implemented this way
useEffect(() = > {
  store.getAsynData();
}, [])
Copy the code
  • The instantiation of Mobx Store is used to initialize the store data
    • 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();
Copy the code

Instantiate store externally

  • Business page, export Class directly, instantiate externally (passable)

1. Define a 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); }}}Copy the code

2. Instantiate externally

import ProductStore from './ProductStore';

const ProDetail: FC<RouteComponentProps> = ({ id }) = > {
  const productStore = new ProductStore(id);
  // Kakadi, which is dry...
  return(a); }Copy the code

Multiple instance stores coexist

  • Business pages generally do not involve multi-instance coexistence. Here, tabsStore multi-tab card coexistence is taken as an example
type TabType = {
  key: string; title? :string;
};

/ / the singleton TAB
class Tab {
  active = false;
  key: string; / / the primary key
  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 management
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);
  }

  /** * add a TAB * if it exists, create a new TAB * if it does not exist@param tab 
   */
  addTabIfNotExist = (tab: TabType): void= > {
    const { key } = tab;
    // Check whether it exists
    let target = this.tabList.find(x= > x.key === key);
    // Activate the route
    history.push(tab.key);
    if(! target) { target =new Tab(tab);
      this.tabList.push(target);
    }
    / / switch TAB
    this.switchTab(target, false);
  }

  /** * Add a TAB page to close the current page *@param tab 
   */
  addTabRemoveCurrent = tab= > {
    const activeTab = this.activeTab.key;
    this.addTabIfNotExist(tab);
    this.removeTab(activeTab, true);
  }

  /** * switch TAB *@param tab 
   * @param pushHistory 
   */
  switchTab(tab, pushHistory: boolean) :void {
    if(! tab? .active) {// Activate the target value after disabling the current active state
      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); }}}/** * Close TAB *@param key 
   * @param state 
   */
  removeTab(key, ): void {
    // Cannot be closed when there is only one TAB
    if (this.tabList.length === 1) {
      return;
    }
    / /...}}export const tabsStore = new Tabse();
Copy the code

The end of the

Basic usage and advanced usage of Mobx are basically over here, I believe that by reading this article, you can better organize and manage the data of the front-end project

Join us at ❤️

Bytedance Xinfoli team

Nice Leader: Senior technical expert, well-known gold digger columnist, founder of Flutter Chinese community, founder of Flutter Chinese community open source project, well-known developer of Github community, author of dio, FLY, dsBridge and other well-known open source projects

We look forward to your joining us to change our lives with technology!!

Recruitment link: job.toutiao.com/s/JHjRX8B