Preface ✍ ️

Front-end scenarios are increasingly complex. At present, new projects use Vue, Angular, and React to manage the mapping relationship between data and view. Each of them has its own unique mechanism to manage component state and life cycle. State containers like Vuex, Ngrx, and Redux are used to manage important global states.

React is the main application in my work. In the project, I used Mobx as a supplement to React state management to speed up the efficiency of coding. This paper mainly records some usage of Mobx.

Introduce 📚

Mobx is a state management library, which has unique advantages in the description of state dependencies, just like writing formulas. It allows developers to more succinct declare the description of attribute state dependencies, and automatically complete the update of related dependencies, causing side effects.

Usage 🔧

Mobx is very flexible. You can directly apply observable features to an object, declare them in a class, or even write them directly into the properties of the React component class (no different from the class in Mobx’s perspective).

Directly decorate object

Objects wrapped directly by An Observable gain Mobx’s capabilities.

import * as mobx from "mobx";

// Declare an object to be observable
const myObj = mobx.observable({
  a: 1,
  b: 3.get c() {
    return this.b * 2; }});// Register a side effect function
mobx.autorun((a)= > {
  console.log("a", myObj.a);
});
mobx.autorun((a)= > {
  console.log("c", myObj.c);
});
// Change the properties of this object
myObj.a = false;
myObj.a = "hello";
myObj.b = 4;

A 1 c 6 a false a hello c 8 */
Copy the code

You can already see some of Mobx’s features

  1. Make an object observable.
  2. The registered side effects function fires automatically when the object property changes.
  3. Side effects do not trigger when properties unrelated to side effects change, as in the previous examplemyObj.b, changes will not trigger only withaAbout side effects.
  4. cthisgetterAttribute will beautorunSide effects were recorded aboutbDepend on, whenbChanges occur, associationscSide effects can also be triggered.

Knowing the rules above, you can try it directly in your project.

Declare its attributes in a class asobservable

import { observable, computed, action, autorun, flow } from "mobx";

// Use the property decorator declaration
class SimpleStore {
  @observable a = 1;
  @observable c = 2;

  @computed get b() {
    return this.a * this.a + 1;
  }
  @action setA(a) {
    this.a = a;
  }
  asyncUpdate = flow(function* () {
    const next1 = yield new Promise(res= > setTimeout((a)= > res(3), 1000));
    this.a = next1;
    const next2 = yield new Promise(res= > setTimeout((a)= > res(4), 1000));
    this.a = next2;
  });
}

const store = new SimpleStore();

autorun((a)= > {
  console.log(store.a);
});

store.setA(2);
store.asyncUpdate();
SetA (2) 3 // asyncUpdate() 1s 4 // asyncUpdate() 2s */
Copy the code

There are a few more things you can see from the observable declaration of the class:

  1. You need to use the decorator syntax for quick declarationsMobxRelated functions require more input than working directly with objects, but can have more fine-grained control over their operation
  2. The instantiated class object is also ownedobservableThe ability to add to the field@observable, the field will be recorded by the side effects.
  3. @actionIt’s used to declare change@observableField method. If the following configuration is enabled, it is mandatory@actionMethod to modify the property, otherwise an error is reported.
mobx.configure({
    enforceActions: true
});
Copy the code
  1. flowisMobxProvides a modifier for asynchronyactionMethods. It’s really just aasync/awaitmethodsGeneratorImplementation, the best feature of this method is that it returns a Promise that can be cancelled.

inreactUse in componentsMobx

Since the React component listens for observable changes, the render logic is a side effect. The correct way to use Autorun is to introduce the Mobx-React library. Import the Observer, a high-level component, to automatically register and destroy autorun.

Type of component

import * as React from "react"; import * as ReactDOM from "react-dom"; import { observable } from "mobx"; import { observer, Observer } from "mobx-react"; // React render extends class Counter extends React.Component {// It can be used as an Observable. Instead of react's own state, the update property is a bit more straightforward than setState. @observable unused = 0; handleInc = () => this.count++; handleDec = () => this.unused--; render() { console.log("render"); return ( <div> {this.count} <button onClick={this.handleInc}>+</button> <button onClick={this.handleDec}>-</button> </div> ); }}Copy the code

Unused does not result in rerendering because render only declares the use of count, and is wrapped with autorun for higher-order components. Autorun actually returns a value. Used to eliminate this side effect, but is automatically destroyed by the React unmount life cycle.

Function component

