Redux is a data management layer that is widely used to manage data for complex applications. But in practice, Redux doesn’t work as well as it should. At the same time, there are some data management solutions emerging in the community, and Mobx is one of them.
The problem of story
Predictable state container for JavaScript apps
That’s how Redux positioned itself, but there are a lot of problems with it. First of all, what did Redux do? CreateStore has only one function that returns four closures. Dispatch only does one thing, calling reducer and then subscribe listener, in which immutable or variable state is all controlled by the user, Redux does not know whether state has changed, let alone where the state has changed. So, if the View layer needs to know which parts need to be updated, the only way to do that is through a dirty check.
Subscribe to store. Subscribe. Each time a subscribe occurs, connect is called to pass in mapStateToProps and mapDispatchToProps. Sure, we can take advantage of immutable data to reduce the number of props and thus reduce the number of dirty checks, but what’s so good about all props coming from the same subtree?
So, if there are n connect components, O(n) time complexity dirty detection will occur every time an action is dispatched, regardless of the granularity of the update.
/ / 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()
}
// ...Copy the code
To make matters worse, the listener is called directly after each reducer execution. If there are many changes (such as user input) in a short period of time, the immutable overhead, the overhead of Redux matching actions with strings, the overhead of dirty checks, and the overhead of the View layer, Overall performance can be very poor, even though the user often only needs to update an “input” when typing. The larger the application, the worse its performance. (By application, I mean a single page. Router Router Router Router Router Router Router Router Router Router Router Router Router Router Router Router Router Router Router
While the app scale increased and the number of asynchronous requests was large, the Predictable operation promoted by Redux was all gone and most of the time was reduced to data visualization tool with various tools.
Mobx
Mobx is arguably the most complete data solution out there. Mobx stands on its own and doesn’t depend on any view layer frameworks, so you can choose the right view layer frameworks (with some exceptions, like Vue, because they work the same way).
Currently Mobx (3.x) and Vue (2.x) use the same reactive principle, using a diagram from the Vue documentation:
Create a Watcher for each component, add hooks to the getter and setter of the data, fire the getter when the component renders (for example, calling the Render method), and then add the component’s corresponding Watcher to the dependencies of the getter related data (for example, a Set). When the setter is fired, it knows that the data has changed, and the corresponding Watcher redraws the component at the same time.
In this way, the data required by each component is precisely known, so that when the data changes, it can precisely know which components need to be redrawn. The redrawing process when the data changes is O(1) time complexity.
Note that in Mobx, you declare the data as an Observable.
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 = (a)= > {
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>
);Copy the code
performance
In this article, the author uses a 128*128 drawing board to illustrate the problem. Because Mobx uses getters and setters (a parallel proxy-based version is likely to emerge) to collect data dependencies for component instances, Mobx knows which components need to be updated every time a single point is updated, and the process of deciding which components need to be updated is O(1) in time. Redux does a dirty check on each connect component to see which components need to be updated. There are n connect components and the time complexity of this process is O(n), which is reflected in the Perf tool as the execution time of JavaScript.
Although Redux’s version was able to achieve the same performance as Mobx’s version after a series of optimizations, Mobx was able to achieve good performance without any optimizations. The perfect optimization for Redux is to create a separate store for each point, which is the same idea as Mobx and other solutions that pinpoint data dependencies.
Mobx State Tree
Mobx is not perfect. Mobx does not require data to be in a tree, so it is not easy to digitize Mobx or log every data change. Based on Mobx, Mobx State Tree was born. Like Redux, Mobx State Tree requires data to be in a Tree, making it easy to visualize and track data, which is a boon for development. Mobx State Tree also makes it easy to get accurate TypeScript type definitions, something Redux doesn’t. Runtime type safety checks are also provided.
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>
);Copy the code
Mobx State Tree also provides a snapshot function, so even though MST data itself is variable, it can still be immutable data effect. Official provides the use of Snaptshot directly combined with Redux development tools, convenient development; The MST data can be used as a Redux store. Of course, snapshot can also be MST embedded in Redux’s store as data (similar to immutable.js which is popular in Redux).
// Connect to Redux's development tools
// ...
connectReduxDevtools(require("remotedev"), store);
// ...
// Use it 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')
);
// ...Copy the code
In MST, mutable data and immutable data (Snapshot) can be converted to each other. You can apply snapshot to data at any time.
applySnapshot(counter, {
count: 12345
});Copy the code
In addition, there is official support for asynchronous actions. Due to the limitations of JavaScript, it is difficult to track asynchronous operations. Even if async functions are used, they cannot be tracked in the execution process. Although data is manipulated in async functions, the async function is also marked as action, but it will be misjudged as modifying data outside the action. Whereas previously asynchronous actions could only be done by combining multiple actions, Vue implemented by separating action and mutation. In the Mobx State Tree Generator is utilized so that asynchronous operations can be completed within an Action function and can be traced.
// ...
SomeModel.actions(self= > ({
someAsyncAction: process(function* () {
const a = 1;
const b = yield foo(a); // Foo must return a Promiseself.bar = b; })}));// ...Copy the code
conclusion
Mobx uses getters and setters to collect data dependencies for components to know exactly which components need to be redrawn as the data changes. As the interface grows in size, there are often many fine-grained updates. Although there are additional overhead associated with responsive design, when the interface is large, This overhead is far less than doing a dirty check on each component, so Mobx can easily achieve better performance than Redux in this case. Dirty checket-based implementations perform better than Mobx types when all data changes, but these are rare. At the same time, some benchmarks are not best practices and their results do not reflect the real world.
However, since React itself provides a mechanism to reduce useless rendering using Immutable data structures (e.g. PureComponent, functional component), and some of React’s ecologically and Immutable bindings (e.g. draft.js), Therefore, it is not so comfortable to work with the data structure of the variable observer mode. Therefore, it is recommended to use Redux and immutable. js with React before running into performance problems.
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 practical
Due to JavaScript limitations, some objects are not native objects, and other type-checking libraries can lead to unexpected results. In Mobx, for example, an Array is not an Array, but an array-like object, in order to listen for subscript assignments. In contrast, an Array in Vue is an Array, but Array subscript assignment is performed using splice, otherwise it cannot be detected.
Due to Mobx principles, getters are triggered in the right place for accurate on-demand updates, and the easiest way to do that is for render to only deconstruct the data to be used in render. Mobx-react as of 4.0, the structure of the map function accepted by Inject will also be traced, so it can be directly written like react-redux. Note that the map function of Inject was not traced prior to 4.0.
Responsivity has additional overhead that can impact performance when rendering large amounts of data (e.g., long lists), so use a reasonable mix of observables. Ref, Observables. Shallow (Mobx), and types.frozen (Mobx State Tree).
This article was originally posted on The Uptech blog.