Starting with this article, take a look at Rematch V2.

Why I joined Rematch

Before I get to V2, let me tell you about my experience with Rematch.

Looking back, I didn’t expect to be a contributor to Rematch, the first high-profile open source project I was deeply involved in. At the beginning, it was because Rematch was used in the company’s new project. In the process of using it, I found its TS compatibility was very poor, especially dispatch, and TS type was not compatible at all. I mentioned issue and corresponding PR for this reason, but Rematch was not handled in time. I just raised a PR in the internal scaffolding and created a file remate.d.ts to override the rematch’s own type definition, fixing most of the types.

About a week later, the maintainer of Rematch contacted me via email asking me to help with the Rematch type issue, which I readily agreed to do, so I got involved in the V2 iteration.

The final version of Rematch V1, the code example for the previous article in this column, is v1.4.0, released on 2020-02-23. After that, the Rematch team began iteration v2 (at this point Rematch’s founders were no longer in charge of Rematch) and released the first pre-release version @rematch/[email protected] on 2020-07-30. I started Rematch on 2020-08-17 and pushed for the pre-release version of @rematch/[email protected] to be released on 2020-08-19. After 10 pre-releases, we finally released the official V2 at 2021-01-31, almost a year in the making.

So that’s a brief history of Rematch V2 and why I joined Rematch. In this article, I’ll focus on some of the changes brought about by the Rematch V2 upgrade. Since MOST of my involvement was in the type compatibility of TS, some of these changes were made by myself, while others were made by asking some members of the team and getting their ideas.

Changes to the directory structure

Rematch V2 makes significant changes to the code and directory structure and uses Lerna for project management (Rematch is a natural fit for Lerna’s Monorepo package management scheme because it contains plugins). Let’s look at the changes to the directory structure first:

Rematch v1 directory structure

. 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

Rematch v2 directory structure

. Blog docs website packages | - core | | - test | | - SRC | | | -- bag. Ts | | | -- config. Ts | | | -- dispatcher. Ts | | | - ReduxStore. Ts | | | -- rematchStore. Ts | | | -- types. Ts | | | -- validate. Ts | | | -- index. Ts | | - node_modules | | -...  | - immer | | - test | | - SRC | | | -- index. Ts | | - node_modules | | -... | - loading | | -- - the select...Copy the code

As you can see, the plugins are managed as packages, and the rematch core code is also a package called core. The two core plugins and pluginFactory are removed, and the relevant code is integrated into the core. And new bag.ts and dispatcher.ts. I’ll talk about each of them individually.

Remove the Rematch class and replace it with a function

In my opinion, the biggest change in Rematch V2 is to remove “classes” from the code and replace them with functions.

Let’s take a look at v1’s Rematch class (I’ll omit most of the code here, but you can see it earlier in this column if you want to learn all of it) :

/** * Rematch class * * an instance of Rematch generated by "init" */
export default class Rematch {
  protected config: R.Config;
  // ... other private configs

  constructor(config: R.Config) {}

  public init() {
    // ...

    const rematchStore = {
      name: this.config.name, ... redux.store,// ...
    };

    returnrematchStore; }}Copy the code

A call:

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

Looking at the way the code is called, the use of “class” doesn’t really highlight any of the advantages. Let’s take a look at v2:

export default function createRematchStore<
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels> > (config: Config<TModels, TExtraModels>) :RematchStore<TModels.TExtraModels> {
  // ...

  const reduxStore = createReduxStore(bag);

  letrematchStore = { ... reduxStore,name: config.name,
    // ...
  } as RematchStore<TModels, TExtraModels>;

  return rematchStore;
}
Copy the code

V2 defines a function with a descriptive name called createRematchStore, which is also easier to use:

/** * Prepares a complete configuration and creates a Rematch store. */
export const init = <
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels> = Record<string.never> >( initConfig? : InitConfig<TModels, TExtraModels> ): RematchStore<TModels, TExtraModels> => {const config = createConfig(initConfig || {});
  return createRematchStore(config);
};
Copy the code

