The cause of

In the use of off-the-shelf small program state management scheme to step on a small hole, then consider whether to introduce a relatively mature state management scheme in the front-end ecosystem of small program. Mobx was chosen for ease of use and extensibility.

MobX is a simple, extensible state management solution. For more information about MobX, go to MobX

If you just want to use Mobx in applets, you can refer directly to the documentation.

process

In addition to completing the basic binding function between MOBx and applets, this design and implementation also hope to follow several points (pain points) :

  • Simple, flexible, complete API
  • Functional implementation and call
  • Complete type support

📚 API design

Mobx originated in the React ecosystem, but due to the differences between React and applets, it is not feasible to copy mobx-React calls. It is hoped that the react call style will be preserved as much as possible, while taking into account the characteristics of the applets themselves. The final API format is as follows.

// App.ts

const App = provider<Stores>(stores)

App<AppOptions>({
  ...
})

  
// index.ts
inject<Stores>('storeA'.'storeB') (({ storeA, storeB }) = >
  observer.page<Data, Custom>({
    onLoad() {
      this.data.storeA.count
      storeA.add()
    },
  })
)

Copy the code

This exposes three main functions: Provider, Observer, and Inject. Provider is used to globally bind stores and Observers to create pages or components, and inject stores to be listened to into the Observer. If you have any questions about this call, read the following.

🎨 Internal implementation

The overall implementation principle is simple. The autorun function provided by Mobx triggers a callback when the observable property in the store referenced by Autorun changes. In this case, autorun can actively trigger the status update of the applet in its callback.

Since the applet itself does not expose render related interfaces, and WXML can only reference data from data, the only option is to map the data being listened to to the data of the page or component, and actively call setData to trigger a status update when Autorun fires.

All you need to do is pass the initial store data along with data into the declaration function when the page (or component) is declared, call Autorun when the page is mounted to listen for data changes and trigger updates, and finally destroy Autorun on unmount to release the listener.

The implementation details are as follows.

provider.ts

Provider is used to receive a Stores object and bind it to App. A Store object is a collection of stores referenced in the project in the format {storeA, storeB}. Meanwhile, App() calls can be made inside the provider. The implementation is very simple, add stores as the parameter attribute of App.

type AppOptions<T> = WechatMiniprogram.App.Options<T>

constprovider = <TStores extends AnyObject>(stores: TStores) => <TAppOptions extends AnyObject>( options: AppOptions<TAppOptions> ) => App({ ... options, stores }) export default providerCopy the code

observer.ts

The Observer is the core of implementing listener functions. There are two declared functions in the applet, Page() and Component(), corresponding to the Page and Component. Take Page() as an example.

The observer is responsible for receiving stores objects that need to be listened on and mapping them to page data. At the same time, observe the changes of properties in stores. When the observed properties change, call setData() to update the state of the page and trigger the view update.

With the Autorun function provided by Mobx, we can perform setData operations when the Autorun callback is triggered.

So the idea is roughly as follows: first, merge stores and data into the Page as options, call Autorun when the Page onLoad, and call setData immediately when the callback function is triggered to update the status to the Page. Autorun also needs to be destroyed at page onUnload.

An Observer call returns an observe function to receive the observedStores object that needs to be injected and listened on the current page. ObservedStores generally need to be calculated and passed in the storeNames specified by the outside inside the Inject, but the reference of the Store object can also be directly passed in the outside. ObservedStores is passed in Observe and the call will make the page declaration.

The final call format is observer.page(options)({storeA, storeB})

The implementation is as follows.

import { autorun, IReactionDisposer } from 'mobx'
import { is, toData } from './utils'
import diff from './diff'

const observer = {
  page: <TData extends DataOption, TCustom extends CustomOption>( options: PageOptions<TData, TCustom> ) => { let dispose: IReactionDisposer const { data = {}, onLoad, onUnload } = options return (observedStores: AnyObject = {}) => Page({ ... options, data: { ... data, ... toData(observedStores) }, onLoad(query) { dispose = autorun(() => { if (this.data) { const diffs: AnyObject = diff({ ... this.data, ... toData(observedStores) }, this.data) this.setData(diffs) } }) if (is.fun(onLoad)) onLoad.call(this, query) }, onUnload() { if (dispose) dispose() if (is.fun(onUnload)) onUnload.call(this) }, }) } } export default observerCopy the code

Note: It is obviously not advisable to update all properties in stores to data every time update is triggered. Here, DIff is used to compare two states and perform minimal state update to optimize performance. The diff function refers to the implementation in WeStore, and the result after diff can be passed directly as the parameter of setData.

The function of toData is to deeply copy the Observable object in Mobx into a JS object in line with the data format of the small program. For specific implementation, please refer to the utils.ts file in the source code.

Inject.ts

Finally, the realization idea of Inject is determined. Inject is responsible for two jobs. First, it receives the storeNames passed in when it is called, that is, the list of stores to be listened on, calculates the corresponding observedStores object, and passes it to the Observer for listening. Second, direct references to provider-bound stores are passed to the outside world for use through calls to their return functions.

Inject (‘storeA’, ‘storeB’) takes two storeName and returns a function that takes createObserver as an argument. The createObserver argument is the Stores object, where the stores reference is obtained. The return value of createObserver is the return value from the Observer call. Inject the function returned by the Observer will be called internally and observedStores will be passed in as a parameter for listening.

The final call format is Inject (‘storeA’, ‘storeB’)(({storeA, storeB}) => observer.page(options)

The implementation is as follows.

constmapStores = <TStores extends AnyObject>(names: (keyof TStores)[]) => (source: TStores) => { const target: TStores = {} as TStores names.forEach(key => { if (source && source[key]) { target[key] = source[key] } }) return target  } const inject = <TStores extends AnyObject>(... storeNames: (keyof TStores)[]) => ( createObserver: (stores: TStores) => (observedStores: AnyObject) => void | string ) => { const stores = getApp().stores ?? {} const observedStores = mapStores(storeNames)(stores) return createObserver(stores)(observedStores) } export default injectCopy the code

Here we get a reference to stores via getApp() and pass it as a parameter to createObserver. StoreNames and stores are calculated by mapStores to obtain the monitoring target object observedStores. The observe function generated by the Observer is passed and called to complete state injection and page declaration.

The end of the

Thank you for reading.

Project source code & documentation