# Rematch Code Deep Dive: Part 2 - The Rematch Core


This is the second part of the Rematch Code Deep Dive Series, focusing on the core code of Rematch.

<!--more-->

> Unless specified otherwise, the code version in this column is `@rematch/core: 1.4.0`

Before explaining Rematch core, let's review the code structure of Rematch:

```
...
plugins
|—— ...
|—— loading
|—— immer
|—— select
src
|—— plugins
|  |—— dispatch.ts
|  |—— effects.ts
|—— typings
|  |—— index.ts
|—— utils
|  |—— deprecate.ts
|  |—— isListener.ts
|  |—— mergeConfig.ts
|  |—— validate.ts
|—— index.ts
|—— pluginFactory.ts
|—— redux.ts
|—— rematch.ts
```

Based on the structure above, I've divided Rematch into the following components:

![rematch code structure](./rematch-code-structure.png)

Recalling the Rematch import statement `import { init } from '@rematch/core'`, the `core` represents the core source code of Rematch, which I categorize into two files:

1. `/src/rematch.ts` contains the Rematch class, including the initialization method of Rematch and methods for model construction, etc.
2. `/src/redux.ts` involves the transformation of code logic from Rematch to Redux, mainly the generation of reducers.

Next, let's discuss these two parts.

## Rematch Class

### The global `init` method

Before understanding the Rematch class, let's look at Rematch's export method `init`, which is the only place to instantiate the Rematch class:

```ts
export interface InitConfig<M extends object = Models> {
  name?: string;
  models?: 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;
  const config: R.Config = mergeConfig({ ...initConfig, name });
  return new Rematch(config).init();
};
```

The `init()` function accepts a configuration object as a parameter and returns a created Rematch store. Inside the function, since `initConfig` supports property defaults, the `mergeConfig()` function is first called to fill in the missing parameters. Also, the `plugins` attribute represents some exported plugin configurations (which will be discussed later in Rematch plugins). These initialization configurations in the plugins also need to be merged:

`/src/utils/mergeConfig.ts`：

```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;
};
```

### The Constructor of Rematch Class

Rematch is instantiated after the `init()` calls. Let's look at its constructor:

```ts
/**
 * 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]);
      }
    }
  }

  // ...
}
```

In the constructor, the main task is to bind `config`, `pluginFactory`, and `plugins` to the Rematch instance. We'll skip the plugin part for now.

Here's a brief explanation of the `forEachPlugin()` method, which is used to traverse all plugins. If a corresponding hook exists on the plugin (matched through the first parameter `method`), it calls the second parameter (a callback function) and passes the hook as a parameter back. For example, in the constructor, all plugins' `middleware` hooks are collected and merged into the `redux.middleware` configuration.

### The `init` method on Rematch instance

After instantiating the Rematch class, its `init()` method is called:

```ts
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;
  }
}
```

In `init()`, models are first initialized and bound. If a plugin has registered an `onModel` hook, it is called. Then, a native Redux store is generated and wrapped in a Rematch store. Finally, if a plugin has registered an `onStoreCreated` hook, it is called. If the hook returns an object, the object's properties are merged into the Rematch store.

The main part of this process is to explain how to use the `createRedux()` function to generate a native Redux store, which is the second part of the Rematch core about Redux.

## Create Redux Store

The Redux part mainly transforms the models configured in Rematch into Redux reducers, eventually generating a Redux store.

The file is located at `/src/redux.ts`, containing only one default export function:

```ts
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 =
    typeof redux.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);

  const middlewares = Redux.applyMiddleware(...redux.middlewares);
  const enhancers = composeEnhancersWithDevtools(redux.devtoolOptions)(
    ...redux.enhancers,
    middlewares
  );

  this.store = createStore(rootReducer, initialState, enhancers);

  return this;
}
```

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

### Create Model Reducers

First, create a unified reducer for each Model. Since each Model contains multiple reducers, this step creates a unified one, then dispatches to the corresponding individual reducers.

```ts
/**
 * 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 =
    typeof redux.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);
  }

  // ...
}
```

First, some default values are filled in, including `createStore`, `combineReducers` methods, initial `initialState`, and `reducers`. Then, each model is traversed to generate a model-level reducer, with `model.name` as the key, filling the `this.reducers` object.

There are 2 main points to note here:

- The generation of `action.type`. If the reducer method name contains `/`, then Rematch considers it a reducer for listening (e.g., if there are two Models, `modelA` and `modelB`, and `modelA` has a reducer named `foo`, then a listening reducer `modelA/foo` can be defined in `modelB`. So when the dispatched action is `modelA/foo`, both reducers will execute). The corresponding action type is the name of that reducer; otherwise, it is `${model.name}/${modelReducerName}`.
- Each model can define a `baseReducer`. If there is a `baseReducer`, each model will go through its corresponding `baseReducer` before reaching the specific reducer.

After creating each Model's reducer, the next step is to create a `rootReducer`.

### Create Root Reducer

After the Root reducer is created, a Redux store can be generated. Let's look at the following code:

```ts
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);

  // ...
}
```

Now, let's look at the `this.createRootReducer()` function. It first calls `this.mergeReducers()` to combine reducers. After the above steps, each model's reducers have been collected into a `reducerMapObject` (`this.reducers`), and in `this.mergeReducers()`, [`combineReducers`](https://redux.js.org/api/combinereducers) is called to merge them into the final rootReducer.

It's important to note that, similar to configuring a `baseReducer` for each Model, Rematch also allows you to configure a global `redux.rootReducers`. When dispatching an action, it first checks if there is a matching reducer in `redux.rootReducers`. If so, it processes through this reducer first, then the final rootReducer dispatches to the specific reducers.

### Create Redux Store

Finally, a Redux store is created and returned:

```ts
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);

  const middlewares = Redux.applyMiddleware(...redux.middlewares);
  const enhancers = composeEnhancersWithDevtools(redux.devtoolOptions)(
    ...redux.enhancers,
    middlewares
  );

  this.store = createStore(rootReducer, initialState, enhancers);

  return this;
}
```

This part is similar to Redux, but Rematch completely encapsulates it, so I won't go into detail.

## Craete Rematch Store

Lastly, let's look at the creation of the Rematch store, recalling the previous code:

```ts
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;
  }
}
```

As can be seen, the Rematch store adds a `name` property and a `model` method on top of the Redux store. The former, I guess, is used to distinguish between multiple Rematch stores, while the latter is used to dynamically add models. Let's focus on how to dynamically add models:

1. First, associate the model with the Rematch instance.
2. Then create a reducer for the model.
3. Finally, recreate a rootReducer and use [Redux's `replaceReducer()` method](https://redux.js.org/api/store#replacereducernextreducer) for replacement. A special action is also dispatched to indicate this.

> It should be noted that in the code `redux.mergeReducers(redux.createModelReducer(model))`, the call to `redux.mergeReducers` is actually redundant and ineffective since its return value is not used. Moreover, in the `redux.createRootReducer(this.config.redux.rootReducers)` method below, `redux.mergeReducers` is called again, where it actually takes effect.

**In Rematch v1, due to the use of the Rematch instance, many variable objects are directly associated with `this` and are updated by direct mutation, leading to some redundant or "strange" aspects. In v2, the Rematch instance is replaced by functional programming. However, some variable bindings to `this` have not been completely removed, so there are still some issues like this in the code, which I will explain separately when writing about the upgrade from v1 to v2.**

---

This concludes the introduction to Rematch core. Next, I will explain the plugin mechanism of Rematch in two parts: the first part will introduce the pluginFactory and Rematch's two core plugins, and the second part will discuss several third-party plugins developed by the Rematch team to understand this mechanism more deeply. 

Finally, I will write two articles, one on the changes in upgrading from Rematch v1 to v2 and the other on the Rematch type system (the biggest change brought by the upgrade to v2) as well as some remaining issues and difficulties of this type system, which I will discuss with you.

Stay tuned!

