How do I “transform” Redux step by step

Time:2021-1-20

It has been more than half a year since Vue was changed to react + Redux for development. Generally speaking, the experience is very good. It is not convenient to abstract various logic and business components. High level components, onion model and so on have brought me a lot of improvement in programming ideas. But in the process of using Redux development, I still don’t feel very comfortable. This article will explain how I “transform” Redux step by step to meet the needs of individual and team development.
Examples and results of the process are presented in theeasy-reduxWelcome to star.
Original link

problem

In the process of using Redux development, we gradually found that although we have separated UI components and business components as far as possible to ensure the reusability of reduction operations,
But we still spend a lot of time writing almost the same code. In particular, we hope to adhere to a principle in our group: all operations and status changes will be executed by action as far as possible, which is convenient for us to locate the problem. When I saw the saying “don’t use Redux, don’t want to work overtime” in a large front-end communication group, I had to sigh that I need to make some efforts to solve my current problems.

yes,Redux is too complicated for me

For a simple operation, we need to complete the following steps:

1. Define action

export const CHANGE_CONDITION = 'CHANGE_CONDITION'

2. Define a corresponding action creation function

export const changeCondition = condition => ({
  type: CHANGE_CONDITION,
  condition
})

3. Introduce action, define reducer, and change the object in the complex switch statement

import { CHANGE_CONDITION } from '@actions'

const condition = (state = initCondition, action) => {
  switch(action.type) {
    case CHANGE_CONDITION:
      return ...
    default:
      return state
  }
}

4. When necessary, the action creation function is introduced and the corresponding state is connected

import { changeCondition } from 'actions'
@connect(...)

I just want to do a simple state modification!

We may say that this splitting can ensure the standardization of our whole project and enhance the predictability and error location ability of the business.
But with the continuous expansion of the project, every page has a pile of actions that I need to add. It’s really a headache.

Moreover, for the modification of the request, we often need to split the action into three states: start, success and failed. In the reducer, we need to make three modifications. And often
In view of these modifications, our processing is basically the same: update loading status, update data, update error, etc.

Therefore, how to “simplify” Redux in terms of ensuring its design principles and project standardization is the problem I need to solve here.

Using middleware to simplify requests

I’ve written an article about request processing beforeGracefully reduce Redux request template codeBy encapsulating a Redux middlewarereact-fetch-middleware
To optimize the request code.

The general idea is as follows:

1. The content returned by the action creation function is an object containing the request information and contains three actions to be distributed. These three actions can be created through the action creator

import { actionCreator } from 'redux-data-fetch-middleware'

// create action types
export const actionTypes = actionCreator('GET_USER_LIST')

export const getUserList = params => ({
  url: '/api/userList',
  params: params,
  types: actionTypes,
  // handle result
  handleResult: res => res.data.list,
  // handle error
  handleError: ...
})

2. In the Redux middleware, the action in the above format is processed. First, the request is made, and the action started by the request is distributed,
When the request succeeds or fails, the corresponding action is distributed respectively

const applyFetchMiddleware = (
  fetchMethod = fetch,
  handleResponse = val => val,
  handleErrorTotal = error => error
) =>
  store => next => action => {
    //Determine the format of action
    if (!action.url || !Array.isArray(action.types)) {
      return next(action)
    }
    //Get the three actions passed in
    const [ START, SUCCESS, FAILED ] = action.types

    //Distribute action in different states and pass in loading and error states
    next({
      type: START,
      loading: true,
      ...action
    })
    return fetchMethod(url, params)
      .then(ret => {
        next({
          type: SUCCESS,
          loading: false,
          payload: handleResult(ret)
        })
      })
      .catch(error => {
        next({
          type: FAILED,
          loading: false,
          error: handleError(error)
        })
      })
  }

3. The corresponding default processing of reducercreator is performed automatically in the function created by reducercreator, and the secondary processing mechanism is provided

const [ GET, GET_SUCCESS, GET_FAILED ] = actionTypes

//The three distributed actions are automatically processed here
const fetchedUserList = reducerCreator(actionTypes)

const userList = (state = {
  list: []
}, action => {
  //Secondary treatment
  switch(action.type) {
    case GET_SUCCESS:
      return {
        ...state,
        action.payload
      }
  }
})
export default combineReducers({
  userList: fetchedUserList(userList)
})

Further, Redux API is simplified

After the previous simplification of the request, we can greatly simplify the request template code without changing the Redux principles and writing habits.
For ordinary data processing, can we go further?

Nice to see this library:Rematch
, which greatly simplifies Redux API.

However, some functions and improvements are not what we want, so I only explain the functions and improvements I need, and implement them in my own way. Let’s take a step-by-step look
The problems we need to solve and how to solve them.

