Mobx, like Redux, is a great tool for state management. It also follows one-way data flow and works with React partners. Unlike Redux, it has a lower learning cost and a more complete framework (for example, it comes with a solution for asynchronous operations, whereas Redux only provides a middleware architecture that must be implemented with third-party libraries). If Redux is just a successor to Flux, Mobx is another evolution based on Flux

Mobx is an excellent tool, but it has very limited learning resources. Even the official website documentation is limited to API explanations and lacks practical examples (I would recommend the slim volume Mobx Quick Start Guide as a better introduction).

Since the second half of last year, the state management framework in work projects has been gradually replaced by Mobx instead of Redux. These projects also need to deal with large amounts of data and complex interactions, so I have gradually gained some experience in this area. Most of these experiences are potholes THAT I personally tread on, not technical pain points, but tips and patterns on how to make programs easier to maintain. I hope it will be helpful for those of you who are using Mobx for development. This will be compared using the Redux development model, which is easier to understand.

The three recommendations are:

  • Store all state in Mobx (define data structures as schema as possible)
  • Give the state a life cycle (don’t forget caching)
  • While Mobx is fast, follow Mobx’s performance rules

Store all state in Mobx

Recall what states would exist in a single page in a full Redux application:

  • Business state: Such as items added to the cart
  • UI state: whether the submit button is clickable, whether an error message needs to be displayed
  • Application level status: Indicates whether the application is offline or online, and whether the user is logged in

The React component’s state attributes also come from several sources:

  • The state of the parent component itself
  • Store the injection of
  • Store is injected after the selector is evaluated

So when we try to trace or debug the state source of an attribute, we are faced with (3 × 3 =)9 possibilities, especially when you split different states into different reducer, which will bring great difficulties

So my personal first condition recommendation is to store all the states in the same Mobx as much as possible. Notice a few key words here:

  • As much as possible: Using a universal dichotomy, we can always divide state into “shared” (such as “application level state” above) and “unshared” (such as corresponding to a particular business). Since the shared state needs to be referenced across the application, it cannot be assigned to a specific business state and needs to be isolated.
  • In the same place: There are two basic rules to follow when designing states: 1) Each page should have its own state; 2) Each component to be reused should have its own state; Based on the above two points, the React component should be stateless

The page-level state is like a container, bringing together all three of the states described above. For example, on the page for registering a new user, the user name, password, and email information entered by the user belong to the service status, and the error message if the password length is insufficient belongs to the application status. Whether the registration button can be clicked belongs to the UI state. The reason for doing this is not just because states are easy to trace and manage, but because they are inherently related. For example, when the registration information is incomplete, the submit button is naturally unclickable, and when the password is incorrectly filled, the error message should naturally appear. That is, most UI states and application states are “calculated” or “derived” based on business states, and in Mobx, with Autorun, Reaction, and computed, these states need to be computed easily and updated in a timely manner.

Schema is very important

I wonder if you will have such a question, if the different nature of the state together, when you need to submit the page, how to correctly select the business information and assemble them, and ensure that the submitted business information is correct? Here I recommend using schema to ensure that data formats for business information are defined in advance. Using Schema should be the best practice throughout building all large front-end applications, not just Mobx applications. As applications become larger, communication between the different components of a program, both internally and externally, becomes more complex. Defining the schema for each type of message or object that needs to be used in advance helps to ensure the correctness of communication and prevent the passing in of non-existent fields or incorrect types of fields. It also acts as a self-explanatory document for future maintenance. For example, using the hapijs/ JOI library to define a schema for user information:

const Joi = require('joi');

const schema = Joi.object().keys({
    username: Joi.string().alphanum().min(3).max(30).required(),
    password: Joi.string().regex(/ ^ [a zA - Z0-9] {30} 3 $/),
    access_token: [Joi.string(), Joi.number()],
    birthyear: Joi.number().integer().min(1900).max(2013),
    email: Joi.string().email({ minDomainAtoms: 2})})Copy the code

Make similar schema class library very many, the simplest use of ImmutableJS Record type can also play a role in the constraint, here will not expand

On the other hand, the ideal state of a component should be imperceptive to its users, and by “imperceptive” it can mean several things: at the most basic level, its API design should be consistent with the native controls and feel no particularity; Additionally, he doesn’t care about your internal implementation, your internal state management has nothing to do with him. In order to keep the application architecture consistent, I recommend that all state within the component be managed by Mobx, which also takes advantage of all of the advantages mentioned above.

The result is that the React component is stateless, making it easy to test and maintain.

It is worth mentioning that, without the Mobx framework, I still prefer to centralize state management in component states in Redux applications, but the weakness is that the state mechanism does not explicitly provide a state-based derivation mechanism. However, why not manage it in reducer, which brings me to my second recommendation

Give the state a life cycle

When developing Redux applications, have you ever considered the question of what state should be placed in a component’s state? What state should be placed in store? There is a clear answer to this question. Answer it from the official document:

