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.