Original link: github.com/whinc/blog/…

MobX is an excellent responsive state management library that provides an alternative to Redux, the popular state management library. If you haven’t tried MobX yet, I strongly encourage you to read on and try it out with the examples. This article is an introduction to MobX, covering MobX’s design philosophy, state-derived model, core apis, and React integration.

MobX is a simple, scalable, responsive state management library. With MobX you can change the state in the most intuitive way, everything else is taken care of by MobX for you (such as automatic UI updates) with very high performance.

MobX documentation has a lot of concepts and apis, which may make you feel lost or unable to grasp the key points at first contact. This article tries to provide a concise tutorial on the core knowledge of MobX as a warm-up before reading the official documentation.

This article does not include anything about decorators, because decorators are not the core of MobX, they are just syntactic sugar and you can use MobX without them.

directory

  • MobX’s design philosophy
  • MobX state response model
  • MobX state-derived model
  • MobX core API parsing
    • observable
    • autorun
    • computed
    • action
  • MobX and React integration
  • summary

MobX’s design philosophy

Before learning to use the MobX API, we should first understand the Design philosophy of MobX, which is the mental model for thinking about MobX applications and helps us to use the MobX API better.

MobX’s design philosophy can be summed up in one sentence: Anything that can be derived from the application state, should be derived. Automatically.

The key to understanding this sentence is to know what the meaning of “derived” is. The meaning of “derived” in MobX is extensive, including:

  • User interfaces (UIs), such as components, pages, diagrams
  • Derived data (computed data), such as the array length computed from an array
  • Side effects include sending network requests, setting scheduled tasks, and printing logs

MobX generalizes this model to form a more general state-derived model, which will be described in more detail below.

MobX state response model

The state response model generally consists of three elements: definition state, response state and modification state (as shown in the figure below).

Observable state is defined by an Observable in MobX. It accepts any JS value (Object, Array, Map, Set) and returns a proxy Object for the raw data. The proxy Object has the same interface as the original data, so you can use it as the raw data.

// Define the state
const store = observable({
  count: 0
});
Copy the code

MobX uses Autorun to define the response to state changes, which takes a function. After that, MobX immediately executes this function whenever the state defined in observable changes.

// Response status
autorun((a)= > {
  console.log("count:", store.count);
});
Copy the code

There is no difference between the way you modify state and the way you modify raw data in MobX, which is the great thing about MobX — it’s intuitive.

// Change the status
store.count += 1;
Copy the code

Strung together, this is the simplest MobX example (below) in which a log is automatically printed every time the count value is changed, and the log contains the latest count value. This example reveals the core functionality of MobX.

import { observable, autorun } from "mobx";
 
// 1. Define the state
const store = observable({
  count: 0
});
 
// 2. Response status
autorun((a)= > {
  console.log("count:", store.count);
});
// count: 0

// 3. Change the status
store.count += 1;
// count: 1
Copy the code

In the example above, the state is first defined by the Observable function provided by MobX. In this example, the state is {count: 0}. The state response function is then defined via MobX’s provided Autorun function, in this case a statement that prints the current count value. When any value in the defined state changes, the response function is executed immediately and is done automatically by MobX. Finally, you modify the state, just as you would with an object property, in this case increment the count property.

MobX state-derived model

We learned from the previous section that MobX’s state response function performs some action when the state changes. In practice, these operations fall into two categories: with side effects (such as printing logs, rendering UI, requesting networks, etc.) and without side effects (such as calculating the length of arrays). MobX refers to these operations collectively as Derivations, which can be derived from anything in the application state.

The operation without side effects is a pure function that generally returns a new value based on the current state. To distinguish between conditions with and without side effects, MobX subdivides the Derivations concept into two categories: Reactions and Computed values. In addition, MobX provides an optional concept, Action, to represent changes to State, to constrain and predict changes to application State. Put together, these concepts are shown below:

Here is a simple example that shows all of the concepts covered in the image above in their entirety.

The index.html file

<html>
  <body>
    <div id="container"></div>
  </body>
</html>
Copy the code

Index. Js file

