Introduction to MobX

First take a look at the official website:

MobX is a battle-hardened library that makes state management simple and extensible through transparently Applying Functional Programming-TFRP. The philosophy behind MobX is simple: anything derived from application state should be automatically acquired. These include UI, data serialization, server communication, and so on.

The core of MobX is simple, efficient and extensible state management through responsive programming.

React and Mobx relationships

React and MobX work together.

About the official website:

React provides a mechanism to convert application state into and render a tree of renderable components. MobX provides a mechanism to store and update application state for use with React.

Mobx workflow

Here’s an overview of the cleanup process, followed by an introduction to each part in combination with the code.

This article summary

This article uses MobX version 5 and introduces MobX usage from the following aspects:

  1. Configure the MobX development environment for Webpack
  2. MobX API introduction (mainly introduces observable data related operations)
  3. Simple example of MobX

Configure the MobX development environment for Webpack

  • Install webPack and Babel dependencies:
cnpm i webpack webpack-cli babel-core babel-preset-env babel-loader -D
Copy the code
  • Install MobX dependencies:
cnpm i mobx-react -D
cnpm i babel-plugin-transform-class-properties -D 
cnpm i babel-plugin-transform-decorators-legacy -D 
Copy the code
  • Add configuration to webpack.config.js:

Note: Transform-Decorators-Legacy must be placed first.

const path = require('path')

const config = {
    mode: 'development'.entry: path.resolve(__dirname, 'src/index.js'),
    output: {
        path:  path.resolve(__dirname, 'dist'),
        filename: 'main.js'
    },
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
                loader: 'babel-loader'.options: {
                    presets: ['env'].plugins: ['transform-decorators-legacy'.'transform-class-properties']}}}]},devtool: 'inline-source-map'
}

module.exports = config
Copy the code

MobX common API introduction

1. Set up observable

1.1 (@) observables

An observable is a method that allows changes to data to be observed by converting the property into getters or setters.

Observable values can be JS primitive data types, reference types, plain objects, class instances, arrays, and maps.

Observables use

  • forJS primitive type(Number/String/Boolean), usingobservable.box()Method setting:
const num = observable.box(99)
const str = observable.box('leo')
const bool = observable.box(true)

// Get the original value get()
console.log(num.get(),str.get(),bool.get())   // 99 "leo" true

// Set (params)
num.set(100);
str.set('pingan');
bool.set(false);
console.log(num.get(),str.get(),bool.get())  // 100 "pingan" false
Copy the code
  • forAn array of,Object type, the use ofobservable()Method setting:
const list = observable([1.2.4]);
list[2] = 3;
list.push(5) // Array methods can be called
console.log(list[0], list[1], list[2], list[3]) // 1 2 3 5

const obj = observable({a: '11'.b: '22'})
console.log(obj.a, obj.b) / / 11 22
obj.a = "leo";
console.log(obj.a, obj.b) // leo 22
Copy the code

Note that subscripts should be avoided from going out of bounds to values in the method array, as such data will not be monitored by MobX:

const list = observable([1.2.4]);
/ / error
console.log(list[9]) // undefined
Copy the code

Therefore, in actual development, attention should be paid to the judgment of array length.

  • formapping(Map) type, usedobservable.map()Method setting:
const map = observable.map({ key: "value"});
map.set("key"."new value");
console.log(map.has('key'))  // true
map.delete("key");
console.log(map.has('key'))  // false
Copy the code

@ observables use

MobX also offers to use the decorator @Observable to transform it into an observable that can be used on instance fields and properties.

import {observable} from "mobx";

class Leo {
    @observable arr = [1];
    @observable obj = {};
    @observable map = new Map(a); @observable str ='leo';
    @observable num = 100;
    @observable bool = false;
}
let leo = new Leo()
console.log(leo.arr[0]) / / 1
Copy the code

Instead of defining JS primitive types (Number/String/Boolean) using the observable.box() method, the decorator @Observable can define these types directly.

The reason is that the decorator @Observable further encapsulates Observable.box ().

2. Respond to changes in observable data

2.1 (@) computed

Computed values are values that can be computed in combination with existing states or other computed values. You can keep the actual modifiable state as small as possible.

The calculated values are also highly optimized, so use them as often as possible.

It is a value that is automatically updated when the associated state changes, can merge multiple observable data into one observable, and is automatically updated only when it is used.

Knowledge point: how to use

  • Usage Mode 1: Declarative creation
