This week’s intensive reading is rethinking Redux.

1 the introduction

Rethinking Redux is a dry essay by Shawn McKay, author of Rematch.

Since DVA, there have been a number of redux-based state management frameworks, but most of them are limited or even regressive-looking. But it wasn’t until Rematch that the Redux community took another step forward.

What’s valuable about this article is that an in-depth rethinking of Redux, away from Mobx and RXjs concepts, is very helpful for most engineering scenarios that still use Redux.

2 an overview

What is new is that the authors present a formula to evaluate the quality of a framework or tool:

Tool quality = time saved by tool/time spent using tool

If we evaluate native Redux this way, we can see that the extra time spent using Redux probably outweighs the time saved, and in that sense, Redux is inefficient.

But redux’s data management ideas are right, and complex front-end projects need them, and in order to use Redux effectively, we need to use a Redux-based framework. The author explains from six perspectives what needs to be addressed by a Redux-based framework.

Simplified initialization

The redux initialization code involves many concepts, such as compose Thunk, and splits the reducer, initialState, and middlewares into function calls instead of the more acceptable configuration:

const store = preloadedState= > {
  return createStore(
    rootReducer,
    preloadedState,
    compose(applyMiddleware(thunk, api), DevTools.instrument())
  );
};
Copy the code

The cost of understanding is much lower if you switch to configuration:

const store = new Redux.Store({
  instialState: {},
  reducers: { count },
  middlewares: [api, devTools]
});
Copy the code

My note: The initialization of Redux is very functional, while the following configuration is a bit more object-oriented. In contrast, the object-oriented approach is easier to understand, because store is an object. InstialState has the same problem. Using preloadedState as an entry parameter to a function is a bit more abstract than using display declarations. Redux’s initial state assignment is also a bit more subtle. Because Reducers are decentralized, if assigned in reducers, take advantage of the default parameter properties of ES and look more like business thinking than the capabilities redux provides.

Simplify the Reducers

The reducer granularity of Redux is too large, which not only leads to manual matching of type within the function, but also brings understanding costs such as type and payload:

const countReducer = (state, action) = > {
  switch (action.type) {
    case INCREMENT:
      return state + action.payload;
    case DECREMENT:
      return state - action.payload;
    default:
      returnstate; }};Copy the code

If you set up reducers as a configuration, as if you were defining an object, it would be clearer:

const countReducer = {
  INCREMENT: (state, action) = > state + action.payload,
  DECREMENT: (state, action) = > state - action.payload
};
Copy the code

Support the async/await

Redux support for dynamic data is a bit of a struggle, you need to understand higher-order functions and understand how middleware is used, otherwise you won’t know why this is right:

const incrementAsync = count= > async dispatch => {
  await delay();
  dispatch(increment(count));
};
Copy the code

Why not wipe out the cost of understanding and just allow async type actions?

const incrementAsync = async count => {
  await delay();
  dispatch(increment(count));
};
Copy the code

Note: We found that the rematch method, dispatch is imported (global variable), while Redux’s dispatch is injected. At first glance, it seems that Redux is more reasonable, but in fact, I prefer rematch. After long-term practice, it is better for components not to use data flow, and only one instance of data flow of a project is sufficient. In fact, the design of global Dispatch is more reasonable, while the design of injection Dispatch seems to pursue the ultimate technology, but ignores the business usage scenarios, resulting in superfluous and unnecessary trouble.

Change action + Reducer to two actions

The redux abstraction of Action and reducer is clear: Action is responsible for everything except store changes, while Reducer is responsible for store changes and is occasionally used for data processing. In fact, this concept is vague, because it is often not clear whether data processing is put in action or reducer, and at the same time, actions should be written to match the reducer which is too simple, which feels too formalized and cumbersome.

Reconsidering this problem, we have only two types of actions: Reducer Actions and Effect Actions.

  • Reducer Action: Change the store.
  • Effect Action: handles asynchronous scenarios, can call other actions, cannot modify store.

