👉Series of articles👈

The era of componentization

React, Vue, Angular and other libraries (frameworks), the front-end into the era of UI component development. By reasonably dividing the functions of the application, encapsulating them into components from the bottom to the top, and finally constructing a component tree to complete our application:

  • App
    • Page1
      • Component1
      • Component2
      • .
    • Page2
    • .

It looks great, doesn’t it?

In practice, however, there is another obstacle: state management. How to organize and divide the state of the application, how to get the state data that the UI component needs, how to reuse the state component and so on, these questions have puzzled me for a long time.

Flux model

In current status management schemes, Flux mode is the mainstream, representative of which are Redux and Vuex.

For example, if you want to implement an e-commerce system, this system contains “list of goods” and “favorites” two functions, they both contain a similar element structure of the list of goods data, but the source of the data (interface) is different. In Redux, you need to write:

// Reducers## 
function productList (state = fromJS({loading: false, data: []}), {type, payload}) {
    switch (type) {
        case types.PRODUCT_GET_LIST_START:
            return state.merge({loading: true});
        case types.PRODUCT_GET_LIST_SUCCESS:
            return state.merge({loading: false.data: payload});
        case types.PRODUCT_GET_LIST_FAILURE:
            return state.merge({loading: false});
        default:
            returnstate; }}function favorites (state = fromJS({loading: false, data: []}), {type, payload}) {
    switch (type) {
        case types.FAVORITES_GET_START:
            return state.merge({loading: true});
        case types.FAVORITES_GET_SUCCESS:
            return state.merge({loading: false.data: payload});
        case types.FAVORITES_GET_FAILURE:
            return state.merge({loading: false});
        default:
            returnstate; }}// Actions
function getProducts (params) {
    return (dispatch, getState) = > {
        dispatch({type: types.PRODUCT_GET_LIST_START});
        return api.getProducts(params)
            .then(res= > {
                dispatch({type: types.PRODUCT_GET_LIST_SUCCESS, payload: res});
            })
            .catch(err= > {
                dispatch({type: types.PRODUCT_GET_LIST_FAILURE, payload: err});
            });
    };
}

function getFavorites (params) {
    return (dispatch, getState) = > {
        dispatch({type: types.FAVORITES_GET_START});
        return api.getFavorites(params)
            .then(res= > {
                dispatch({type: types.FAVORITES_GET_SUCCESS, payload: res});
            })
            .catch(err= > {
                dispatch({type: types.FAVORITES_GET_FAILURE, payload: err});
            });
    };
}

export const reducers = combineReducers({
    productList,
    favorites
});

export const actions = {
    getProductList,
    getFavorites
};
Copy the code

It can be seen that two almost identical Reducer and action should be written for the same commodity list data loading. Ill, very ill!

You can package it into a factory method to generate it, for example:

function creteProductListReducerAndAction (asyncTypes, service, initialState = fromJS({loading: false, data: []})) {
    const reducer = (state = initialState, {type, action}) = > {
        switch (type) {
            case asyncTypes.START:
                return state.merge({loading: true}); . }};const action = params= > dispatch => {
        dispatch({type: asyncTypes.START});
        return service(params)
            .then(res= > {
                dispatch({type: asyncTypes.SUCCESS, payload: res});
            })
            .catch(err= > {
                dispatch({type: asyncTypes.FAILURE, payload: err});
            });
    }
    
    return {reducer, action};
}
Copy the code

That seems acceptable at first, but what if ONE day I wanted to expand the Reducer of Favorites? As applications begin to grow, the factory approach needs to be constantly reinvented to meet the needs of the business.

The example above is simple, and there are certainly better solutions, and the community has framework offerings such as DVA, but none are perfect: reusing and extending state parts is very difficult.

MobX State Tree: Data componentization

Similar to UI componentization, data componentization solves the problem of Store model being difficult to reuse and extend. Like a React component, it is easy to reuse at various points in the component tree. Using HOC and other means, it is also easy to extend the functionality of components themselves.

MobX State Tree (MST), the main character of this series of articles, is a powerful tool for data componentization.

React, but for data.

MST is known as React for data management. It is based on MobX and absorbs the advantages of Redux and other tools (state serialization, deserialization, time travel, etc.). It can even replace Redux directly, as shown in redux-Todomvc example.

The details of MST will not be covered in the introduction, but let’s see how MST can be used to write the data containers for “itemlists” and “favorites” mentioned above:

import { types, applySnapshot } from 'mobx-state-tree';

// The message notifies BaseModel
export const Notification = types
    .model('Notification')
    .views(self= > ({
        get notification () {
            return {
                success (msg) {
                    console.log(msg);
                },
                error (msg) {
                    console.error(msg); }}; }}));// BaseModel can be loaded
export const Loadable = types
    .model('Loadable', {
        loading: types.optional(types.boolean, false)
    })
    .actions(self= >({ setLoading (loading: boolean) { self.loading = loading; }}));// Remote resource BaseModel
export const RemoteResource = types.compose(Loadable, Notification)
    .named('RemoteResource')
    .action(self= > ({
        asyncfetch (... args) { self.setLoading(true);
            try {
                // self.servicecall is the interface method to get data
                // Need to be defined in action when extending RemoteResource
                const res = awaitself.serviceCall(... args);// self.data is used to store the returned data
                // Needs to be defined in props when extending RemoteResource
                applySnapshot(self.data, res);
            } catch (err) {
                self.notification.error(err);
            }
            self.setLoading(false); }}));/ / commodity Model
export const ProductItem = types.model('ProductItem', {
    prodName: types.string,
    price: types.number,
    ...
});

// Item list data Model
export const ProductItemList = RemoteResource
    .named('ProductItemList')
    .props({
        data: types.array(ProductItem),
    });

// Product list Model
export const ProductList = ProductItemList
    .named('ProductList')
    .actions(self= > ({
        serviceCall (params) {
            returnapis.getProductList(params); }}));// Favorites Model
export const Favorites = ProductItemList
    .named('Favorites')
    .actions(self= > ({
        serviceCall (params) {
            returnapis.getFavorites(params); }}));Copy the code

Accidentally, the code was written more than the Redux version, but if you look closely, the above code encapsulates some fine-grained components, which are then combined and extended in a few lines of code to produce the desired “itemlist” and “favorites” data containers.

In MST, a “data component” is called a “Model”. The definition of a Model uses a chain call method and can repeatedly define props, views, actions, etc. The MST internally merges multiple definitions to form a new Model.

Looking at the implementation code above, we define three BaseModel (the Model that provides the basic functionality), Notification, Loadable, and RemoteResource. Notification provides message Notification, Loadable provides loading state and the method to switch loading state, and RemoteResource provides the ability to load remote resources on the basis of the former two.

The three BaseModel implementations are very simple and have zero coupling to the business logic. Finally, ProductList and Favorites are realized by combining BaseModel and extending corresponding functions.

When building an application, break down the functionality of the application into simple BaseModel pieces so that the application code is pleasing to the eye and easier to maintain.

About this article

This article is the first in a series of articles called MobX State Tree Data Componentization. This article will introduce you to the use of MST and some of the techniques and experiences I have learned while using MST.

The update cycle of this series is uncertain, so I will try my best to spare time to write subsequent articles.

Please note the source, thank you for your support.