import {observable, computed} from "mobx";

class Money {
    @observable price = 0;
    @observable amount = 2;

    constructor(price = 1) {
        this.price = price;
    }

    @computed get total() {
        return this.price * this.amount; }}let m = new Money()

console.log(m.total) / / 2
m.price = 10;
console.log(m.total) / / 20
Copy the code
  • Use Method 2: Introduce using Pipeline
import {decorate, observable, computed} from "mobx";

class Money {
    price = 0;
    amount = 2;
    constructor(price = 1) {
        this.price = price;
    }

    get total() {
        return this.price * this.amount;
    }
}
decorate(Money, {
    price: observable,
    amount: observable,
    total: computed
})

let m = new Money()

console.log(m.total) / / 2
m.price = 10;
console.log(m.total) / / 20
Copy the code
  • Usage 3: Use Observable. object

Observable. object and extendObservable both automatically derive getters into computed properties, so the following is sufficient:

import {observable} from "mobx";

const Money = observable.object({
    price: 0.amount: 1.get total() {
        return this.price * this.amount
    }
})

console.log(Money.total) / / 0
Money.price = 10;
console.log(Money.total) / / 10
Copy the code
  • Pay attention to the point

If any value that affects the calculated value changes, the calculated value changes automatically based on the state.

If the data used in the previous calculation has not changed, the calculation property will not be rerun. If the calculated property is not used by some other calculated property or reaction, it will not be run again. In that case, it will be suspended.

Setters for computed

Setters for computed cannot be used to change the value of the computed property, but instead to use its members to make computed changes.

Here we use the first declaration method of computed as an example, but the other methods work similarly:

import {observable, computed} from "mobx";

class Money {
    @observable price = 0;
    @observable amount = 2;

    constructor(price = 1) {
        this.price = price;
    }

    @computed get total() {
        return this.price * this.amount;
    }

    set total(n) {this.price = n + 1}}let m = new Money()

console.log(m.total) / / 2
m.price = 10;
console.log(m.total) / / 20
m.total = 6;
console.log(m.total) / / 14
Copy the code

The set total method receives a parameter n as the new value of price. We call m.total and set the new value of price, and the value of m.total changes accordingly.

Note: Always define setters after Geeter; some versions of typescript consider declaring two properties with the same name.

Computed (expression) functions

Generally, the changes can be observed and the calculated values can be obtained by the following two methods:

  • Method 1: WillcomputedUsed as a function call on the returned object.get()To get the current value of the calculation.
  • Method 2: Useobserve(callback)To observe the change in the value, its calculated value in.newValueOn.
import {observable, computed} from "mobx";

let leo = observable.box('hello');
let upperCaseName = computed(() = > leo.get().toUpperCase())
let disposer = upperCaseName.observe(change= > console.log(change.newValue))
leo.set('pingan')
Copy the code

For more detailed computed parameters, see the documentation, Computed Options.

Error handling

If a calculated value throws an exception during evaluation, the exception is caught and thrown when its value is read.

Throwing an exception does not break the trace, and all calculated values can be recovered from the exception.

import {observable, computed} from "mobx";
let x = observable.box(10)
let y = observable.box(2)
let div = computed(() = > {
    if(y.get() === 0) throw new Error('Y is 0.')
    return x.get() / y.get()
})

div.get() / / 5
y.set(0)  // ok
div.get() // error, y = 0

y.set(5)
div.get() // Return to normal, return 2
Copy the code

summary

Usage:

  • computed(() => expression)
  • computed(() => expression, (newValue) => void)
  • computed(() => expression, options)
  • @computed({equals: compareFn}) get classProperty() { return expression; }
  • @computed get classProperty() { return expression; }

There are also various options for controlling computed behavior. Include:

  • equals: (value, value) => booleanComparison function used to override default detection rules. Built-in comparators are:comparer.identity.comparer.default.comparer.structural;
  • requiresReaction: booleanWaiting to be traced before recalculating derived attributesobservablesThe value changes;
  • get: () => value)Overloading the calculation propertygetter;
  • set: (value) => voidOverloading the calculation propertysetter;
  • keepAlive: booleanSet totrueTo automatically keep the calculated value active instead of pausing when there is no observer;

2.2 autorun

concept

Autorun literally means automatic operation, so we need to know these two questions:

  • Auto run what?

That is: automatically run the parameter function passed into Autorun.