In synchronous scenarios, only a Reducer function can handle the job. In asynchronous scenarios, effect actions need to handle the asynchronous part and the synchronous part is still assigned to the Reducer function. These two actions have clearer responsibilities.

Action type declarations are no longer displayed

ACTION_ONE = ‘ACTION_ONE’ const ACTION_ONE = ‘ACTION_ONE’ const ACTION_ONE = ‘ACTION_ONE’ const ACTION_ONE = ‘ACTION_ONE’

Redux recommends payload key as the input parameter. Why not use action. Payload as the input parameter? Using payload directly not only visually reduces the amount of code and makes it easier to understand, but also enforces a code style that allows suggestions to be implemented.

Reducer acts directly as ActionCreator

Redux is cumbersome to call action. Dispatch or reducer is packaged by ActionCreator function. Why not automatically package ActionCreator directly to reducer? Reduce boilerplate code and make every line of code have business meaning.

Finally, the author gives a complete example of rematch:

import { init, dispatch } from "@rematch/core";
import delay from "./makeMeWait";

const count = {
  state: 0,
  reducers: {
    increment: (state, payload) = > state + payload,
    decrement: (state, payload) = > state - payload
  },
  effects: {
    async incrementAsync(payload) {
      await delay();
      this.increment(payload); }}};const store = init({
  models: { count }
});

dispatch.count.incrementAsync(1);
Copy the code

3 intensive reading

I felt that this article basically analyzed the engineering problems of Redux thoroughly, and also provided a very good implementation.

The ultimate optimization of details

Payload payload payload payload payload payload payload payload payload payload payload payload payload

increment: (state, payload) = > state + payload;
Copy the code

Use async in effects instead of put({type:}), use this.increment in effects instead of put({type:}). “Increment “}) (DVA) in typescript has type support, which not only replaces string searches with automatic jumps, but also validates parameter types, which is rare in the Redux framework.

Finally, in the dispatch function, two calls are provided:

dispatch({ type: "count/increment", payload: 1 });
dispatch.count.increment(1);
Copy the code

If you want better type support, or if you want to mask the payload concept, you can use the second scheme to simplify the Redux concept again.

There are more plug-ins built in

Rematch integrates the common reselect, Persist, immer, and so on into plug-ins, relatively reinforcing the concept of plug-in ecology. Data flow has further room for data caching, performance optimization, and development experience optimization. Embracing the plug-in ecosystem is a good development direction.

For example, the rematch-immer plugin allows you to modify stores in a mutable way:

const count = {
  state: 0,
  reducers: {
    add(state) {
      state += 1;
      returnstate; }}};Copy the code

But immer won’t work when state is not an object, so it’s best to get in the habit of returning state.

One final flaw is that the reducers declaration is inconsistent with the call parameters.

The Reducers declaration is inconsistent with the call parameter

Such as the following reducers:

const count = {
  state: 0,
  reducers: {
    increment: (state, payload) = > state + payload,
    decrement: (state, payload) = > state - payload
  },
  effects: {
    async incrementAsync(payload) {
      await delay();
      this.increment(payload); }}};Copy the code

Increment is defined as two arguments, while incrementAsync calls incrementAsync with only one argument, which may be misleading. We recommend keeping the parameter relationship and putting state in this:

const count = {
  state: 0,
  reducers: {
    increment: payload= > this.state + payload,
    decrement: payload= > this.state - payload
  },
  effects: {
    async incrementAsync(payload) {
      await delay();
      this.increment(payload); }}};Copy the code

Of course, the rematch method preserves the no side effect nature of the function, so we can see that there are some trade-offs.

4 summarizes

Repeat the author’s formula for tool quality:

Tool quality = time saved by tool/time spent using tool

One of the biggest lessons I’ve learned is that if a tool saves development time but incurs significant use costs, don’t rush into a project until you’ve figured out how to reduce use costs.

Finally, I would like to thank the authors of Rematch for their spirit of excellence and for bringing further optimization to Redux.

5 More Discussions

The discussion address is: Close reading rethinking Redux · Issue #83 · dt-fe/weekly

If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays.