How did things get so complicated

Redux is not complicated originally, and its basic concept can be summarized as follows: changes are submitted through action and new states are calculated by reducer. Reducer is a pure function with state and action as parameters and new state as return value.

state -> dispatch(action) -> reducer -> new State => new View
Copy the code


Consider the most primitive case, dispatch action objects directly in the business code, then only three files need to be defined: state.js, reduce. js, and store.js, which are used to define state, reducer, and create store respectively.


1. Do not submit action objects directly

It is not good for business code to dispatch action objects directly because of duplication and typeless validation. The official recommendation is to use functions to create actions with constants of type rather than strings, and to ensure that the type is globally unique. We add two more files: actions.js and type.js, which define the action function and type constant respectively.

As a result of this widely criticized template code (all caps for constants), schemes to simplify action and type definitions such as Reduxsause and React-Arc emerged.


2. Reducer needs to be disassembled

The reducer interface was only agreed officially, but no specific implementation was specified. Switch was used in the original case, and each case corresponded to one action. When the number of actions increased, an excessively long reducer was obviously not conducive to maintenance. The official provides a splitting idea, and provides the corresponding auxiliary functions (combineReducers).

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}
Copy the code


However, this mode has many problems, such as: each field needs to define a reducer; State parameters of the reducer are different, and the initial values should not be forgotten. The same action may be distributed in different reducers. If a new action corresponds to changes in multiple fields, case branches need to be added in multiple Reducers.


3. How to update the deep data structure

The REdux contract state must be globally unique and immutable (conventions, not constraints), and reducer needs to return a new state in its entire set each time. If the data structure is deep, updating is cumbersome. This problem is relatively easy to solve by using a helper library such as immutable helper or seamless immutable.


4. What about asynchronous update logic

It seems that all the articles about Redux make the misleading claim that Redux does not support asynchronous status updates.

Does that mean that asynchronous logic in your application can’t be written if you just use Redux? Obviously not.

// Write asynchronous logic as usual, then commit the update, what's wrong?
fetch().then(data= > {
    dispatch(updateAction(data))
})
Copy the code


I just want to submit an action that will do the asynchronous logic. Vuex and mobX have support for it, so why not Redux?

Redux-thunk provides a syntactic sugar that lets you dispatch a thunk function that encapsulates asynchronous logic, making it semantically possible to commit an asynchronous action.

// A few lines of redux-Thunk middleware
function createThunkMiddleware() {
  return ({ dispatch, getState }) = > (next) => (action) = > {
    if (typeof action === 'function') { / / aha!
      return action(dispatch, getState)
    }
    return next(action)
  };
}
Copy the code


Read the bottom line and you’ll see where redux doesn’t support asynchronous status updates. The difference is just where the code logic aggregation is. The reducer update state must be triggered when an object is dispatched. There is no such thing as “having to introduce a library of Redux to support asynchrony.” Whether it’s Redux-Thunk, Redux-Promise, or Redux-Saga, they’re not so much trying to fix redux’s flaws as they’re trying to solve problems they’ve created.

At its extreme, Async Action is a conceptual trap unrelated to Redux. To be fair, Async Action is a code design abstraction. Instead of being a pure object, an action can now be a function, called an asynchronous action.

