MobX Practice Guide

I. Overview

Introduction to the

MobX is a state-focused library that is second only to Redux in popularity in the React world. But MobX has its own unique advantage: it makes state management simple, efficient, and free by using transparent functional response programming.

MobX philosophy

Anything derived from the application state should be acquired automatically.

Core principles

Observable of data is implemented by intercepting object attribute changes with defineProperty(<= V5) or Proxy(v6), relying on collection in GET and triggering binding dependent listener functions in SET. If you’ve looked at the reactive principles of some of the MVVM frameworks, vue.js, Knockout, etc., you should be familiar with them. Yes, the principle is the same.

The core concept

Not only are the principles very similar, but the underlying concepts and top-level Api design are also very similar. Data, computed and watch in Vue.js almost correspond to observabily-State, computed and reaction in Mobx. The biggest difference is that MobX implements the important step of state management by restricting how state is updated by actions. The overall operation process is shown in the figure below.

The installation

Mobx is a package that provides all of mobx’s base apis independent of the specific framework platform. Such as observable, makeObservable, Action, etc.

npm i mobx
Copy the code

If you use react, you need to add the mobx-React package for React

npm i mobx mobx-react
Copy the code

If you’re developing React using only functional components and not class components, you can replace mobx-React with a lighter package, mobx-React-Lite

NPM I mobx mobx-react lite compared to mobx-react - Removed support for class Components, 2. And removed provider and inject (cause: Both HOC are not necessary after React. CreateContext is provided)Copy the code

Second, practice

1. The statement Store

MobX recommends using classes to create stores rather than ordinary objects, mainly because classes are more friendly to the TS type system and are easier to index for auto-completion and other functions.

There are three ways to declare

Method 1. Direct use of common objects (not recommended)

import { observable,action } from 'mobx'

const userStore = observable({
  roleType:1
})

export const changeRoleType = action((val)=>{
  userStore.roleType = val
})

export default userStore
Copy the code

2. Use classes + decorators (recommended before V6)

import { observable } from 'mobx' class UserStore{ @observable roleType=1 @action changeRoleType(val){ this.data = val }  } export default UserStoreCopy the code

Use class + makeObservable (recommended in V6) (the reason why decorators are no longer recommended can be found in Q&A)

import { makeObservable,observable,computed,action } from 'mobx' class UserStore{ constructor(){ makeObservable(this,{ roleType:observable, roleName:computed, changeRoleType:action }) } roleType = 1 get roleName(){ return roleMap[roleType] } changeRoleType(val){ this.roleType = val } } export default UserStore //or import { makeAutoObservable } from 'mobx' class UserStore{ constructor(){ MakeAutoObservable (this) /* Declarations that do not need to be displayed are automatically decorated with the appropriate Mox-API. For example, (1) value fields are inferred as Observable, (2) GET methods, (3) computed, (3) common methods, and action is automatically applied. (4) If you need to customize some fields, Please refer to the method of [the other] (https://zh.mobx.js.org/observable-state.html#makeautoobservable) * /} roleType = 1 get roleName () {return roleMap[roleType] } changeRoleType(val){ this.roleType = val } } export default UserStoreCopy the code

Implement communication between stores

Example: In a role management module, because they have administrator rights, the power is so big that they can even change their role type, so when such operation, it is necessary to synchronize the modification of RoleStore to UserStore, which involves the communication between multiple stores. Create a common parent rootStore to read state between multiple stores.

