After the Redux redesign, this post will continue to talk about the differences between Rematch and Redux. I’ll implement a simple Counter with Redux and Rematch, respectively, where the Redux implementation uses redux-Thunk and redux-saga for asynchronous processing, respectively. Using this example, we will compare the differences between Rematch and Redux, and what benefits Rematch has (what learning costs are reduced, what code is reduced, etc.). Finally, we will show how Rematch wraps Redux to smooth the transition. This will cover Rematch’s code architecture, which I’ve broken down into several parts that I’ll cover in a later article.

The article Outlines

Simple Counter (Counter) case

Next, I’ll implement a React version of the simple counter using Redux and Rematch, respectively, with increments, decrement, and asynchronous increments.

The screenshot is as follows:

Redux implementation

In the pure Redux implementation version, one solution uses Redux-Thunk and the other uses Redux-Saga for the implementation of asynchronous addition.

The directory structure is simple:

SRC | - components | | - Counter. Js | - reducers | | -- index. Js | -- index. JsCopy the code

The components/ counter.js code is as follows:

import React, { Component } from "react";
import PropTypes from "prop-types";

class Counter extends Component {
  render() {
    const { value, onIncrement, onDecrement, onIncrementAsync } = this.props;
    return (
      <p>
        Clicked: {value} times <button onClick={onIncrement}>+</button>{" "}
        <button onClick={onDecrement}>-</button>{" "}
        <button onClick={onIncrementAsync}>Increment async</button>
      </p>
    );
  }
}

Counter.propTypes = {
  value: PropTypes.number.isRequired,
  onIncrement: PropTypes.func.isRequired,
  onDecrement: PropTypes.func.isRequired,
  onIncrementAsync: PropTypes.func.isRequired,
};

export default Counter;
Copy the code

The code of reducers/index.js is as follows:

export default (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      returnstate; }};Copy the code

The code in index.js, however, contains asynchronous logic, so the code varies depending on the scheme used, as described below.

Asynchronous based on Redux-Thunk

Please click for the full code

If redux-thunk is used asynchronously, the code in index.js is as follows:

import React from "react";
import ReactDOM from "react-dom";
import { applyMiddleware, createStore } from "redux";
import Counter from "./components/Counter";
import counter from "./reducers";
import thunk from "redux-thunk";

const store = createStore(counter, applyMiddleware(thunk));
const rootEl = document.getElementById("root");

function fakeAsyncLogic() {
  return new Promise(function (rs) {
    setTimeout(rs, 1000);
  });
}

function makeAsyncIncrementAction() {
  return async function (dispatch) {
    await fakeAsyncLogic();
    dispatch({ type: "INCREMENT" });
  };
}

const render = () = >
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={()= > store.dispatch({ type: "INCREMENT" })}
      onDecrement={() => store.dispatch({ type: "DECREMENT" })}
      onIncrementAsync={() => store.dispatch(makeAsyncIncrementAction())}
    />,
    rootEl
  );

render();
store.subscribe(render);
Copy the code

With Thunk middleware, Dispatch () can receive functions as arguments, and the Redux Store will pass dispatch and getState as arguments to the function, so that if the function is asynchronous, the action can be dispatched asynchronously.

Asynchronous based on Redux-Saga

Please click for the full code

If redux-saga is used asynchronously, the code in index.js is as follows:

import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import Counter from "./components/Counter";
import counter from "./reducers";
import defaultSaga from "./reducers/saga";

const sagaMiddleware = createSagaMiddleware();

const store = createStore(counter, applyMiddleware(sagaMiddleware));
const rootEl = document.getElementById("root");

sagaMiddleware.run(defaultSaga);

const render = () = >
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={()= > store.dispatch({ type: "INCREMENT" })}
      onDecrement={() => store.dispatch({ type: "DECREMENT" })}
      onIncrementAsync={() => store.dispatch({ type: "INCREMENT_ASYNC" })}
    />,
    rootEl
  );

render();
store.subscribe(render);
Copy the code

In addition to the differences in middleware configuration code, instead of sending a function as an action like Redux-Thunk, saga needs to define some asynchronous logic for saga (using some of the asynchronous apis that come with Saga), so, Add a saga.js to SRC /reducers:

import { takeEvery, call, put } from "redux-saga/effects";

async function fakeAsyncLogic() {
  return new Promise((rs) = > setTimeout(rs, 1000));
}

function* increamentAsync() {
  yield call(fakeAsyncLogic);
  yield put({ type: "INCREMENT" });
}

export default function* defaultSaga() {
  yield takeEvery("INCREMENT_ASYNC", increamentAsync);
}
Copy the code

Saga uses iterator functions (generators) to control asynchronous flows more finely. TakeEvery (“INCREMENT_ASYNC”, increamentAsync) to listen to all actions whose action.type is INCREMENT_ASYNC. Execute the increamentAsync() function, in which call(fakeAsyncLogic) is used to simulate asynchronous calls, followed by put({type: “INCREMENT”}) to dispatch an action, which will eventually make reducer execute.

Rematch implementation

Please click for the full code

There are no separate reducers in Rematch, the reducers all belong to the same data structure called Model, so the reducers have a slightly different directory structure (change reducers to Models) :

SRC | - components | | - Counter. Js | - models | | -- index. Js | -- index. JsCopy the code