const a = { type, payload } This is a reducer action
const b = payload= > ({ type, payload }) This is also a reducer action
const c = dispatch= > fetch().then(res= > dispatch(xxx) // This is an async action
Copy the code


Any state management library that provides so-called asynchronous state update functionality is just an API wrapper. This certainly has its benefits, such as providing additional abstraction constraints, making the state update logic more cohesive, and making the business logic more pure, with components just issuing actions and rendering data. But don’t say “Redux doesn’t support asynchronous action, so you have to add thunk, you have to add saga.” That’s standing at the height of abstraction, getting lost in the cloud of concepts.


Why do we need middleware for async flow in Redux?


summary

Redux + React-redux + Action Type definition + IMMUTABLE data update + asynchronous process encapsulation Well, to write the simplest “request the interface and update the data” logic, you need to change 5 or 6 files. The length of the logical link is not only hard to write, but also hard to see/find.

As a result, relatively complete redux-like solutions such as REmatch (Redesigning Redux) and DVA have been integrated, packaged and even restructured by the community. More radically, some people were unwilling to be constrained by the “dispatch action only instead of directly calling the reducer update state”, and worked out [Action Reducer] plans, such as Redux-Zero and Reduxless.

// redux
dispatch(action) => reducer= > newState

// redux-zero
action(state, payload) => newState
Copy the code


There are not only a lot of “patching” solutions around Redux, but also a lot of “integration,” “reconfiguration,” and “alternative” solutions. On the positive side, this is the embodiment of ecological prosperity, and on the negative side, it is a mockery of Redux as the de facto industry standard for react state management. Does all this prove that Redux is not a good design in itself, or is it just that the user is messing things up?


A simplified scheme for automatically generating action

Each action must correspond to a Reducer processing logic. If the Reducer function is split according to the action granularity, each action corresponds to a reducer function, and each action has a unique type, it can be obtained as follows: Action, Type, and Reducer correspond one by one.

If the reducer names are not the same and the actions and types are directly multiplexed with the reducer names, the actions can be automatically generated according to the Reducer.

// reducer.js
There are many ways to reduce the reducer
// Each reducer corresponds to one action
// The first parameter is the global state, and the second parameter corresponds to the payload in the action
export const reducerA = (state, payload) = > state
export const reducerB = (state, payload) = > state

// action.js
Actions automatically generated by the reducer are expected
export const actions = {
    reducerA: payload= > ({ type: 'reducerA', payload }),
    reducerB: payload= > ({ type: 'reducerB', payload })
}
Copy the code


First, the reducer split according to the above method needs to be aggregated as follows:

// store.js
import initialState from './state.js'
import * as reducers from './reducer.js'

/ / polymerization reducer
function reducer(state = initialState, { type, payload }) {
  const fn = reducers[type];
  return fn ? fn(state, payload) : state;
}
Copy the code

The above method requires that all the reducer be aggregated in reducer.js. Note that all the reducer may not be defined in reducer.js, but can be divided into different files and exported uniformly in reducer.js, so as to ensure that the reducer will not have the same name.


With the Reducer map object, it is easy to automatically generate actions:

// action.js
import * as reducers from './reducer.js'

export const actions = Object.keys(reducers).reduce(
  (prev, type) = > {
    prev[type] = payload= > ({ type, payload })
    return prev
  },
  {}
)
Copy the code


To use it, bring in actions:

import { actions } from 'store/action.js'

dispatch(actions.reducerA(payload))
Copy the code


In this way, neither action nor Type need to be defined. Each time the logic is added, the state part only needs to write state and reducer.

Still, the above approach is not perfect because there are no types. In general, action is type-defined, and can verify parameters, and automatic completion prompt, can not lose.

// reducer.ts
export interface A {}
export const reducerA = (state: StateType, payload: A) = > state

// action.ts
//
import { A } from './reducer.ts'
export const actionA = (payload: A) = > ({ type: TYPE, payload })
Copy the code


Considering that the parameter type of the action function is the same as the type of the second parameter corresponding to the Reducer, is it possible to reuse both the reducer name and the parameter type, and automatically generate actions as well as the types?

// The interface to be generated
// How do I get the payload type defined by the reducer function
interface Actions {
    reducerA (payload: A): AnyAction
    reducerB (payload: B): AnyAction
}
Copy the code


Payload = payload (); / / Interface = / / interface = / / interface = / / interface = / /

As you can see, with the keyof keyword, the Actions type has taken all the keys.

So you set the payload type, and the key is how do you get the type of the second parameter in a given function type definition. The INFER keyword is provided in TS to extract types.

T represents the type of args, which is an array, and T[1] is the type of the second parameter.

With these two key steps in mind, the rest is a bit easier:

Now we have a type, and each time we add a Reducer, the latest actions and their types are automatically generated.

By omitting the action definition, you eliminate most of the template code. Other code cuts: mapDispatchToProps uses the official recommended shorthand, class defines state to extract StateType types directly, and encapsulates store definitions, but I won’t go into details.

Codesandbox.io /s/clever-ra…