Redux is a data management layer that is widely used to manage data for complex applications. But in practice, Redux is poorly used, and arguably not well used. And at the same time, a number of data management solutions have emerged in the community, Mobx being one of them.

Problems with Redux

Predictable state container for JavaScript apps

This is the position Redux has given itself, but there are many problems with it. First, what does Redux do? Looking at Redux’s source code, createStore has only one function that returns 4 closures. The dispatch only does one thing, it calls the reducer and then the listener of the subscribe, where the state is immutable or mutable, all controlled by the user, and Redux doesn’t know if the state has changed, let alone where it has changed. So, if the view layer needs to know which part needs to be updated, it can only do so by dirty checking.

Look at what react-redux does, it hangs a callback on store.subscribe, calls connect to pass in mapStateToProps and mapDispatchToProps every time a subscribe occurs, and then dirty checks every item in props. Of course, we could use the immutable data feature to reduce the number of props and thus reduce the number of dirty detections, but where is the nice thing to have props all coming from the same subtree?

So, if there are n components connected, every time an action is dispatched, no matter what granularity of update is done, O(n) time complexity of dirty detection will occur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Redux 3.7.2 createStore.js

// ...
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
// ...

Worse still, each time the reducer is executed Redux calls the listener directly, and if multiple changes (e.g. user input) occur in a short period of time, the immutable overhead, combined with the overhead of redux matching actions with strings, the overhead of dirty detection, and the overhead of the view layer, the overall performance can be very bad, even when the user input The performance can be very bad, even when the user input is often updated with only one “input”. The larger the application, the worse the performance. (Here the application refers to a single page. The single page here does not mean a single page of SPA, because with Router, all components of the page that is cut away are unmounted)

As the size of the application increases, and the number of asynchronous requests increases, the Predictable advertised by Redux is simply a bubble, and more often than not, it is reduced to a data visualization tool with various tools.

Mobx

Mobx is arguably the most complete of many data solutions. mobx itself independent, not interdependent with any view layer framework, so you can choose the appropriate view layer framework at will (except for some, such as Vue, because they are the same principle).

Mobx (3.x) and Vue (2.x) currently use the same responsive principle, to borrow a diagram from the Vue documentation.

Create a Watcher for each component, add hooks to the getter and setter of the data, trigger the getter when the component is rendered (e.g., call the render method), and then add the Watcher corresponding to this component to the dependency of the data associated with the getter (e.g., a Set). When the setter is triggered, it knows that the data has changed, and then the corresponding Watcher goes to redraw the component at the same time.

In this way, the data needed by each component is precisely known, so when the data changes, it is possible to know exactly which components need to be redrawn, and the process of redrawing when the data changes is O(1) time complex.

Note that in Mobx, the data needs to be declared as observable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from 'react';  
import ReactDOM from 'react-dom';  
import { observable, action } from 'mobx';  
import { Provider, observer, inject } from 'mobx-react';

class CounterModel {  
    @observable
    count = 0

    @action
    increase = () => {
        this.count += 1;
    }
}

const counter = new CounterModel();

@inject('counter') @observer
class App extends React.Component {  
    render() {
        const { count, increase } = this.props.counter;

        return (
            <div>
                <span>{count}</span>
                <button onClick={increase}>increase</button>
            </div>
        )
    }
}

ReactDOM.render(  
    <Provider counter={counter}>
        <App />
    </Provider>
);

Performance

In this article , the authors use a 128*128 drawing board to illustrate the problem. Since Mobx uses getter and setter (a parallel Proxy-based version may appear in the future) to collect the data dependencies of component instances, every time a point is updated, Mobx knows which components need to be updated and the time complexity of the process of deciding which component to update is O(1), while Redux gets which components need to be updated by dirty checking each connect component, and with n components connect the time complexity of this process is O(n), which is ultimately reflected in the execution time of JavaScript in the Perf tool.

Although, after a series of optimizations, the Redux version can get a performance that does not lose the Mobx version, when Mobx can get a good performance without any optimization. The most perfect optimization of Redux is to create a separate store for each point, which is the same idea as a bunch of solutions like Mobx that pinpoint data dependencies.

Mobx State Tree