import * as React from "react"; import * as ReactDOM from "react-dom"; import { observable } from "mobx"; import { observer, Observer, useLocalStore } from "mobx-react"; Const Counter = Observer (() => {// Create a local Observable with useLocalStore const local = useLocalStore(() => ({ count: 0, unused: 0, handleInc() { this.count++; }, handleDec() { this.unused--; }})); console.log("render"); return ( <div> {local.count} <button onClick={() => local.handleInc()}>+</button> <button onClick={() => local.handleDec()}>-</button> </div> ); });Copy the code

Note: Use observer:









}/> Put it in the new Autorun context, otherwise the update will not take effect.




Outside of theobservableobject

You can create an Observable object or class directly outside and consume it with an Observer. Here’s how to use the global Store.

import * as React from "react"; import * as ReactDOM from "react-dom"; import { observable } from "mobx"; import { observer, Observer, useLocalStore } from "mobx-react"; const store1 = observable({ a: 1, b: "hello", incA() { this.a++; }, repeatB() { this.b += this.b; }, asyncIncA() { setTimeout(() => { this.a++; }, 1000); }}); //// {const stores = {store1}; type TStore = typeof stores; const storeCtx = React.createContext<TStore>(stores); const StoreProvider = ({ children }) => ( <storeCtx.Provider value={stores}>{children}</storeCtx.Provider> ); const useSore = () => React.useContext(storeCtx); ////} // Main code const UsingStore = observer(() => {const {store1} = useSore(); return ( <div> <div>a:{store1.a}</div> <div>b:{store1.b}</div> <button onClick={() => store1.incA()}>incA</button> <button onClick={() => store1.asyncIncA()}>asyncIncA</button> <button onClick={() => store1.repeatB()}>repeatB</button> </div> ); }); @observer class UsingStoreInClass extends React.Component { static contextType = storeCtx; render() { const { store1 } = this.context as TStore; return ( <div> <div>a:{store1.a}</div> <div>b:{store1.b}</div> <button onClick={() => store1.incA()}>incA</button> <button onClick={() => store1.repeatB()}>repeatB</button> </div> ); } } const App = () => { return ( <> <StoreProvider> <UsingStore /> <br /> <UsingStoreInClass /> <br /> </StoreProvider> < / a >); };Copy the code

The main piece of code is to create a store and put it into the Context, then class components and function components are decorated with an observer, take the global state out of the Context and use it, and when the global state is updated, the related components are notified and rerender.

Use the React lifecycle function to do this for you. The react lifecycle function does this for you. The react lifecycle function does this for you. In particular, the Hooks provided by React V16.8 are a good choice, and should only put critical global mutable state, such as user information, into the global Store.

Some skills

debugging

When an Observable wraps an object or property, it recursively converts it to an Observable, which is inconvenient to debug in console.log and full of proxies (if Mobx 5.x is used). You can use mobx.tojs to turn it into a normal object

To optimize the

In some cases, recursion to transform an attribute into an Observable is too fine-grained and unnecessary. In fact, it can reduce the Proxy overhead in this part by using observable.ref and Observable. shallow for the attribute. Or use option {deep:false} when creating an observable. Object, observable. Array, or Observable.

The chain reaction

In complex scenarios, computed attributes are often obtained asynchronously based on dependencies, so using computed is not appropriate. You can use multiple Observables and reaction to perform fetch logic.

import * as React from "react"; import * as ReactDOM from "react-dom"; import { observable, reaction, autorun } from "mobx"; import { observer, Observer, useLocalStore } from "mobx-react"; class ChainDemo { @observable a = 0; @observable b = 0; @observable c = 0; @observable d = 0; init = () => { const disposer = [ reaction( () => { const val = this.a;  return new Promise<number>(res => setTimeout(() => res(val + 1), 100) ); }, async p => { this.b = await p;  } ), reaction( () => { const val = this.b; return new Promise<number>(res => setTimeout(() => res(val + 1), 100) );  }, async p => { this.c = await p; } ), reaction( () => { const val = this.c;  return new Promise<number>(res => setTimeout(() => res(val + 1), 100) ); }, async p => { this.d = await p; } ) ]; return () => disposer.forEach(d=>d()); }; } const chain = new ChainDemo(); chain.init(); autorun(() => { console.log(chain.a, chain.b, chain.c, chain.d); }); chain.a = 2; /** ** 0 0 0 0 * 2 0 0 0 * 2 3 0 0 * 2 3 4 0 * 2 3 4 5 * /Copy the code

In the above example, several delayed computed values are used, and the state is gradually updated according to the react chain we described. When the change is fast, more granular and controllable performance optimization can be done with flow and Debounce.

byreactDepartment of Life Cycle Managementobservablestate

Continuing with the above code, a series of reactions returns to the Disposer of many disposers, so it is perfect to put this init directly to useEffect, taking advantage of the component’s life cycle to complete the initialization and destruction of the state.

const Comp = observer(() => {
  const [state] = useState(() => new ChainDemo());
  useEffect(state.init, [state]);
});
Copy the code

Give it a thumbs up if you think it’s good