Semoal, the current Rematch maintainer, agrees with me about this change. He thinks JavaScript is better for functional programming, and he prefers this approach. In fact, I wrote an article before: JavaScript object oriented, do you really understand? Do YOU really need to use “classes” to program? In this article, I’ve also looked at the difference between using pure objects and “classes” when programming. Most of the time, we don’t need to use “classes”, and classes in JavaScript are still functions and objects. But JavaScript class inheritance, the essence of the prototype chain of the chain association, interested students can look at.

Remove core plugins and pluginFactory

Rematch’s plug-in system, mentioned earlier in this column, has pluginFactory and two core plug-ins, Dispatch and Effects. However, this code has been removed from V2 and the related logic has been integrated into Core.

It is highly recommended that you read this section after reading the previous column.

pluginFactory

I think pluginFactory has two functions:

  1. Support the exposed. Plug-ins can use the exposed attribute, and exposed methods or objects are bound to the pluguinFactory

  2. Bind this to pluginFactory for all hook contexts as well

The two core plugins in Rematch V1, Dispatch and Effects, expose the Dispatch and Effects objects, respectively, for other plug-ins to access in the hooks function. But in V2, everything becomes a function to simplify the process, effects goes into the Rematch bag configuration mentioned below, and Dispatch is the direct redux.dispatch operation.

Exposed misuse of V2

To be honest, I don’t think the early team did a very good job of optimizing the pluginFactory. For example, this from hooks is bound to pluginFactory, which makes it easy to access Dispatch and effects, but in V2 dispatch is directly equivalent to redux.dispatch, It needs to be accessed via rematchStore.dispatch, and Effects goes into the Rematch bag. The arguments are not consistent when different hooks are called, but I suspect that other plugins are not accessing Dispatch and Effects frequently in their own hooks either, so no one is paying attention or raising the issue.

In addition to the above, another big problem is v2’s misuse of the exposed parameter. In V1, plugins can expose methods or properties, such as Dispatch and Effects, using the exposed parameter, but this exposure is limited to plugins (because pluginFactory objects are attached to pluginFactory objects). And V2 specifically created an addExposed function for the plug-in of these methods or attributes directly exposed to rematchStore, personally believe that this is a misunderstanding of the original design concept. Normally, this should be done using the onStoreCreated hook. If this function is simply added, it also appears redundant.

Here is the addExposed code:

/** * Adds properties exposed by plugins into the Rematch instance. If a exposed * property is a function, it passes rematch as the first argument. * * If you're implementing a plugin in TypeScript, extend Rematch namespace by * adding the properties that you exposed from your plugin. */
function addExposed<
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels> > (store: RematchStore
       
        , plugins: Plugin
        
         []
        ,>
       ,>) :void {
  plugins.forEach((plugin) = > {
    if(! plugin.exposed)return;
    const pluginKeys = Object.keys(plugin.exposed);
    pluginKeys.forEach((key) = > {
      if(! plugin.exposed)return;
      const exposedItem = plugin.exposed[key] as
        | ExposedFunction<TModels, TExtraModels>
        | ObjectNotAFunction;
      const isExposedFunction = typeof exposedItem === "function"; store[key] = isExposedFunction ? (... params:any[]) :any= >
            (exposedItem asExposedFunction<TModels, TExtraModels>)( store, ... params ) :Object.create(plugin.exposed[key]);
    });
  });
}
Copy the code

dispatch plugin

Dispatch in v1 is mainly used to enhance the story. Dispatch, distributed supply chain action, such as dispatch. The modelName. ReducerName. This logic is put into v2 dispatcher.ts.

effects plugin

The Effects Plugin does two things:

  1. Exposing the whole pictureeffects
  2. usemiddlewareHook treatment side effects
  3. Support chain distribution of side effects actions, for exampledispatch.modelName.effectName.

In V2, the effects of point 1 are put into a rematch bag, and the function createEffectsMiddleware is created to handle point 2. The third point of logic has also been moved to the Dispatcher.ts file.