import { observable, autorun, computed, action } from "mobx"
 
const containerEl = document.querySelector("#container");
 
// State
const store = observable({
  count: 0
});
 
// Actions
window.increaseCount = action((a)= > store.count++);
 
// Computed values
const doubleCount = computed((a)= > 2 * store.count);
 
// Reactions
autorun((a)= > {
  containerEl.innerHTML = `
    <span>${store.count} * 2 = ${doubleCount.get()}</span>
    <button onclick='window.increaseCount()'>+1</button>
  `;
});
 
// 0 * 2 = 0
// (click the button)
// 1 * 2 = 2
// (click the button)
// 2 * 2 = 4
Copy the code

This example shows a simple multiplication in which the data increases and the results of the calculation are updated when the button is clicked. There are several notable differences from the previous example:

  • throughcomputedDefines a calculated value and returns a calculated value objectdoubleCount, through itsget/setMethod to access internally computed values.computedAccepts a function that returns a calculated value, which can be used inside the functionobservableDefined state data (e.gcount), whenevercountWhen the value of is changed,doubleCountInternal values are automatically updated.
  • throughactionDefine state change operations,actionTakes a function and returns a function with the same signature. It can be modified directly inside the functionobsevableDefined states (e.gcount) the triggerautorunandcomputedRerun.
  • autorunThe response operations in the render UI are replaced with a re-render UI every time the state changes.

MobX core API parsing

MobX’S apis fall into four categories: Definition state (Observable), response state (Autorun, computed), modification state (Action), and helper functions. The following highlights the core apis, but not the detailed usage of the APIS (see MobX API Reference).

MobX currently supports both V4 and V5. The TWO versions have the same API (functionality), but the difference is the way they implement data responses internally. Note the notes in the documentation if you use V4.

observable

Observable is used to define observable state. Its type definition is as follows (simplified) :

<T extends Object>(value: T): T & IObservableObject;
<T = any>(value: T[]): IObservableArray<T>;
<K = any, V = any>(value: Map<K, V>): ObservableMap<K, V>;
<T = any>(value: Set<T>): ObservableSet<T>;
Copy the code

It takes Object/Array/Map/Set data as a parameter and returns a proxy Object for the corresponding data. The proxy Object has the same interface as the original data type, and you can use the proxy Object just like the original data. Such as:

import { observable } from "mobx";
 
const object = observable({a: 1})
console.log(object.a)
 
const array = observable([1.2])
console.log(array[0])
 
const map = observable(new Map({a: 1}))
console.log(map.get('a'))
 
const set = observable(new Set([1.2]))
console.log(set.has(1))
Copy the code

Observable data can be nested, and nested data can be observed. Such as:

import { observable, autorun } from "mobx";
 
const store = observable({
  a: {
    b: [1.2]
  }
})
 
autorun((a)= > console.log(store.a.b[0]))
/ / 1
 
store.a.b[0] + =1
/ / 2
Copy the code

Observable allows you to dynamically add observable states. Such as:

import { observable, autorun } from "mobx";
 
const store = observable({});
 
autorun((a)= > {
  console.log("a =", store.a);
});
// a = undefined
 
store.a = 1;
// a = 1
Copy the code

Dynamically adding Observable states is only applicable to MobX V5 +. MobX V4 and later require auxiliary functions. For details, see Direct Observable Manipulation.

autorun

Autorun is used to define the response function, whose type definition is as follows (simplified) :

autorun(reaction: (a)= > any): IReactionDisposer;
Copy the code

Autorun takes a response function reaction and executes it immediately when it is defined. Inside the reaction function, actions with side effects can be performed. In the future, Autorun automatically reruns reaction whenever the dependency state changes. The first time Autorun runs reaction is to collect the dependent states — the states actually used during running reaction (by way of obj.name or obj[‘name’] dereferencing).

For example, in the following example, autorun uses state A, so when the value of state A changes, the response function is executed. Although state B is listed as an observable state, it is not actually used in Autorun, so when the value of state B changes, the response function will not be executed. This is where MobX is “smart” in fine-grained control over the scope of updates based on how the state is actually being used. The program performance is improved by reducing unnecessary execution overhead.

