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:
- The effect parameter in the Model configuration supports function form. When called, the parameter is passed in enhanced form
dispatch
Function object, and the return value is the actual Effects object. So you can pass it in effectdispatch
Calls all the effect and reducer of the model. - Model for the context of a single effect function
this
Bound tothis.dispatch[model.name]
Which is the current modeldispatch
. Therefore, it can be used internallythis
To call reducer and effect in the current model. - Added an identifier for effect
isEffect
为true
Is 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!