Some common rules of thumb for determining what kind of data should be put into Redux:

  • Do other parts of the application care about this data?
  • Do you need to be able to create further derived data based on this original data?
  • Is the same data being used to drive multiple components?
  • Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?
  • Do you want to cache the data (ie, use what’s in state if it’s already there instead of re-requesting it)?
  • Do you want to keep this data consistent while hot-reloading UI components (which may lose their internal state when swapped)?

Simply put, if the data needs to be shared, put it in the Store. This also implies another thing: data placed in the Store has no life cycle, or, more precisely, its life cycle equals that of the entire application.

Pages and components have a life cycle. For example, you need to continuously input product information in a background page and then click Submit, and then continue to input. It’s the same route, the same component, the same page, but every time you type in, you’re faced with a new page instance, which is the beginning of the page life cycle, until you fill in the information and submit it successfully, which is the end of the page life cycle, which means the instance is destroyed. This is especially true of components, which may even be used multiple times on the same page, and each time the component and state is used it needs to be a new instance

But neither Mobx nor Redux provides such a mechanism. You may use the Mobx-React inject function, which injects a store in context via bindings into the component. It’s the same as connect in Redux-React, regardless of the declaration cycle

Here we use the High Order Component to solve this problem.

Higher-order components solve the problem by simply wiring together store and component instances (encapsulated in another component) and letting them live and die together

Assuming that our higher-order component function is named withMobx, we design the usage of withMobx as follows, similar to Redux’s connect usage:

@withMobx(props= > {
  return {
    userStore: new UserStore()
  }
})
@observer
export default class UserForm extends Component {}Copy the code

The implementation of withMobx is also very simple. Since we are not in the advanced component program, we will not appreciate the various development modes for advanced components. You can also create with acDLite/Recompose. Here’s the handwritten answer:

export default function withMobx(injectFunction) {
  return function(ComposedComponent) {
    return class extends React.Component {
      constructor(props) {
        super(props);
        this.injectedProps = injectFunction(props);
      }
      render() {
        // const injectedProps = injectFunction(this.props);
        return <ComposedComponent {. this.props} {. this.injectedProps} / >; }}; }; }Copy the code

The interesting thing is that this. InjectedProps is evaluated in the constructor of the anonymous class that is to be returned. But in fact it can exist in other places, such as in the comments of the above code, the Render function. But that can lead to some problems, and you can use your imagination here.

Essentially, this is similar to the component’s own state, except we hand it over to Mobx and glue it together through higher-order components

Caching is very important

The frequent creation of store instances results in data waste.

Suppose there is a component to select the city of a country
. You just need to pass in the name of the country, and it can dynamically request all the city names in that country to choose from. However, due to the destruction, the results of the previous request are not saved, causing the same data to be requested again the next time it is sent to the same country. So:

  1. Before each request, check the cache to see if there is any needed data, and use it if there is
  2. After the data is requested (meaning there was no previous cache), the data is stored in the cache

As for the cache module with Map data type can be realized, the key can be set according to specific requirements

Observe performance rules

Although Mobx is better than Redux in some respects, Mobx still has a hood. Here’s an example:

class Person extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    console.log('Person Render')
    const {name} = this.props
    return <li>{name}</li>
  }
}

@inject("store")
@observer
class App extends React.Component {
  constructor(props) {
    super(props);
    setInterval(this.props.store.updateSomeonesName, 1000 * 1)
  }
  render() {
    console.log('App Render')
    const list = this.props.store.list
    return <ul>
      {list.map((person) => {
        return <Person key={person.name} name={person.name} ></Person>
      })}
    </ul>}}Copy the code

The updateSomeonesName method randomly changes the name field of an object in the list. From the actual running state, even if only one object’s field changes, the entire
and other unmodified components of the object are rerendered.

One way to fix this problem is to “decorate” the Observer of the Person component with the same Mox-React library:

@observer
class Person extends React.Component {
Copy the code

The role of the Observer is as follows:

Function (and decorator) that converts a React component definition, React component class or stand-alone render function into a reactive component, which tracks which observables are used by render and automatically re-renders the component when one of these values changes.

The “decorated” component Person will only be re-rendered if the name changes

However, we can do better, in official parlance, by Dereference values late: referencing values only when needed

In the example of the code above, the name field should not be accessed in the App, nor in Person, assuming that we do not use (render) the Name field in the Person component, but instead render it from another component named PersonName. Otherwise it will render meaningless. In simple terms, this is the difference between 1 and 2:

@observer
class Person extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    / / 1:
    return <PersonName person={this.props.person}></PersonName>
    / / 2:
    return <PersonName name={this.props.person.name}></PersonName>}}Copy the code
  • If you use the 1 notation, then whenperson.nameWhen changes are made,<Person />Components are not re-rendered, only<PersonName />It will rerender
  • If you use the 2 notation, then whenperson.nameWhen changes are made,<Person />Components will be rerendered,<PersonName />Components are also re-rendered

Of course, passing the entire object directly into the component is also problematic, resulting in redundancy of data and difficulties for future maintainers

At the end

Of course, there are many more lessons to be learned about developing Mobx apps, but these three are the ones that I personally feel are the most important. Hope to help you


This article is also published in my zhihu front column, welcome your attention