Constructor (rootStore){this.rootStore = rootStore makeAutoObservable(this)} uid = 'zyd123' roleType = 1 changeRoleType(val){this.roleType = val}} constructor(rootStore){  this.rootStore = rootStore makeAutoObservable(this) } changeUserRoleType(uid,type){ const {userStore} = this.rootStore If (uid === userStore.uid){//*** synchronize userStore *** userStore.changeroleType (type)... }else{// Change someone else's role type... }}} // Create a new layer rootStore, Constructor () {this.userStore = new userStore (this) this.roleStore = new roleStore (this) } } const rootStore = new RootStore() export default rootStoreCopy the code

2. Use the React component

2.1 the observer

What it does: Automatically subscribes to observable properties that are used during react component rendering. When they change, the component automatically rerenders them. As mentioned earlier in the overview, MobX’s core capability is the ability to publish all dependencies collected in data GET in a set at once. In the React scenario, the idea is to link state to component rendering. Once the state changes, all components that use that state need to be re-rendered, and the key to this is the Observer. The usage is as follows: (Demo: implements a function to change global roles, RoleManage component takes care of the change, UserInfo component takes care of the display) SRC /demos/ userinfo.jsx

import { observer } from "mobx-react"; RootStore import rootStore from './.. /store'; Const {userStore} = rootStore; Class UserInfo extends Component {render() {const {roleName} = userStore; class UserInfo extends Component {render() {const {roleName} = userStore; return ( <Row justify="space-between"> <Col></Col> <Col span={5} className='border'> <Space align='center'> < span > current role types: < / span > < h2 > {roleName} < / h2 > < Space > < / Col > < / Row > "); Observer HOC wraps around the component, giving MobX's powerful responsive update capabilities to the React component. export default observer(UserInfo)Copy the code

src/demos/RoleManage.jsx

import rootStore from './.. /store' const { userStore } = rootStore; Class RoleManage extends Component {handleUpdateRoleType = ()=>{//(2) Userstore.changeroletype (2)} render() {return <Button OnClick = {this. HandleUpdateRoleType} > change roles < / Button >}} export default RoleManage;Copy the code

2.2 the Provider, inject

What it does: In this example, you can see that the global Store is introduced as a file.

import rootStore from './.. /store' const { userStore } = rootStore;Copy the code

This method is cumbersome and not conducive to maintenance. If store files are reorganized, the introduced places need to be changed and checked everywhere. So, is there a way that Store can be easily referenced across all components in a single injection during project development? The answer is to use Provider and inject. Let’s refactor the previous example: SRC /index.jsx

import App from "./App"; Import {Provider} from 'mobx-react' import store from './store' // reactdom.render (<Provider {... store}> <App/> </Provider>, document.getElementById("root") );Copy the code

src/demos/UserInfo.jsx

Class UserInfo extends Component {render() {const {roleName} = this.props. UserStore; return ( <Row justify="space-between"> <Col></Col> <Col span={5} className='border'> <Space align='center'> < span > current role types: < / span > < h2 > {roleName} < / h2 > < Space > < / Col > < / Row > "); Export default inject('userStore')(observer(UserInfo))Copy the code

Provider and Inject look very similar to the context Api introduced by React, and solve basically the same problems. In fact, the latest version of Mobx-React is based on the latter, which shows that the two apis are not necessary for react application development. So when MobX officially launched mobx-React-Lite for the React platform, they excluded these apis first. Provider and inject are still recommended if you use class components, because contextApi is not very convenient, but if you use hooks, you don’t need to use Provider and inject. The convenience and simplicity of useContext greatly reduces the need to use them (more on their usage later).

2.3 MobX + Hooks

The function component +hooks is currently the preferred way to develop React applications. MobX followed the trend with a new Hook Api that has become the dominant way to use MobX.

2.3.1 Using the Global Store

User-defined useStore replacement Provider, inject the following example the author will use mobx-React-Lite lightweight package to write. This package does not provide Provider or inject, but it does not matter. CreateContext and useContext are provided by React. Below we begin to package a good use of usestore-hook. src/store/index.js

. Export const rootStoreContext = react.createcontext (rootStore) /** * @description Store * @param {*} storeName Component name. Functions like inject(storeName), No default rootStore */ export const useStore = (storeName) => {const rootStore = react. useContext(rootStoreContext) if (storeName) { const childStore = rootStore[storeName] if (! ChildStore) {throw new Error(' storeName ')} return childStore} return rootStore}Copy the code

src/index.jsx

- import { Provider } from 'mobx-react'

+ import rootStore, {rootStoreContext} from './store'
+ const { Provider } = rootStoreContext

ReactDOM.render(
     <Provider value={rootStore}>
          <App/>
      </Provider>,
      document.getElementById("root")
Copy the code

src/demos/UserInfo.jsx

-import {observer} from "mobx-react"; + import { observer } from "mobx-react-lite"; import { Row, Col, Space } from "antd"; + import { useStore } from '.. /store'; // Function component const UserInfo = ()=> {// Get global store with custom useStore const {roleName} = useStore('userStore') return (<Row Justify ="space between"> <Col></Col> <Col span={5} className='border'> < space align='center'> <span> <h2>{roleName}</h2> </Space> </Col> </Row> ) } export default observer(UserInfo)Copy the code

If you only wanted MobX to be responsible for global state management in your daily projects, this would be enough. I’ll introduce MobX+ Hook’s powerful capabilities for local state management. Global state management: Stores are defined outside of components, often in a separate Store folder globally. Suitable for managing states that are public or public relative to a module. Local state management: Store is usually defined within components and is suitable for complex component design scenarios. It is used to solve the problems of state transfer in multi-layer nested components, multiple component states and complex update.

2.3.2 Creating a local Store

Let’s start with two hooks

useLocalObservable

Function: Declare a Store in a component by hook, return a reactive version of the normal object passed in, and keep a unique reference to the reactive object in every subsequent rendering of the function component (consistent with useState). (useLocalStore is the predecessor of this API, but will soon be deprecated, No introduction here).

useObserver

What it does: The observer is HOC and can only be used externally by wrapping the entire component. In order to implement local state management inside a component, the class component must use renderProps as the built-in Observer component. However, in functional mode, hook must be the preferred solution. Therefore, useObserver is the hook version of Observer. Example: useLocalObservable + useObserver achieve a state of local management SRC/demos/UserInfoScopeStore JSX

import { useLocalObservable, useObserver } from "mobx-react-lite"; import { Row, Col, Space,Button } from "antd"; Const Store = useLocalObservable(()=>({name:' XXX ', ChangeName (text){this.name = text}})) // Compare the following two update modes of local status view in components. // useObserver return useObserver(()=> <Row justify="space-between"> <Col></Col> <Col span={5} className='border'> < Space the align = "center" > < span > the current user: < / span > < h2 > {store. The name} < / h2 > < Button onClick = {() = > store. ChangeName (' millet ')} > change < / Button >  </Space> </Col> </Row>) // or Observer return <Observer> {() => <Row justify="space-between"> <Col></Col> <Col span={5} </span> <h2>{store.name}</h2> <Button onClick={() => Store. ChangeName (' mi ')}> modify </Button> </Space> </Col> </Row>} </Observer>} export default UserInfoCopy the code

Summary: (1) Observer HOC is suitable for the overall update scenario of components. (2) useObserver or Observer can be used to deal with local update scenarios within components. The former is hook mode, which only supports functional components, while the latter uses renderProps. Class and function components are compatible.

3. Developer tools

Chrome plug-ins

Third, Q&A

  1. Can IE project be used?

V4 version is available by default, V5 and later if you need to support Proxy IE/React Native, please use the initialization to change the global configuration

Import {configure} from "mobx" // Disable proxy configure({useProxies: "never"}) by globally configuring the proxies to be compatible with IE/RN.Copy the code
  1. Why does the new V6 version of MobX not recommend a class decorator syntax, but a makeObservable approach to the Store?

Reasons for not recommending decorators: Decorator syntax has not been finalized, the time frame for inclusion in the ES standard is far off, and future standards may differ from current decorator implementations. For compatibility reasons, MobX 6 doesn’t recommend decorators and instead recommends makeObservable/makeAutoObservable. However, if TS is used in the project, I think we can basically ignore the impact, after all, decorators are simpler to use.

  1. Why is my component not updated as Store data is updated?

(1) Forget observer, useObserver package (mostly for this reason). (2)defineProperty’s reactive scheme has some restrictions on arrays and objects that need special attention. If necessary, mobx’s set method should be used to solve the problem. (3) The Observer works fine as long as you always pass a reference to a responsive object. If you just pass property values, you end up with a lost response, which is common in ES6 deconstructed scenarios, or just passing properties of a responsive object. If you’re familiar with VUe3, toRefs is designed to solve a similar problem, but you can avoid this in Mobx by following the example below. ❌ const TimerView = observer(({secondsPassed}) => Seconds passed: {secondsPassed})

React.render(<TimerViewer secondPassed={myTimer.secondsPassed} />, Document.body) // correct 🙆 const TimerView = observer(({myTimer}) => <span>Seconds passed: {myTimer.secondsPassed}</span>) React.render(<TimerViewer secondPassed={myTimer} />, document.body) ```Copy the code
  1. Do I have to update the Store through action?

Not in principle, but in principle. It is also possible to trigger reactive updates by changing the Store directly, which is a direct mutable way, but Mobx strongly discourages you from doing so because you lose the following benefits: (1) Actions can clearly express the intent of a function to modify state, which is beneficial to project maintenance. (2) Actions, combined with developer tools, provide very useful debugging information when strict mode is enabled, modifying store state requires enforceActions. See enforceActions for global configuration. MobX does not restrict state updates in principle like Redux does, but only by convention. Therefore, it is highly recommended to enable this option.

  1. Are there performance issues with frequent use of the Observer?

The component automatically rerenders when its associated Observable changes, whereas it ensures that the component does not rerender when no changes are made. True on-demand rendering of components, in practice, allows MobX applications to be optimized right out of the box, and they generally don’t need any extra code to prevent over-rendering.

  1. What is MobX’s biggest advantage over Redux?

Specifically: MobX’s out-of-the-box simplicity, flexibility, and minimal intrusion into existing projects are all advantages over Redux. Abstract: Compared with Redux, MobX is naturally friendly to the entity model. It cleverly converts data into Observable with interception proxy inside, so that you can still perceive the entity model in the use level, but MobX has a responsive ability. This is the most powerful part of MobX, which is suitable for the abstract domain model!

At the end

All of these examples can be found in the Github repository.

The END THANKS ~