import { observable, autorun } from "mobx";

const store = observable({
  a: 1.b: 2
});
 
autorun((a)= > {
  console.log("a =", store.a);
});
// a = 1
 
store.a += 1;
// a = 2
store.b += 1;
// (no output)
Copy the code

Let’s review MobX usage. State is defined by an Observable, used in Autorun, and reexecuted when the state used in Autorun changes. It works fine most of the time, but if you encounter a state change and Autorun doesn’t behave as you expected, you need to learn more about how MobX reacts to the state change. .

computed

Computed is used to define computed values, and its type definition is as follows (simplified) :

<T>(func: () => T) = > { get(): T, set(value: T): void}
Copy the code

Computed is similar to Autorun in that it restarts when the dependent state changes. The difference is that computed receives a pure function and returns a calculated value that is updated automatically when the state changes and can be used in Autorun.

For example, in the following example, CA is a calculated value that depends on the value of state A. When the value of state A changes, CA recalculates the value. The computed value CA is a “boxed” object that requires access to the inner value via GET /set to keep references to the computed value unchanged and the inner value mutable.

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

const store = observable({
  a: 1
});

const ca = computed((a)= > {
  return 10 * store.a;
});

autorun((a)= > {
  console.log(`${store.a} * 10 = ${ca.get()}`);
});
// 1 * 10 = 10


store.a += 1;
// 2 * 10 = 20
store.a += 1;
// 3 * 10 = 30
Copy the code

Because computed is considered a pure function, MobX provides many out-of-the-box optimizations, such as caching of computed values and lazy computing.

computedValues are cached

Each time a computed value is read, the last cached result is used to reduce computational overhead if the dependent state or other computed values have not changed, and caching can greatly improve performance for complex computed values.

For example, in the following example, computed values ca and CB depend on state A and state B, respectively. When autorun is executed for the first time, both CA and CB recalculate and then modify the value of state A. When Autorun is executed for the second time, only CA recalculates and CB uses the cached results of the last time.

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

const store = observable({
  a: 1.b: 1
});

const ca = computed((a)= > {
  console.log("recomputed ca");
  return 10 * store.a;
});

const cb = computed((a)= > {
  console.log("recomputed cb");
  return 10 * store.b;
});

autorun((a)= > {
  console.log(
    `a = ${store.a}, ca = ${ca.get()}, b = ${store.b}, cb = ${cb.get()}`
  );
});
// recomputed ca
// recomputed cb
// a = 1, ca = 10, b = 1, cb = 10

store.a += 1;
// recomputed ca
// a = 2, ca = 20, b = 1, cb = 10
Copy the code

In order to observe computed computation, you insert statements that print logs, which have side effects, but do not do this in practice

computedThe value is lazily evaluated

Values are recalculated only if computed values are used. Conversely, even if the state of computed value dependence changes but it is not used temporarily, it does not recalculate.

For example, in the following example, computed VALUE CA depends on state A. When the value of state A is less than 3, the Autorun runtime prints only the value of state A, because CA is not used for computed values, so ca is not recalculated. When the value of state A increases to 3, the Autorun runtime prints both state A and computed CA, and since computed CA is used, the CA recalculates.

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

const store = observable({
  a: 1
});

const ca = computed((a)= > {
  console.log("recomputed ca");
  return 10 * store.a;
});

autorun((a)= > {
  if (store.a >= 3) {
    console.log(`a = ${store.a}, ca = ${ca.get()}`);
  } else {
    console.log(`a = ${store.a}`); }});// a = 1

store.a += 1;
// a = 2

store.a += 1;
// recomputed ca
// a = 3, ca = 30
Copy the code

action

Action is used to define state modification operations, and its type definition is as follows (simplified) :

<T extends Function>(fn: T) = > T
Copy the code

Although it is possible to modify the state directly without an action, explicitly modifying the state with an action makes the state change predictable (the state change can be located to which action caused it). In addition, the action function is transactional, and when the state is changed through the action, the response function is not executed immediately, but after the action ends, which helps improve performance.