Mobx is not perfect; Mobx does not require the data to be in a tree, so it is not easy to make Mobx data cubic or record every data change. Based on Mobx, the Mobx State Tree was born. Like Redux, the Mobx State Tree requires data to be in a tree, making it easy to visualize and track data, which is a boon for developers. The Mobx State Tree also makes it very easy to get accurate TypeScript type definitions, which is not easy to do with Redux. Runtime type safety checks are also provided.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from 'react';  
import ReactDOM from 'react-dom';  
import { types } from 'mobx-state-tree';  
import { Provider, observer, inject } from 'mobx-react';

const CountModel = types.model('CountModel', {  
    count: types.number
}).actions(self => ({
    increase() {
        self.count += 1;
    }
}));

const store = CountModel.create({  
    count: 0
});

@inject(({ store }) => ({ count: store.count, increase: store.increase }))
class App extends React.Component {  
    render() {
        const { count, increase } = this.props;

        return (
            <div>
                <span>{count}</span>
                <button onClick={increase}>increase</button>
            </div>
        )
    }
}

ReactDOM.render(  
    <Provider store={store}>
        <App />
    </Provider>
);

Mobx State Tree also provides the snapshot function, so that although the MST data itself is variable, it can still hit the effect of immutable data. Officially, you can use snaptshot directly in conjunction with Redux development tools to facilitate development, and you can also use the MST data as a Redux store; of course, you can also use snapshot to embed the MST in a Redux store as data (similar to what is popular in Redux Immutable.js).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Development tools for connecting to Redux
// ...
connectReduxDevtools(require("remotedev"), store);  
// ...

// Used directly as a Redux store
// ...
import { Provider, connect } from 'react-redux';

const store = asReduxStore(store);

@connect(// ...)
function SomeComponent() {  
    return <span>Some Component</span>
}

ReactDOM.render(  
    <Provider store={store}>
        <App />
    <Provider />,
    document.getElementById('foo')
);

// ...

And, in MST, variable data and immutable data (snapshot) can be transformed into each other, and you can apply snapshot to data at any time.

1
2
3
applySnapshot(counter, {  
    count: 12345
});

In addition, official support for asynchronous actions is also provided. Due to the limitations of JavaScript, asynchronous actions are difficult to track, and even if an async function is used, it cannot be tracked during execution. Previously, asynchronous actions could only be accomplished by combining multiple actions, but Vue does so by separating the action and the mutation. The Mobx State Tree makes use of the Generator so that asynchronous actions can be done within a single action function and can be tracked.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ...

SomeModel.actions(self => ({  
    someAsyncAction: process(function* () {
        const a = 1;
        const b = yield foo(a); // foo must return a Promise
        self.bar = b;
    })
}));

// ...

Summary

Mobx uses getter and setter to collect data dependencies of components so that it knows exactly which components need to be redrawn when the data changes. When the interface gets large, there are often many fine-grained updates, and while responsive design has additional overhead, at large interface sizes this overhead is far smaller than doing dirty checks on each component. So in this case Mobx will easily get better performance than Redux. Dirty check-based implementations have better performance than Mobx responsive ones when all the data changes, but this is rare. Also, some benchmarks are not best practices, and their results do not reflect the real situation.

However, because React itself provides mechanisms to reduce useless rendering using immutable data structures (e.g. PureComponent, functional components), and because some of React’s ecology is bound to Immutable (e.g. Draft.js), it is not so comfortable when working with variable observer pattern data structures. So, it is recommended to use Redux and Immutable.js with React until you encounter performance issues.

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.

Some practices

Due to the limitations of JavaScript, some objects are not native objects and other type checking libraries may lead to unexpected results. For example, in Mobx, an array is not an Array, but an Array-like object, which is designed to listen for the assignment of data subscripts. In contrast, in Vue the array is an Array, but the array subscript assignment is done using splice, otherwise it is not detected.

Due to the principle of Mobx, to do accurate on-demand updates, you have to trigger the getter in the right place, and the easiest way to do that is to render the data to be used and only deconstruct it in render. mobx-react From 4.0 onwards, the structure in the map function accepted by inject is also tracked, so it can be written directly in a way similar to react-redux. Note that prior to 4.0 inject’s map functions were not tracked.

Responsive has additional overhead that can have a performance impact when rendering large amounts of data (e.g., long lists), so use observable.ref, observable.allow (Mobx), and types.frozen (Mobx State Tree) in a sensible way.


Reference https://tech.youzan.com/mobx_vs_redux/