New rematch bag

V2 added an object called rematchBag to store some globally accessible information, such as the forEachPlugin method in v1 rematch class, as well as the complete models, reduxConfig configuration. And the global effects exposed by the Effects plugin mentioned above:

/** * Object for storing information needed for the Rematch store to run. * Purposefully hidden from the end user. */
export interface RematchBag<
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels>
> {
  models: NamedModel<TModels>[];
  reduxConfig: ConfigRedux;
  forEachPlugin: <Hook extends keyof PluginHooks<TModels, TExtraModels>>(
    method: Hook,
    fn: (content: NonNullable<PluginHooks<TModels, TExtraModels>[Hook]>) => void
  ) => void;
  effects: ModelEffects<TModels>;
}
Copy the code

The new plugin hooks

V1 can use four hooks: onInit, Middleware, onModel and onStoreCreated. V2 deleted onInit (because onInit is only used in pluginFactory, the pluginFactory mentioned above has been removed) and added onReducer and onRootReducer. Also change the name of Middleware to createMiddleware.

Middleware was renamed createMiddleware

Instead of receiving redux Middleware directly, createMiddleware receives a function that returns redux Middleware, However, you can pass the aforementioned Rematch bag as an argument to make it easier for users to customize middleware.

In V1, effects, partition-state and Subscriptions (which were removed in V2) were used. In V2, only partition-state was left and the rematch bag parameter was not used for the time being.

For example, v1 and v2 use comparison:

V1:

const typedStatePlugin = (): Plugin= > ({
  // ...
  middleware: (store) = > (next) = > (action) = > {
    // ...}});Copy the code

V2:

const typedStatePlugin = <
  TModels extends Models<TModels>,
  TExtraModels extends Models<TModels> = Record<string.any>
>(
  config = DEFAULT_SETTINGS
): Plugin<TModels, TExtraModels> => {
  // ...

  return {
    // ...
    createMiddleware: () = > (store) = > (next) = > (action) = > {
      // ...}}; };Copy the code

Remove the onInit hook

The onInit hook is called in the pluginFactory create method. I searched the v1 code and did not see any plugin using this hook, so I do not know what it does.

V2 has no onInit because pluginFactory has been removed.

New onReducer hook

V2 added onReducer on the basis of onModel to control reducer behavior with finer granularity. This hook will be called when a single Reducer is generated. If there is a return value, the final reducer will be replaced by the return value, otherwise the original reducer will continue to be used.

As mentioned before when talking about immer plug-in, immer in V1 is mainly implemented through redux.combineReducers configuration, and this configuration has the problem of overwriting. Therefore, in V2, immer Plugin was reconstructed through the fine-grained onReducer, and likewise, the Persist plug-in was reconstructed. In this way, the combinerers overwrite problem is avoided when immer and persist are used together.

New onRootReducer hook

This hook is similar to onReducer except that it is called when rootReducer is finally generated. Currently, only the Persist plug-in uses this hook.

conclusion

These are the major changes brought about by this update. It seems a bit much, but it does not have much impact on the code running, and the code flow is clearer and easier to understand due to the use of functional programming. However, there are still some issues, such as the people behind this update and the Rematch V1 developers are not the same, and the authors are no longer maintaining the framework, which inevitably leads to some misunderstanding of previous design concepts.

In addition, v1 code used a lot of this, and did not follow the pure function programming method, the code was various mutations. The use of this was reduced in V2, but there were still many mutation of parameters. All this makes the source code more complex and the process difficult to comb and understand.

In my final article, I’ll talk about what I do best, which is the type system for Rematch. This is a very complex system, and I almost rewrote it in V2 to greatly improve Rematch usability. However, there are still a lot of problems, some of which are limited by my knowledge and some of which are also limited by TypeScript design. I will discuss them in the next article and hope that more talented type gymnasts can join us to continuously improve Rematch and create the best experience. Stay tuned!