1. Lengthy switch statement

For reducer, we don’t want to repeatedly refer to the defined actions, and remove the lengthy switch judgment. In fact, we can invert and split it, define each action as a standardized reducer, and process the state

const counter = {
  state: 1,
  reducers: {
    add: (state, payload) => state + payload,
    sub: (state, payload) => state - payload
  }
}

2. Complex action creation function

Remove the previous action and action creation function, directly process the data in actions, and match with the corresponding reducer

export const addNum = num => dispatch => dispatch('/counter/add', num)

We will see that when matching with reducer, we use the way of ‘/ counter / add’ as the namespace,
The purpose is to ensure that the action and its reducer are one-to-one corresponding while ensuring its intuitiveness.

We can set the namespace through the enhanced combinereducer

const counter1 = {
  ...
}
const counter2 = {
  ...
}

const counters = combinceReducer({
  counter1,
  counter2
})

const list = {
  ...
}
//Set the root namespace of the large reducer
export default combinceReducer({
  counters,
  list
}, '/test')

//We can visit it in this way
dispatch('/test/counters/counter1/add')

Don’t forget to ask

For these asynchronous actions, we can refer to our previous modification, the dispatch object

export const getList = params => dispatch => {
  return dispatch({
    //Corresponding to the namespace we want to dispatch
    action: '/list/getList',
    url: '/api/getList',
    params,
    handleResponse: res => res.data.list,
    handleError: error => error
  })
}

At the same time, we can simply process it in the reducer, and we can still process the default three states

const list = {
  //Define the reducer header, which will automatically change to GetList (start request), getlistsuccess, getlistfailed
  //And carry out the default processing such as loading
  fetch: 'getList'
  state: {
    list: []
  },
  reducers: {
    //Secondary treatment
    getListSuccess: (state, payload) => ({
      ...state,
      list: payload
    })
  }
}

Integration with projects

We will see that we have greatly simplified the Redux API, but still maintain the original structure. The purpose is as follows

  1. Still follow the default principle to ensure the standardization of the project
  2. The match between action and reducer is ensured by convention and namespace
  3. The bottom layer is still implemented in Redux, which is just syntax sugar
  4. Ensure compatibility with old projects

The original data has changed as follows:
How do I

Therefore, we do secondary encapsulation on the basis of redux. We still guarantee the original Redux data stream, ensure the traceability of data, and enhance the predictability and error location ability of business. This can greatly ensure the compatibility with the old project, so what we need to do is to transform the action and reducer

1. Combineceducer returns the original format reducer

We use the new combineceducer to transform the new format into the previous format, and save the namespace of each reducer and its corresponding action.

The code is simple

//Get the methods in the reducers
const actionNames = Object.keys(reducers)
const resultActions = actionNames.map(action => {
  const childNamespace = `${namespace}/${action}`
  //Save the action into the namespace
  Namespace.setActionByNamespace(childNamespace)
  return {
    name: Namespace.toAction(childNamespace),
    fn: reducers[action]
  }
})

//Return to default format
return (state = inititalState, action) => {
  //Query the method in the new reducer corresponding to action
  const actionFn = resultActions.find(cur => cur.name === action.type)
  if (actionFn) {
    return actionFn.fn && actionFn.fn(state, action.payload)
  }
  return state
}

2. Create a new action function, and finally dispatch the original action

We need to convert a function in this format into a function in this format

count => dispatch => dispatch('/count/add', count)

//or
params => dispatch => { dispatch('/count/add', 1), dispatch('/count/sub', 2) }

//Results
count => ({ type: 'count_add', payload: count })

The processing here is more complicated. In fact, it is to transform our dispatch function

action => params => (dispatch, getstate) => {
  const retDispatch = (namespace, payload) => {
    return dispatch({
      type: Namespace.get(namespace),
      payload
    })
  }
  return action(params)(retDispatch, getstate)
}

summary

Through the transformation of Redux API, it is equivalent to secondary encapsulation, which has greatly simplified the current template code in the project, and is used smoothly in the project.

In view of the whole process, there are several areas that can be improved

  • The transformation process of actions is handled by middleware
  • The performance problem is equivalent to one more layer of transformation, but it has little impact at present
  • Reduce, action reuse

If you are interested, you are welcome to attach GitHubeasy-redux

Recommended Today

Video compatibility in wechat

1. In line properties of video tag SRC: URL of video Poster: Video cover, no picture displayed when playing Preload: preload Autoplay: autoplay Loop: loop playback Controls: browser’s own control bar Width: video width Height: video height style=”object-fit:fill” /Adding this style will make the Android / Web video full screen in wechat. If it is […]