background
I believe that in project development, in the case of a more complex page, often encounter a problem, is that it is very difficult to communicate between the page components.
For example, a list of items and a list of added items:
If the two lists are independent components, they share a data “selected items”. Selecting an item from the list affects the list of added items, and deleting an item from the list of added items also affects the list of selected items.
These two components are siblings. Without the help of data flow framework, data can only be transferred through the parent component when there is a change in the data in the component. OnSelectedDataChange often appears. Hence the emergence of various frameworks for addressing data flow.
Nature analysis
React is the V of MVC and is data-driven for views. Simply put, views are rendered based on data:
V = f(M)
Copy the code
When the data is updated, it will be transformed into a Virtual DOM before entering the rendering, and the real rendering will only be carried out if there is any change.
V + δ V = f(M + δ M)Copy the code
There are two ways for data-driven view changes. One is setState, which changes the page state, and the other is to trigger changes in props.
We know that data does not change on its own, so there must be some “external force” to push it, usually the remote request for data back or the UI interaction, we collectively call these actions:
Δ M = perform (action)Copy the code
Each action changes the data, so the state of the view is the sum of all the action changes,
state = actions.reduce(reducer, initState)
Copy the code
So a real scenario would have something like this or more complicated:
The problem is that updating data is troublesome and chaotic. Every time data is updated, it has to be passed layer by layer. In the case of complex page interaction, data cannot be controlled.
Is there a way, a centralized place to manage the data, centralized processing of receiving, modifying and distributing the data? The answer is yes. The Data flow framework does just that. If you’re familiar with Redux, you’ll know that this is the core concept of Redux, which matches the data-driven principles of React.
Data flow framework
Redux
The dominant data flow framework is Redux, which provides a global Store for receiving, modifying, and distributing application data.
Its principle is relatively simple. If there is any interactive behavior in the View that needs to change data, an action should be sent first, which is received by the Store and handed to the corresponding Reducer for processing. After processing, the updated data will be passed to the View. Redux doesn’t rely on any framework, it just defines a way to control the flow of data that can be applied to any scenario.
Although a set of data flow methods are defined, there will be many problems in real use. My personal summary is mainly two problems:
- Definition is too tedious, document is many, easy to cause thinking jump.
- There is no elegant solution for handling asynchronous flows.
Let’s take a look at writing a data request. This is a very typical case:
actions.js
export const FETCH_DATA_START = 'FETCH_DATA_START';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_ERROR = 'FETCH_DATA_ERROR';
export function fetchData() {
return dispatch= > {
dispatch(fetchDataStart());
axios.get('xxx').then((data) = > {
dispatch(fetchDataSuccess(data));
}).catch((error) = > {
dispatch(fetchDataError(error));
});
};
}
export function fetchDataStart() {
return {
type: FETCH_DATA_START, } } ... FETCH_DATA_SUCCESS ... FETCH_DATA_ERRORCopy the code
reducer.js
import { FETCH_DATA_START, FETCH_DATA_SUCCESS, FETCH_DATA_ERROR } from 'actions.js';
export default (state = { data: null }, action) => {
switch (action.type) {
case FETCH_DATA_START:
...
case FETCH_DATA_SUCCESS:
...
case FETCH_DATA_ERROR:
...
default:
return state
}
}
Copy the code
view.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from 'reducer.js';
import { fetchData } from 'actions.js';
const store = createStore(reducer, applyMiddleware(thunk));
store.dispatch(fetchData());
Copy the code
The first problem is that sending a request requires a lot of actions to be defined because you need to host all the states of the request. It’s easy to get confused, and even if someone tries to encapsulate these states into abstractions, they will be swamped with template code. Someone will challenge, said although the beginning is more troublesome, tedious, but the project maintainability, scalability are friendly, I don’t quite agree with this statement, it is easy, the real business logic complex cases, can appear more nausea, low efficiency and bad reading experience, I believe you also have written or seen such a code, behind his look back, Search the action name in the Actions file and search the Reducer file, then go around and slowly understand it.
The second problem is that the redux-thunk method, which is recommended for asynchronous actions, simply returns a function in the action. This is too much for the ocD to handle. The actions file is very impure. Interspersed with data requests and even UI interactions!
I think there is no problem with the design of Redux. The idea is very simple, and IT is a library I like very much. The way of data flow provided by Redux has been widely recognized by the community. However, in the use of its defects, although it can be overcome, but it itself can not be optimized?
dva
Dva is designed to solve the development experience problem of Redux. It puts forward the concept of Model for the first time and well combines action, reducers and state into one model.
model.js
export default {
namespace: 'products'.state: [].reducers: {
'delete'(state, { payload: id }) {
return state.filter(item= >item.id ! == id); ,}}};Copy the code
Its core idea is that an action corresponds to a reducer. By convention, the definition of action is omitted, and the function name in the default reducers is the name of action.
In the asynchronous action processing, defined the concept of effects (side effects), and synchronous action to distinguish, internal with the help of Redux-Saga to achieve.
model.js
export default {
namespace: 'counter'.state: [].reducers: {},effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus'}); ,}}};Copy the code
With this encapsulation, basically keeping the Redux usage, we could write our data logic in the Model in an immersive way, which I think solved the problem pretty well.
However, MY personal preference is not to use the Redux-Saga library to solve asynchronous flows. Although it is cleverly designed to take advantage of the generator’s features, it does not invade actions, but intercepts them through middleware, which is a good way to isolate asynchronous processing from a separate layer. And in doing so claim to be the friendliest way to implement unit tests. Yes, I think the design is really great, at that time I also specially read its source code, praise the author is really great, such a solution can come out, but later I saw there is a better solution (will be introduced later), so I gave up using it.
mirrorx
Mirrorx is similar to DVA except that it uses a singleton approach. All actions are stored in actions objects and there is a different way to access the actions. We can also use async/await when processing asynchronous actions.
import mirror, { actions } from 'mirrorx'
mirror.model({
name: 'app'.initialState: 0.reducers: {
increment(state) { return state + 1 },
decrement(state) { return state - 1}},effects: {
async incrementAsync() {
await new Promise((resolve, reject) = > {
setTimeout((a)= > {
resolve()
}, 1000)
})
actions.app.increment()
}
}
});
Copy the code
It handles asynchronous flows internally, similar to redux-Thunk, by injecting a middleware that determines whether the current action is an asynchronous action (as long as it’s an action defined in Effects), and if so, Directly interrupt the chain call middleware, you can look at this code.
In this case, functions in Effects can call asynchronous requests with async/await. We do not have to use async/await. There is no limit to the implementation of functions because the middleware just calls the function to execute.
I prefer to use async/await for asynchronous streams, that’s why I don’t use Redux-saga.
xredux
However, I did not choose to use MirrorX or DVA in the end, because they would bind a lot of things, I think it should not be made like this, why solve the Redux problem well, and finally make a scaffold? Isn’t this mandatory consumption? There are limits to how people can use it. After understanding their principles, I wrote my own reference to XRedux, which is purely to solve the problems of Reudx, independent of any framework, can be regarded as just an upgraded version of Redux.
It is similar to Mirrorx in use, but it is the same as Redux in that it is not bound to any framework and can be used independently.
import xredux from "xredux";
const store = xredux.createStore();
const actions = xredux.actions;
// This is a model, a pure object with namespace, initialState, reducers, effects.
xredux.model({
namespace: "counter".initialState: 0.reducers: {
add(state, action) { return state + 1; },
plus(state, action) { return state - 1; }},effects: {
async addAsync(action, dispatch, getState) {
await new Promise(resolve= > {
setTimeout((a)= > {
resolve();
}, 1000); }); actions.counter.add(); }}});// Dispatch action with xredux.actions
actions.counter.add();
Copy the code
In asynchronous processing, there is also a problem, which you may have encountered, that is, the data request has three states. Let’s look at the effects of writing a data request:
import xredux from 'xredux';
import { fetchUserInfo } from 'services/api';
const { actions } = xredux;
xredux.model({
namespace: 'user'.initialState: {
getUserInfoStart: false.getUserInfoError: null.userInfo: null,},reducers: {
// fetch start
getUserInfoStart (state, action) {
return {
...state,
getUserInfoStart: true}; },// fetch error
getUserInfoError (state, action) {
return {
...state,
getUserInfoStart: false.getUserInfoError: action.payload,
};
},
// fetch success
setUserInfo (state, action) {
return {
...state,
userInfo: action.payload,
getUserInfoStart: false}; }},effects: {
async getUserInfo (action, dispatch, getState) {
let userInfo = null;
actions.user.getUserInfoStart();
try {
userInfo = await fetchUserInfo();
actions.user.setUserInfo(userInfo);
} catch(e) { actions.user.setUserInfoError(e); }}}});Copy the code
It can be seen that there are still many useless codes. A request needs 3 reducer and 1 effect. At that time, I was thinking about how to optimize the reducer, but there was no good way. I have a built-in Reducer of setState that deals exclusively with such actions that are just assignments.
It ended up like this:
import xredux from 'xredux';
import { fetchUserInfo } from 'services/api';
const { actions } = xredux;
xredux.model({
namespace: 'user'.initialState: {
getUserInfoStart: false.getUserInfoError: null.userInfo: null,},reducers: {},effects: {
async getUserInfo (action, dispatch, getState) {
let userInfo = null;
// fetch start
actions.user.setState({
getUserInfoStart: true});try {
userInfo = await fetchUserInfo();
// fetch success
actions.user.setState({
getUserInfoStart: false,
userInfo,
});
} catch (e) {
// fetch error
actions.user.setState({
getUserInfoError: e, }); }}}});Copy the code
At present, I am satisfied with this plan, and I have practiced it in the project. It is really simple and easy to understand when written. I wonder if you have a better way.
Anemic component/hyperemic component
With Redux, the state data in the application should be stored in the Store. Can components have their own state? Now there are two views:
- All states should be there
Store
All components are pure presentation components. - Components can have some states of their own, and others of their own
Store
Hosting.
These two are respectively corresponding to the anemia component and the congestion component, the difference is whether the component has its own logic, or just pure display. I don’t think there is a right or wrong way to argue about this.
In theory, of course, it is good to say that the anaemic component is good, because it ensures that the data is managed in one place, but the price may be heavy. Using this way, there will often be a feeling of wanting to die behind, a feeling of wanting to go back but not wanting to give up. In fact, there is no need to be so persistent.
There are some states that are only related to the component and are managed by the component. There are some states that need to be shared and managed by the Store. There are even some states that are managed by the component because the page is too simple to use the data flow framework.
conclusion
In React development, it is inevitable to encounter data flow problems. There is no perfect solution for how to deal with them gracefully. There are various methods in the community, so you can think more about why you do this.
If you want to learn more about how xredux works with React, you can use RIS to initialize a Standard application. RIS: New Options for Creating React applications
The resources
- Exploration of data flow schemes for single page applications