import { observable, autorun } from 'mobx'
class Store {
    @observable str = 'leo';
    @observable num = 123;
}

let store = new Store()
autorun(() = > {
    console.log(`${store.str}--${store.num}`)})// leo--123
Copy the code

You can see that Autorun is automatically run once and outputs a value of Leo –123, which is obviously not automatic.

  • How to trigger auto run?

When modifying any of the observable data in Autorun, automatic operation can be triggered.

// The code follows

store.str = 'pingan'

// leo--123
// pingan--123
Copy the code

You can now see that the console prints these two logs, proving that Autorun has been executed twice.

Observe computed data

import { observable, autorun } from 'mobx'
class Store {
    @observable str = 'leo';
    @observable num = 123;

    @computed get all() {return `${store.str}--${store.num}`}}let store = new Store()
autorun(() = > {
    console.log(store.all)
})
store.str = 'pingan'

// leo--123
// pingan--123
Copy the code

It can be seen that the same effect can be achieved by observing computed values in Autorun, which is often used in actual development.

Difference between computed and Autorun

Similarities:

Are all expressions that are called in response;

Difference:

  • @computedUsed to produce a response that can be used by other observersvalue;
  • autorunInstead of generating a new valueAchieve an effect(e.g., printing logs, making network requests, and other imperative side effects);
  • @computedIf a calculated value is no longer being observed, MobX automatically removes itThe garbage collectionAnd theautorunValues in must be manually cleaned.

summary

Autorun executes once by default to get what observable data is referenced.

The role of Autorun is to automatically perform observable dependent behavior after observable data has been modified. This behavior is always passed into autorun’s function.

2.3 the when

The first function must return a Boolean value based on the observable data. When the Boolean value is true, the second function is executed only once.

import { observable, when } from 'mobx'
class Leo {
    @observable str = 'leo';
    @observable num = 123;
    @observable bool = false;
}

let leo = new Leo()
when(() = > leo.bool, () = > {
    console.log('it's true')
})
leo.bool = true
/ / this is true
Copy the code

You can see that when leo.bool is set to true, the second method of when executes.

Pay attention to

  1. The first argument must be a Boolean value returned based on observable data, not a normal variable’s Boolean value.

  2. If the first argument defaults to true, the when function is executed once by default.

2.4 reaction

Accepts two function arguments, the first referencing observable data and returning observable data as an argument to the second function.

When Reaction renders for the first time, the first function is executed so MobX knows which observable data is being referenced. The second function is then executed when the data is modified.

import { observable, reaction } from 'mobx'
class Leo {
    @observable str = 'leo';
    @observable num = 123;
    @observable bool = false;
}

let leo = new Leo()
reaction(() = > [leo.str, leo.num], arr= > {
    console.log(arr)
})
leo.str = 'pingan'
leo.num = 122
// ["pingan", 122]
// ["pingan", 122]
Copy the code

Here we modify the leo. STR and leo.num variables in turn, and see that reaction is executed twice and the console outputs the result twice [“pingan”, 122], because the observable data STR and num have been modified once each.

Actual usage scenarios:

There is no need to perform caching logic when we don’t get the data, we perform caching logic when we get the data for the first time.

2.5 summary

  • Computed can combine multiple observable data into one observable data;

  • Autorun can automatically track referenced observable data and trigger automatically when the data changes;

  • When is a variant of Autorun.

  • Reaction can improve On Autorun by separating observable data claims;

They all have their own characteristics, complement each other, and can play an important role in the right context.

3. Modify observable data

In the last installment, we learned that responding to observable data requires us to manually modify the value of observable data. This is done by assigning a value directly to the variable, which is straightforward, but has the serious side effect of triggering either Autorun or reaction for each change. In most cases, this high-frequency triggering is completely unnecessary.

For example, a user click on a view requires many changes to N state variables, but a view update only needs to be done once.

To optimize this, MobX introduced actions.

3.1 (@) action

Action is the action of modifying any state. The advantage of using action is that you can merge multiple observable states into one, thus reducing the number of times an Autorun or reaction is triggered.

It can be understood as a batch operation, that is, an action contains several changes in the observable state. In this case, only a one-time recalculation and reaction will be performed after the action is completed.

There are also two ways to use actions, which we introduce as follows.

import { observable, computed, reaction, action} from 'mobx'

class Store {
    @observable string = 'leo';
    @observable number = 123;
    @action bar(){
        this.string = 'pingan'
        this.number = 100}}let store = new Store()
reaction(() = > [store.string, store.number], arr= > {
    console.log(arr)
})
store.bar() // ["pingan", 100]
Copy the code

When we run store.bar() after modifying the store.string and store.number variables consecutively, we find that the console output [“pingan”, 100] indicates that reaction is executed only once.

Action. Bound

There is also a special use of action: action.bound, which is often used as a callback argument, and can be executed the same way:

import { observable, computed, reaction, action} from 'mobx'

class Store {
    @observable string = 'leo';
    @observable number = 123;
    @action.bound bar(){
        this.string = 'pingan'
        this.number = 100}}let store = new Store()
reaction(() = > [store.string, store.number], arr= > {
    console.log(arr)
})
let bar = store.bar;
function foo(fun){
    fun()
}
foo(bar) //["pingan", 100]
Copy the code

RunInAction (name? , thunk)

The runInAction is a simple utility function that receives a block of code and executes it in (asynchronous) action. This is useful for creating and executing actions in real time, such as during asynchronous processes. The runInAction(f) is the syntactic sugar for action(f)().

import { observable, computed, reaction, action} from 'mobx'
class Store {
    @observable string = 'leo';
    @observable number = 123;
    @action.bound bar(){
        this.string = 'pingan'
        this.number = 100}}let store = new Store()
reaction(() = > [store.string, store.number], arr= > {
    console.log(arr)
})
runInAction(() = > {
    store.string = 'pingan'
    store.number = 100
})//["pingan", 100]
Copy the code

Mobx-react example

Here, a simple counter is taken as an example to realize the simple operation of clicking the button and adding values, as shown in the figure:

In this case, we use the Mobx-React library to implement mobx-React. It is obvious that mobx-React serves as a bridge between Mobx and React.

It converts the React component into a response to observable data by wrapping the component’s Render method as an Autorun method to automatically rerender when the state changes.

Can view the detail: www.npmjs.com/package/mob… .

Let’s start with our case:

1. Install dependencies and configure WebPack

Since the configuration is similar to that described in section 2, you will add the configuration based on section 2.

First install the mobx-React dependency:

cnpm i mobx-react -D
Copy the code

Modify webpack.config.js to add react to presets:

/ /... Omit the other- entry: path.resolve(__dirname, 'src/index.js'),
+ entry: path.resolve(__dirname, 'src/index.jsx'),
module: {
    rules: [{
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
            loader: 'babel-loader',
            options: {
- presets: ['env'],
+ presets: ['env', 'react'],
                plugins: ['transform-decorators-legacy', 'transform-class-properties']
            }
        }
    }]
},
Copy the code

