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

Before explaining Rematch Core, let’s review the code structure 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

Based on the above structure, I split rematch into the following components:

Recall the rematch reference statement import {init} from ‘@rematch/core’, where core represents the core source of rematch, which I have grouped into two files:

  1. /src/rematch.tsContains the Rematch class, which contains rematch initialization methods, model building methods, and so on
  2. /src/redux.tsContains some transformations from rematch to redux code logic, mainly generated by reducer

So let’s talk about these two parts separately

rematch class

Global init method

Before we get to the rematch class, let’s look at the rematch export method init, which is the only place to instantiate the rematch class:

export interface InitConfig<M extendsobject = Models> { name? :stringmodels? : M plugins? : Plugin[] redux? : InitConfigRedux }// incrementer used to provide a store name if none exists
let count = 0

/**
 * init
 *
 * generates a Rematch store
 * with a set configuration
 * @param config* /
export const init = (initConfig: R.InitConfig = {}): R.RematchStore => {
	const name = initConfig.name || count.toString()
	count += 1
	constconfig: R.Config = mergeConfig({ ... initConfig, name })return new Rematch(config).init()
}
Copy the code

The init() function takes a configuration object as an argument and returns a created Rematch store. Inside the function, since initConfig supports attribute defaults, the mergeConfig() function is first called to fill in the default parameters, while the plugins attribute represents some of the exported plug-in configurations (more on rematch plug-ins later). The plug-in configuration can also have these initial configuration items, so merge them:

/ SRC/utils/mergeConfig. Ts:

/** * mergeConfig * * merge init configs together */
export default (initConfig: R.InitConfig & { name: string }): R.Config => {
	const config: R.Config = {
		name: initConfig.name,
		models: {},
		plugins: [],
		...initConfig,
		redux: {
			reducers: {},
			rootReducers: {},
			enhancers: [].middlewares: [],
			...initConfig.redux,
			devtoolOptions: {
				name: initConfig.name, ... (initConfig.redux && initConfig.redux.devtoolOptions ? initConfig.redux.devtoolOptions : {}), }, }, }// ...

	// defaults
	for (const plugin of config.plugins) {
		if (plugin.config) {
			// models
			const models: R.Models = merge(config.models, plugin.config.models)
			config.models = models

			// plugins
			config.plugins = [...config.plugins, ...(plugin.config.plugins || [])]

			// redux
			if (plugin.config.redux) {
				config.redux.initialState = merge(
					config.redux.initialState,
					plugin.config.redux.initialState
				)
				config.redux.reducers = merge(
					config.redux.reducers,
					plugin.config.redux.reducers
				)
				// ...}}}return config
}
Copy the code

Rematch class constructor

The init() method finally instantiates rematch, starting with its constructor:

/** * Rematch class * * an instance of Rematch generated by "init" */
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 constructors, we bind config, pluginFactory, and plugins to the Rematch instance, but we’ll skip the plugin section for now.

A brief description of the forEachPlugin() method is used to iterate over all plugins and, if the plugin has a corresponding hook (matched by the first method argument), call the second argument (a callback function) and pass that hook back as an argument. For example, the constructor will eventually collect the Middleware hooks from all the plug-ins and merge them into the redux.Middleware configuration.

Rematch instance init method

When the rematch class is instantiated, its init() method is called:

export default class Rematch {
    // ...
	protected config: R.Config
	protected models: R.Model[]

    // ...

    public getModels(models: R.Models): R.Model[] {
		return Object.keys(models).map((name: string) = >({ name, ... models[name],reducers: models[name].reducers || {},
		}))
	}

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

		// 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)
		}
		// create a redux store with initialState
		// merge in additional extra reducers
		const redux = createRedux.call(this, {
			redux: this.config.redux,
			models: this.models,
		})

		const rematchStore = {
			name: this.config.name, ... redux.store,// dynamic loading of models with `replaceReducer`
			model: (model: R.Model) = > {
				this.addModel(model)
				redux.mergeReducers(redux.createModelReducer(model))
				redux.store.replaceReducer(
					redux.createRootReducer(this.config.redux.rootReducers)
				)
				redux.store.dispatch({ type: '@@redux/REPLACE '}})},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

In init(), models are first initialized and bound, and onModel is called if plugin registers a hook for it. Then, generate a native Redux store and wrap a Rematch Store on top of that. Finally, if plugin registers an onStoreCreated hook, it is called, and when the hook returns an object, the properties on the object are incorporated into the Rematch Store.

The above procedure focuses on how to use the createRedux() function to generate a native Redux store, the second part of rematch Core, about Redux.

redux store

The Redux part is mainly about transforming the Models configured by Rematch into redux Reducers, and finally generating a Redux Store.

The file is located at/SRC /redux.ts and contains only one default export function:

export default function({ redux, models, }: { redux: R.ConfigRedux models: R.Model[] }) {
	const combineReducers = redux.combineReducers || Redux.combineReducers
	const createStore: Redux.StoreCreator = redux.createStore || Redux.createStore
	const initialState: any =
		typeofredux.initialState ! = ='undefined' ? redux.initialState : {}

	this.reducers = redux.reducers

	// combine models to generate reducers
	this.mergeReducers = (nextReducers: R.ModelReducers = {}) = > {
		// merge new reducers with existing reducers
		this.reducers = { ... this.reducers, ... nextReducers }if (!Object.keys(this.reducers).length) {
			// no reducers, just return state
			return (state: any) = > state
		}
		return combineReducers(this.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)
	}
	// initialize model reducers
	for (const model of models) {
		this.createModelReducer(model)
	}

	this.createRootReducer = (
		rootReducers: R.RootReducers = {}
	): Redux.Reducer<any, R.Action> => {
		const mergedReducers: Redux.Reducer<any> = this.mergeReducers()
		if (Object.keys(rootReducers).length) {
			return (state, action) = > {
				const rootReducerAction = rootReducers[action.type]
				if (rootReducers[action.type]) {
					return mergedReducers(rootReducerAction(state, action), action)
				}
				return mergedReducers(state, action)
			}
		}
		return mergedReducers
	}

	const rootReducer = this.createRootReducer(redux.rootReducers)

	constmiddlewares = Redux.applyMiddleware(... redux.middlewares)constenhancers = composeEnhancersWithDevtools(redux.devtoolOptions)( ... redux.enhancers, middlewares )this.store = createStore(rootReducer, initialState, enhancers)

	return this
}
Copy the code

It might look a little messy, but let me break it down and explain it step by step.

Create a Model reducers

The first is to create a unified Reducer for the individual models. As there are multiple reducers in each Model, this step is to create a unified reducer and then distribute it to the corresponding single reducer.

/** * isListener * * determines if an action is a listener on another model */
const isListener = (reducer: string) :boolean= > reducer.indexOf('/') > -1

export default function({ redux, models, }: { redux: R.ConfigRedux models: R.Model[] }) {
	const combineReducers = redux.combineReducers || Redux.combineReducers
	const createStore: Redux.StoreCreator = redux.createStore || Redux.createStore
	const initialState: any =
		typeofredux.initialState ! = ='undefined' ? redux.initialState : {}

	this.reducers = redux.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)
	}

	// initialize model reducers
	for (const model of models) {
		this.createModelReducer(model)
	}

    // ...
}
Copy the code

Start by filling in some default values, including createStore, combineReducers methods, initial initialState and Reducers. We then iterate over each model and generate a Reducer at the model level, filling this. Reducers object with model.name as the key.

Here are two main points to note:

  • If the reducer method name contains /, rematch considers it to be a reducer to listen to (for example, there are two models, modelA and modelB, and there is a Reducer called foo in modelA, A listen on Reducer modelA/foo can be defined in modelB, so that when the assigned actions are modelA/foo, the two reducers will be executed), and the corresponding action type is the reducer name. Otherwise, ${model.name}/${modelReducerName}.

  • A baseReducer can be defined for each model. If there is a baseReducer, each model goes through the corresponding baseReducer before arriving at the corresponding reducer.

After creating the Reducer for each Model, create the rootReducer.

Create a Root reducer

Redux store can be created only after the Root reducer has been created.

export default function({ redux, models, }: { redux: R.ConfigRedux models: R.Model[] }) {
    // ...

	const combineReducers = redux.combineReducers || Redux.combineReducers
	this.reducers = redux.reducers

    // ...

    // combine models to generate reducers
	this.mergeReducers = (nextReducers: R.ModelReducers = {}) = > {
		// merge new reducers with existing reducers
		this.reducers = { ... this.reducers, ... nextReducers }if (!Object.keys(this.reducers).length) {
			// no reducers, just return state
			return (state: any) = > state
		}
		return combineReducers(this.reducers)
	}

	this.createRootReducer = (
		rootReducers: R.RootReducers = {}
	): Redux.Reducer<any, R.Action> => {
		const mergedReducers: Redux.Reducer<any> = this.mergeReducers()
		if (Object.keys(rootReducers).length) {
			return (state, action) = > {
				const rootReducerAction = rootReducers[action.type]
				if (rootReducers[action.type]) {
					return mergedReducers(rootReducerAction(state, action), action)
				}
				return mergedReducers(state, action)
			}
		}
		return mergedReducers
	}

	const rootReducer = this.createRootReducer(redux.rootReducers)

    // ...
}
Copy the code

Take a look at the this.createrootreducer () function, which first calls this.mergereducers () to merge the reducers. After the above steps, the reducers corresponding to each model have been summarized into a reducerMapObject (this.reducers), Call combineReducers in this.mergereducers () to merge it into the final rootReducer.

It is important to note that, in the same way that you can configure a baseReducer per Model, rematch also allows you to configure a redux.rootReducers globally. Check whether there is a matched reducer in redux.rootReducers. If there is, it is processed first, and then the final rootReducer is distributed to a specific reducer.

Create a redux store

Finally, create and return a redux store:

const composeEnhancersWithDevtools = (
	devtoolOptions: R.DevtoolOptions = {}
): any= > {
	const{ disabled, ... options } = devtoolOptions/* istanbul ignore next */
	return! disabled &&typeof window= = ='object' &&
		window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
		? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(options)
		: Redux.compose
}

export default function({ redux, models, }: { redux: R.ConfigRedux models: R.Model[] }) {
    // ...

	const rootReducer = this.createRootReducer(redux.rootReducers)

	constmiddlewares = Redux.applyMiddleware(... redux.middlewares)constenhancers = composeEnhancersWithDevtools(redux.devtoolOptions)( ... redux.enhancers, middlewares )this.store = createStore(rootReducer, initialState, enhancers)

	return this
}
Copy the code

This part is similar to Redux, except that Rematch is completely encapsulated, so I won’t go over it.

rematch store

Finally, take a look at the rematch Store creation and review the previous code:

export default class Rematch {
    // ...

    public init() {
		// ...

		// create a redux store with initialState
		// merge in additional extra reducers
		const redux = createRedux.call(this, {
			redux: this.config.redux,
			models: this.models,
		})

		const rematchStore = {
			name: this.config.name, ... redux.store,// dynamic loading of models with `replaceReducer`
			model: (model: R.Model) = > {
				this.addModel(model)
				redux.mergeReducers(redux.createModelReducer(model))
				redux.store.replaceReducer(
					redux.createRootReducer(this.config.redux.rootReducers)
				)
				redux.store.dispatch({ type: '@@redux/REPLACE '}})},// ...

		return rematchStore
	}
}
Copy the code

As you can see, the Rematch Store adds a name attribute to the Redux store and a Model method, which I suspect is used to distinguish between multiple Rematch stores and dynamically add models. Let’s look at how to dynamically add models:

  1. First associate the Model with the Rematch instance
  2. Then create a Reducer of the Model
  3. Finally, create a new rootReducer and replace it with replaceReducer() of Redux. A special action identifier is issued.

The call to redux.mergereducers (redux.createmodelreducer (model)) is redundant and has no effect because its return value is not being used. And the story below. CreateRootReducer (this. Config. Redux. RootReducers) method, also called again redux. MergeReducers, this time really to take effect.

In rematch V1, because of the rematch instance, many variable objects are directly associated with the instancethisAll updates are directly mutate, so there are a lot of redundant or “weird” things like this. In V2, rematch instances are replaced with functional programming, but not completely removedthisSome variable bindings, so there are some problems in the code that I’ll address separately when I write v1 upgrade v2.

This is the end of the rematch core introduction. Next, I will explain the plugin mechanism of rematch, divided into two parts. The first part introduces pluginFactory and two core plugins of Rematch. Part 2 introduces several third-party plugins developed by the Rematch team to take a closer look at this mechanism. Finally, I will write two articles, one of which is about the changes of the rematch V1 to V2 upgrade, the other introduces the Rematch type system (which is the biggest change brought by the upgrade to V2) and some remaining problems and difficulties of this type system, so as to discuss with you.

Stay tuned!