Unless otherwise noted, the code version for this column is @rematch/core: 1.4.0

The last part introduced the code for the Rematch core and ignored the plugin part. This part of rematch is also the highlight, and I’ll cover it in more detail in this article.

In addition, I’ll introduce rematch’s two core plugins, which are called core plugins because they must be used for Rematch to function fully. In the next article, I’ll introduce a few third-party plugins that developers can choose to use or not use.

Before I explain, let’s review the code structure and components of Rematch:

. The plugins | -... | - loading | - immer | - select the SRC | - plugins | | -- dispatch. Ts | | -- effects. Ts | - typings | | - index. The ts | - utils . | | - deprecate ts | | -- isListener. Ts | | -- mergeConfig. Ts | | - validate the ts | -- index. Ts | -- pluginFactory. Ts | - Redux. Ts | - rematch. TsCopy the code

Plugin Factory

The first is the Plugin factory function, taking the arguments initialized by the Rematch Store and returning the factory object with the key method property of create:

export default (config: R.Config) => ({
  // ...
  create(plugin: R.Plugin): R.Plugin {
    // ... do some validations

    if (plugin.onInit) {
      plugin.onInit.call(this);
    }

    const result: R.Plugin | any = {};

    if (plugin.exposed) {
      for (const key of Object.keys(plugin.exposed)) {
        this[key] =
          typeof plugin.exposed[key] === "function"
            ? plugin.exposed[key].bind(this) // bind functions to plugin class
            : Object.create(plugin.exposed[key]); // add exposed to plugin class}}for (const method of ["onModel"."middleware"."onStoreCreated"]) {
      if (plugin[method]) {
        result[method] = plugin[method].bind(this); }}returnresult; }});Copy the code

This binds the execution context of some of the plugin’s function properties to the pluginFactory. If the plugin contains exposed attributes, they are added to the pluginFactory for sharing of the plugins. In the upgrade of Rematch V2 source code, misunderstanding the role of the Exposed attribute has led to some ambiguous behavior, which I’ll cover in a later article. Finally, return an object that contains the plugin hook.

PluginFactory is called in the Rematch class mentioned in the previous article:

export default class Rematch {
  protected config: R.Config;
  protected models: R.Model[];
  private plugins: R.Plugin[] = [];
  private pluginFactory: R.PluginFactory;

  constructor(config: R.Config) {
    this.config = config;
    this.pluginFactory = pluginFactory(config);
    for (const plugin of corePlugins.concat(this.config.plugins)) {
      this.plugins.push(this.pluginFactory.create(plugin));
    }
    // preStore: middleware, model hooks
    this.forEachPlugin("middleware".(middleware) = > {
      this.config.redux.middlewares.push(middleware);
    });
  }
  public forEachPlugin(method: string, fn: (content: any) = >void) {
    for (const plugin of this.plugins) {
      if(plugin[method]) { fn(plugin[method]); }}}// ...
}
Copy the code

In the constructor, create the pluginFactory as a private property of the Rematch class, and then in turn put the created plugins into an array. Finally, configure the Plugin Middleware hook to redux middleware.

In addition to the Middleware hooks, execute the onModel and onStoreCreated hooks in turn:

export default class Rematch {
  // ...

  public addModel(model: R.Model) {
    // ...

    // run plugin model subscriptions
    this.forEachPlugin("onModel".(onModel) = > onModel(model));
  }

  public init() {
    // collect all models
    this.models = this.getModels(this.config.models);
    for (const model of this.models) {
      this.addModel(model);
    }

    // ...

    this.forEachPlugin("onStoreCreated".(onStoreCreated) = > {
      const returned = onStoreCreated(rematchStore);
      // if onStoreCreated returns an object value
      // merge its returned value onto the store
      if (returned) {
        Object.keys(returned || {}).forEach((key) = >{ rematchStore[key] = returned[key]; }); }});return rematchStore;
  }
  // ...
}
Copy the code

OnModel is executed when the model is traversed and added, and onStoreCreated is executed before the store is created and returned. The former is typically used to read, add, or modify the configuration of the Model, while the latter is used to add new properties to the Store, and if there is a return value and the returned value is an object, the properties on it are added to the Store. Let’s take a look at two specific plugins and how these hooks work.

Core Plugins

Rematch V1 is designed with two core plugins that must be referenced in the constructor of the Rematch class. You can see the following code snippet:

export default class Rematch {
  // ...

  constructor(config: R.Config) {
    // ...

    for (const plugin of corePlugins.concat(this.config.plugins)) {
      this.plugins.push(this.pluginFactory.create(plugin));
    }

    // ...
  }

  // ...
}
Copy the code

The two core plugins are Dispatch and Effects. Dispatch the plugin is used to enhance the dispatch of redux store, make its support chain calls, such as dispatch. The modelName. ReducerName, this is one of the characteristics of rematch. And effects the plugin is used to support asynchronous operations such as side effects, and realize through the dispatch. The modelName. EffectName calls.

Dispatch plugin

Let’s take a look at the entire dispatch code, and then I’ll break it down into two parts:

const dispatchPlugin: R.Plugin = {
  exposed: {
    // required as a placeholder for store.dispatch
    storeDispatch(action: R.Action, state: any) {
      console.warn("Warning: store not yet loaded");
    },

    storeGetState() {
      console.warn("Warning: store not yet loaded");
    },

    /**
     * dispatch
     *
     * both a function (dispatch) and an object (dispatch[modelName][actionName])
     * @param action R.Action
     */
    dispatch(action: R.Action) {
      return this.storeDispatch(action);
    },

    /**
     * createDispatcher
     *
     * genereates an action creator for a given model & reducer
     * @param modelName string
     * @param reducerName string
     */
    createDispatcher(modelName: string, reducerName: string) {
      return async(payload? :any, meta? :any) :Promise<any> = > {const action: R.Action = { type: `${modelName}/${reducerName}` };
        if (typeofpayload ! = ="undefined") {
          action.payload = payload;
        }
        if (typeofmeta ! = ="undefined") {
          action.meta = meta;
        }
        return this.dispatch(action); }; }},// access store.dispatch after store is created
  onStoreCreated(store: any) {
    this.storeDispatch = store.dispatch;
    this.storeGetState = store.getState;
    return { dispatch: this.dispatch };
  },

  // generate action creators for all model.reducers
  onModel(model: R.Model) {
    this.dispatch[model.name] = {};
    if(! model.reducers) {return;
    }
    for (const reducerName of Object.keys(model.reducers)) {
      this.validate([
        [
          !!reducerName.match(+ / / /. / / /),
          `Invalid reducer name (${model.name}/${reducerName}) `,], [typeofmodel.reducers[reducerName] ! = ="function".`Invalid reducer (${model.name}/${reducerName}). Must be a function`,]]);this.dispatch[model.name][reducerName] = this.createDispatcher.apply(
        this, [model.name, reducerName] ); }}};Copy the code

onModel hook

As mentioned earlier, onModel is executed before onStoreCreated, so let’s take a look at onModel:

const dispatchPlugin: R.Plugin = {
  exposed: {
    // ...

    storeDispatch(action: R.Action, state: any) {
      console.warn("Warning: store not yet loaded");
    },
    dispatch(action: R.Action) {
      return this.storeDispatch(action);
    },
    createDispatcher(modelName: string, reducerName: string) {
      return async(payload? :any, meta? :any) :Promise<any> = > {const action: R.Action = { type: `${modelName}/${reducerName}` };
        if (typeofpayload ! = ="undefined") {
          action.payload = payload;
        }
        if (typeofmeta ! = ="undefined") {
          action.meta = meta;
        }
        return this.dispatch(action); }; }},// ...

  // generate action creators for all model.reducers
  onModel(model: R.Model) {
    this.dispatch[model.name] = {};
    if(! model.reducers) {return;
    }
    for (const reducerName of Object.keys(model.reducers)) {
      // ... some validations

      this.dispatch[model.name][reducerName] = this.createDispatcher.apply(
        this, [model.name, reducerName] ); }}};Copy the code

In the onModel hook, the properties in this.Dispatch (mentioned above) are added to the pluginFacotry for each model, and this in the context of the hook function is bound to the pluginFactory. So this.dispatch creates an empty object with the attribute name model, and then traverses the Reducer, using the reducer name as the attribute name for each reducer. Add an actionCreator to the previously empty object. This enables cascading calls to reducer.

The createDispatcher function is used to generate actionCreator, which uses ${model name}/${reducer name} as the action type. Called with this.dispatch (which is also a function).

After the action is dispatched, the reducer is executed correctly. The reducer constructor code is described in the previous section on creating Model reducers.

this.createModelReducer = (model: R.Model) = > {
  const modelBaseReducer = model.baseReducer;
  const modelReducers = {};
  for (const modelReducer of Object.keys(model.reducers || {})) {
    const action = isListener(modelReducer)
      ? modelReducer
      : `${model.name}/${modelReducer}`;
    modelReducers[action] = model.reducers[modelReducer];
  }
  const combinedReducer = (state: any = model.state, action: R.Action) = > {
    // handle effects
    if (typeof modelReducers[action.type] === "function") {
      return modelReducers[action.type](state, action.payload, action.meta);
    }
    return state;
  };

  this.reducers[model.name] = ! modelBaseReducer ? combinedReducer :(state: any, action: R.Action) = >
        combinedReducer(modelBaseReducer(state, action), action);
};
Copy the code

As you can see, in combinedReducer, action is assigned the correct reducer using action.type, which is also a combination of ${model name}/${reducer name}.

onStoreCreated hook

Take a look at the final onStoreCreated hook:

const dispatchPlugin: R.Plugin = {
  exposed: {
    // required as a placeholder for store.dispatch
    storeDispatch(action: R.Action, state: any) {
      console.warn("Warning: store not yet loaded");
    },

    storeGetState() {
      console.warn("Warning: store not yet loaded");
    },

    /**
     * dispatch
     *
     * both a function (dispatch) and an object (dispatch[modelName][actionName])
     * @param action R.Action
     */
    dispatch(action: R.Action) {
      return this.storeDispatch(action); }},// access store.dispatch after store is created
  onStoreCreated(store: any) {
    this.storeDispatch = store.dispatch;
    this.storeGetState = store.getState;
    return { dispatch: this.dispatch }; }};Copy the code

Since the ReMatch Store is an enhancement to the Redux Store, it relies on the Redux Store. Therefore, storeDispatch, storeGetState, and Dispatch cannot be accessed until the Redux Store is created. Once created, the storeDispatch and storeGetState need to be overwritten first, and then an enhanced dispatch needs to be returned that overrides the original dispatch in the Redux Store.

Effect plugin

The Effect Plugin is used to support side effects:

const effectsPlugin: R.Plugin = {
  exposed: {
    // expose effects for access from dispatch plugin
    effects: {},},// add effects to dispatch so that dispatch[modelName][effectName] calls an effect
  onModel(model: R.Model): void {
    if(! model.effects) {return;
    }

    const effects =
      typeof model.effects === "function"
        ? model.effects(this.dispatch)
        : model.effects;

    for (const effectName of Object.keys(effects)) {
      // ... some validations

      this.effects[`${model.name}/${effectName}`] = effects[effectName].bind(
        this.dispatch[model.name]
      );
      // add effect to dispatch
      // is assuming dispatch is available already... that the dispatch plugin is in there
      this.dispatch[model.name][effectName] = this.createDispatcher.apply(
        this,
        [model.name, effectName]
      );
      // tag effects so they can be differentiated from normal actions
      this.dispatch[model.name][effectName].isEffect = true; }},// process async/await actions
  middleware(store) {
    return (next) = > async (action: R.Action) => {
      // async/await acts as promise middleware
      if (action.type in this.effects) {
        await next(action);
        return this.effects[action.type](
          action.payload,
          store.getState(),
          action.meta
        );
      }
      returnnext(action); }; }};Copy the code

The onModel hook does much the same thing as the Dispatch Plugin does with reducer, but there are a few differences:

  1. The effect parameter in the Model configuration supports function form. When called, the parameter is passed in enhanced formdispatchFunction object, and the return value is the actual Effects object. So you can pass it in effectdispatchCalls all the effect and reducer of the model.
  2. Model for the context of a single effect functionthisBound tothis.dispatch[model.name]Which is the current modeldispatch. Therefore, it can be used internallythisTo call reducer and effect in the current model.
  3. Added an identifier for effectisEffecttrueIs used to distinguish the reducer from the regular reducer.

Finally, there is the core part of the Effect Plugin, which implements itself as a Redux asynchronous middleware. As mentioned earlier, an Effect Action can also be dispatched via rematchStore.dispatch. After passing through the asynchronous middleware, it will first determine if it is in this. It then executes the effect, passing in the parameters payload, global state, and meta (in this order since meta is rarely used, but payload is the most common). If not, proceed directly.

Note: Processing down means being processed by the middleware that follows (if any), and finally goes to the Reducer. This can be confusing because if the model and effect share the same name, the reducer will be executed earlier than the effect. Both are executed, which raises the challenge of the TS design part of Rematch V2 (which I’ll explain in more detail later in this article).

conclusion

After detailing Rematch’s plugin mechanism and the two plug-ins at its core, you should be happy with Rematch’s clever design. In the next article, I’ll continue to look at a few third-party plug-ins that we can choose to use to improve development efficiency. Stay tuned!