2. Initialize the React project

Here’s a simple skeleton for our project:

// index.jsx
import { observable, action} from 'mobx';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import {observer, PropTypes as observablePropTypes} from 'mobx-react'

class Store {}const store = new Store();

class Bar extends Component{}class Foo extends Component{
    
}

ReactDOM.render(<Foo />.document.querySelector("#root"))
Copy the code

These components correspond to our final page as shown below:

Implement the Store class

The Store class is used to Store data.

class Store {
    @observable cache = { queue: [] }
    @action.bound refresh(){
        this.cache.queue.push(1)}}Copy the code

3. Implement Bar and Foo components

The implementation code is as follows:

@observer
class Bar extends Component{
    static propTypes = {
        queue: observablePropTypes.observableArray
    }
    render(){
        const queue = this.props.queue;
        return <span>{queue.length}</span>}}class Foo extends Component{
    static propTypes = {
        cache: observablePropTypes.observableObject
    }
    render(){
        const cache = this.props.cache;
        return <div><button onClick={this.props.refresh}>Click on the + 1</button>Current value:<Bar queue={cache.queue} /></div>}}Copy the code

Note here:

  1. Observed data types in the array, in fact, is not an array type, need use observablePropTypes here. ObservableArray to declare its type, object, too.

  2. @Observer references components that need to change their UI based on data transformations. It is also recommended that any class that uses relevant data be referenced.

  3. In fact, we just need to remember the Observer method, which decorates all React components with observers, which is how React -mobx is used.

Use the Foo component

Finally we use the Foo component and need to pass it two parameters so that the Bar component can get it and use it:

ReactDOM.render(
    <Foo cache={store.cache} refresh={store.refresh}/>.document.querySelector("#root"))Copy the code

At the end

Reference for this article:

  • MobX Official Documentation
  • Basic MobX Tutorial