Model /index.js = model /index.js

async function fakeAsyncLogic() {
  return new Promise((rs) = > {
    setTimeout(rs, 1000);
  });
}

export const count = {
  state: 0.reducers: {
    increment: (state) = > {
      return state + 1;
    },
    decrement: (state) = > {
      return state - 1; }},effects: (dispatch) = > ({
    async incrementAsync() {
      awaitfakeAsyncLogic(); dispatch.count.increment(); }})};Copy the code

As you can see, we have defined a model called COUNT, which contains state, reducers and effectes, and state is the data belonging to the model, Equivalent to the first parameter (or its return value) of the Reducer function in redux. Reducers are the same as Redux Reducer, and the last effects are logic with side effects (such as asynchronous interface calls, etc.).

Finally, index.js:

import { init } from "@rematch/core";
import React from "react";
import ReactDOM from "react-dom";
import Counter from "./components/Counter";
import * as models from "./models";

const store = init({ models });
// const store = createStore(counter, applyMiddleware(thunk));
const rootEl = document.getElementById("root");

const render = () = >
  ReactDOM.render(
    <Counter
      value={store.getState().count}
      onIncrement={store.dispatch.count.increment}
      onDecrement={()= > store.dispatch({ type: "count/decrement" })}
      onIncrementAsync={() => store.dispatch({ type: "count/incrementAsync" })}
    />,
    rootEl
  );

render();
store.subscribe(render);
Copy the code

Instead of creating the store using Redux’s createStore API, use rematch’s init. Two more points to note here:

  1. At this timestore.getState()Instead of returning a value, it returns{ count: number }
  2. store.dispatchNot only is a function (maintain the redux call mode), but also supportsdispatch.modelName.xxxThis calls a reducer or effect. But it’s important to note,action.typeAt this time formodelName/reducerNameormodelName/effectNameThis form

As mentioned earlier, the smallest component of a rematch is a Model. Therefore, the above changes are also intended to be compatible with the model form.

rematch vs redux

From the example above, we can see the following differences:

  1. With redux, asynchrony requires a separate middleware, such as Thunk or Saga. In rematch, we can just use ESasync/awaitAsynchronous syntax to implement asynchronous dispatch action.
  2. There is no concept of model in Redux. If the state structure is complex, you can use combineReducers to combine different Reducer to form a similar state structure. And rematch is supported natively.
  3. The storystore.dispatchIt’s just a function. However, rematch retains the function function, while providing a way to chain calls.

Because the above example is relatively simple, there are few differences. More differences can be found in Redux. Here are two more common differences:

  1. Redux uses more functional programming ideas, such as store initialization, and provides a utility function, compose, for composing store enhancer.
  2. The reducer is simplified, mainly including omittingaction.typeConstant definition, omitting the reducer inswitch/caseBranch judgment. Therefore, a reducer of the model in rematch is equivalent to a reducercaseBranch, its name is the same thing asaction.type.

In my opinion, rematch is better than Redux in three main ways:

  1. More “reasonable” data structure design. Rematch uses the concept of model and integrates state, Reducer and effect, which is very practical in front-end development. For example, different models can be designed for different page routes.
  2. The more concise API design, the combination of function configuration methods used in Redux, may be confusing at first for developers unfamiliar with functional programming, but rematch uses object-based configuration items, which makes it easier to get started.
  3. Less code.
  • I removed a lot of the reduxaction.typeConstant and branch judgment
  • The native syntax supports asynchrony without the need for middleware. With Saga, there is some learning cost, and with Thunk, the types of action distributed are different, and there is some confusion

In addition to this, Rematch also provides a plug-in mechanism, which allows for custom development in addition to the many plug-ins developed by the community, more on which in a later article.

Rematch code structure

As we know, rematch is really just a wrapper around Redux, which simplifies redux’s complex syntax:

Rematch is Redux best practices without the boilerplate.

Because of this, it’s still redux on top, and it doesn’t diminish redux’s functionality. So, how does Rematch do it, and how does he design it? I’ll go over the rematch core code structure based on rematch V1.4.0 (the last version of Rematch V1).

Note: The translator was involved in the rematch V2 update and will write a post on the rematch v1 to v2 change. V1 is used here because the code logic does not change radically, v1 is easier to read and understand, whereas V2 is more varied in style.

Let’s look at the code directory structure of Rematch v1.4.0:

. 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:

Rematch is composed of core and plugin. The core is divided into two parts: rematch class and redux.ts file. The former is the core source code of Rematch, while the latter mainly contains the codes merged by reducer. Used to create the Redux store.

Plugin is the plugin mechanism provided by rematch to enhance rematch. The main code is defined in plugin Factory. The Rematch Core contains two core plugins, Dispatch and Effect. The Dispatch plug-in enhances store.Dispatch to support chaining calls. The Effect plug-in is primarily used to support async/await mode. In addition to these two plugins, the Rematch team has developed other third party plugins, such as Loading, select, etc., that integrate asynchronous request loading state and selector.

Next, I will explain these parts separately and break them down into three articles, rematch core, Plugin Factory && Core plugins, 3rd Party plugins. At the end of the three articles, I will write two more, one on the changes to Rematch V1 to V2, and the other on the rematch type system (the biggest change to v2) and some of the remaining problems and difficulties of the type system.

Stay tuned!