For example, the following example shows two ways to modify the state: one is to modify state A directly, and the other is to modify the state by calling a predefined action. It is worth noting that changing the value of state A twice in a row in one action triggers only one execution of Autorun.

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

const store = observable({
  a: 1
});

autorun((a)= > {
  console.log(`a = ${store.a}`);
});
// a = 1

store.a += 1;
// a = 2
store.a += 1;
// a = 3

const increaseA = action((a)= > {
  store.a += 1;
  store.a += 1;
});
increaseA();
// a = 5
Copy the code

With some MobX configuration, action can be the only way to change the state, which avoids unfettered state change behavior and improves the maintainability of the project.

For example, in the example below, when enforceActions is configured to “always”, the state can only be modified using actions. Attempting to modify the state directly will trigger an exception.

import { observable, autorun, action,  configure } from "mobx";

// Force the state to be changed only through action
configure({
  enforceActions: "always"
});

const store = observable({
  a: 1
});

autorun((a)= > {
  console.log(`a = ${store.a}`);
});
// a = 1

// store.a += 1;
// Directly modify the state, will raise the following exception
// Error: [mobx] Since strict-mode is enabled, changing observed 
// observable values outside actions is not allowed. 
// Please wrap the code in an `action` if this change is intended.

const increaseA = action((a)= > {
  store.a += 1;
});
increaseA();
// a = 2
Copy the code

MobX and React integration

MobX is framework-neutral. You can use it alone or with any popular UI framework. MobX provides binding implementations for popular frameworks like React/Vue/Angular. MobX is most commonly used with React, and their binding implementation is mobx-React (or mobx-React – Lite).

Mobx-react provides an observer method, which is a higher-order component that receives the React component and returns a new React component that responds (as defined by an Observable) to changes in state. That is, the component updates automatically when observable state changes. The Observer method is a wrapper around MobX’s Autorun method and React component update mechanism for use in React. You can still update components with Autorun in React. Here is the type declaration for observer methods, which support component classes and function components.

function observer<T extends React.ComponentClass | React.FunctionComponent> (target: T) :T
Copy the code

The following is an example of a component class using MobX. It uses an Observable to define the observable state and wraps the component in an Observer. After the state is changed in the component event handler, the component updates automatically.

import React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.store = observable({
      count: 0
    });
  }

  render() {
    return (
      <button onClick={()= > this.store.count++}>
        {this.store.count}
      </button>)}}export default observer(Counter);
Copy the code

Mobx-react useLocalStore defines observable state. UseLocalStore also defines observable state by Observable.

import React, { useMemo } from "react";
import { observable } from "mobx";
import { observer,  useLocalStore } from "mobx-react";

const Counter = (a)= > {
  const store = useLocalStore((a)= > ({
    count: 0
  }));
  // Equivalent to the following
  // const store = useMemo(() => observable({ count: 0 }), []);
  return (
    <button onClick={()= > store.count++}>
      {store.count}
    </button>)};export default observer(Counter);
Copy the code

summary

MobX’s design philosophy is that “anything that can be derived from an application state should be derived automatically”, with two key words: automatic and derived. With this design philosophy in mind, MobX’s derived State model is clear: Computed values and Reactions can be seen as derived from State, Changes in State trigger a recalculation of Computed Values and a rerun of Reations. To make derivations automatic, MobX intercepts reads and writes of objects via Object.definePropery or Proxy, allowing users to change the state in a natural way. MobX is responsible for updating the derivations.

MobX provides several core apis that help define state (Observable), response state (Autorun, computed), and modified state (Action) to make applications instantly responsive. These apis are not complex, but proficiency requires a deep understanding of MobX responses, and simple examples are provided to help understand the behavior of these apis.

MobX can be used on its own or with any popular UI framework, and binding implementations of MobX to popular frameworks can be found on Github. MobX is most commonly used with React. Mobx-react is the popular MobX and React binding implementation library. This article introduces some basic uses of mobx-React in component classes and function components.

reference

  • MobX Documentations
  • mobx-react