React series — higher order application of Redux

Time:2021-9-19

Reference: deep into react technology stack


Higher order reducer

High order functions refer to functions that take functions as parameters or return values. High order reducers refer to functions that take reducers as parameters or return values.

In the Redux architecture, reducer is a pure function whose responsibility is to calculate a new state according to the previous state and action. In complex applications, combinereducers provided by Redux allows us to split the top-level reducers into multiple small reducers and operate different parts of the state tree independently. In an application, many small granularity reducers often have a lot of duplicate logic. How to extract common logic and reduce code redundancy for these reducers? In this case, using higher-order reducer is a better solution.

Reducer reuse

When we split the top-level reduce into several small reducers, we will certainly encounter the problem of reusing reducers. For example, there are two modules a and B, whose UI parts are similar. At this time, they can be distinguished by configuring different props. In this case, can modules a and B share a reducer? The answer is No. Let’s start with a simple reducer:

const LOAD_DATA = 'LOAD_DATA';
const initialState = { ... };

function loadData() {
    return {
        type: LOAD_DATA,
        ...
    };
}

function reducer(state = initialState, action) {
    switch(action.type) {
        case LOAD_DATA:
            return {
                ...state,
                data: action.payload
            };
        default:
            return state;
    }
}

If we bind this reducer to two different modules a and B, the problem will be that when module a calls loaddata to distribute the corresponding action, the reducers of a and B will process the action, and then the contents of a and B are completely consistent.

Here, we must realize that in an application, actiontypes between different modules must be globally unique.

Therefore, another way to solve the only problem of actiontype is to add a prefix:

function generateReducer(prefix, state) {
    const LOAD_DATA = prefix + 'LOAD_DATA';
    
    const initialState = { ...state, ...};
    
    return function reducer(state = initialState, action) {
        switch(action.type) {
            case LOAD_DATA:
                return {
                    ...state,
                    data: action.payload
                };
            default:
                return state;
        }
    }
}

In this way, as long as modules a and B call generatereducer to generate corresponding reducers respectively, the problem of reducer reuse can be solved. For prefix, we can decide according to our own project structure, such as ${page name}_$ {module name}. As long as global uniqueness can be guaranteed, it can be written as a prefix.

Reducer enhancements

In addition to solving the reuse problem, another important role of high-order reducer is to enhance the original reducer. Redux Undo is a typical example of using high-level reducers to enhance reducers. Its main function is to turn any reducer into a new reducer that can perform undo and redo. Let’s take a look at its core code implementation:

function undoable(reducer) {
    const initialState = {
        //Record past state
        past: [],
        //Call reducer with an empty action to generate the initial value of the current value
        present: reducer(undefined, {}),
        //Record subsequent States
        future: []
    };
    
    return function(state = initialState, action) {
        const { past, present, future } = state;
        
        switch(action.type) {
            case '@@redux-undo/UNDO':
                const previous = past[past.length - 1];
                const newPast = past.slice(0, past.length - 1);
                
                return {
                    past: newPast,
                    present: previous,
                    future: [ present, ...future ]
                };
            case '@@redux-undo/REDO':
                const next = future[0];
                const newFuture = future.slice(1);
                
                return {
                    past: [ ...past, present ],
                    present: next,
                    future: newFuture
                };
            default:
                //Delegate other actions to the original reducer for processing
                const newPresent = reducer(present, action);
                
                if(present === newPresent) {
                    return state;
                }
                
                return {
                    past: [ ...past, present ],
                    present: newPresent,
                    future: []
                };
        }
    };
}

With this high-level reducer, you can encapsulate any reducer:

import { createStore } from 'redux';

function todos(state = [], action) {
    switch(action.type) {
        case: 'ADD_TODO':
        // ...
    }
}

const undoableTodos = undoable(todos);
const store = createStore(undoableTodos);

store.dispatch({
    type: 'ADD_TODO',
    text: 'Use Redux'
});

store.dispatch({
    type: 'ADD_TODO',
    text: 'Implement Undo'
});

store.dispatch({
    type: '@@redux-undo/UNDO'
});

Looking at the implementation code of high-level reducer undoable, we can find that high-level reducer mainly enhances reducer through the following three points:

  • Be able to handle additional actions;
  • Be able to maintain more states;
  • Delegate the actions that cannot be processed to the original reducer for processing.

Redux and forms

The feature of react one-way binding greatly improves the execution efficiency of applications, but compared with the simple and easy-to-use two-way binding, one-way binding is really unable to handle forms and other interactions. Specific to react application, one-way binding means that you need to manually provide onchange callback function for each form control, and initialize their state in this.state. Moreover, an experience friendly form also needs to have clear error status and error information, and even some input items need asynchronous verification function. In other words, a valid field in the form needs at least 2 ~ 3 local states.

In angular.js, form related problems have been well solved at the framework level. So, is there any good solution for react + Redux applications?

Let’s answer this question from two aspects: for simple form applications, in order to reduce redundant code, you can use Redux form utils, a tool library, which can use the characteristics of high-level components to provide necessary values such as value and onchange for each field of the form without creating it manually; For complex forms, you can use Redux form. Although it is also based on the principle of high-order components, if Redux form utils is a fruit knife, then Redux form is a multifunctional Swiss Army knife. In addition to providing necessary fields for forms, Redux form can also realize complex functions such as synchronous verification, asynchronous verification and even nested forms.

Use Redux form utils to reduce redundant code for creating forms

Before learning about Redux form utils, let’s take a look at how to process forms using native react:

import React, { Component } from 'react';

class Form extends Component {
    constructor(props) {
        super(props);
        
        this.handleChangeAddress = this.handleChangeAddress.bind(this);
        this.handleChangeGender = this.handleChangeGender.bind(this);
        
        this.state = {
            name: '',
            address: '',
            gender: ''
        };
    }
    
    handleChangeName(e) {
        this.setState({
            name: e.target.value
        });
    }
    
    handleChangeAddress(e) {
        this.setState({
            address: e.target.value
        });
    }
    
    handleChangeGender(e) {
        this.setState({
            gender: e.target.value
        });
    }
    
    render() {
        const { name, address, gender } = this.state;
        return (
            <form className="form">
              <input name="name" value={name} onChange={this.handleChangeName} />
              <input name="address" value={address} onChange={this.handleChangeAddress} />
              <select name="gender" value={gender} onChange={this.handleChangeGender}>    
                <option value="male" />
                <option value="female" />
              </select>
            </form>
        );
    };
}

As you can see, although there are only three fields in our form, there are already a lot of redundant code. If you need to add verification and other functions, the processing code corresponding to this form will be more expanded.

After careful analysis of the code implementation of the form, we find that almost all onchange processor logic is very similar, except that the form fields need to be changed. For some complex input controls, such as encapsulating a TimePicker component, the callback name may not be onchange, but onselect. Similarly, the parameter provided in the onselect callback may not be a composite event of react, but a specific value. By analyzing the possible input and output of form controls, we will reduce the redundant code when Redux processes form applications by using Redux form utils:

// components/MyForm.js
import React, { Component } from 'react';
import { createForm } from 'redux-form-utils';

@createForm({
    form: 'my-form',
    fields: ['name', 'address', 'gender']
})

class Form extends Component {
    render(){
        const { name, address, gender } = this.props.fields;
        return (
          <form className="form">
            <input name="name" value={...name} />
            <input name="address" value={...address} />
            <select {...gender}>    
              <option value="male" />
              <option value="female" />
            </select>
          </form>
        );
    }
}

You can see that the amount of code for forms with the same function is reduced by more than half.

Redux form utils provides two convenient tool functions – createform (config) and bindredux (config). The former can be used as a decorator to import form configuration and automatically add form related props to the decorated components; The latter can generate reducer, initialstate and actioncreator related to Redux applications.

Let’s take a look at how to integrate Redux form utils in reducer:

// reducer/MyForm.js
import { bindRedux } from 'redux-form-utils';

const { state: formState, reducer: formReducer } = bindRedux({
    form: 'my-form',
    fields: ['name', 'address', 'gender'],
});

const initialState = {
    foo: 1,
    bar: 2,
    ...formState
};

function myReducer(state = initialState, action) {
    switch(action.type) {
        case 'MY_ACTION': {
            // ...
        }
        
        default:
            return formReducer(state, action);
    }
}

We pass the same configuration to the bindredux method, obtain the corresponding reducer and initial state formstate of the form, and integrate these contents into the reducer.

After the createform and bindredux functions are completed, a Redux based form application is completed. In order to make the subsequent modification of forms more flexible, it is recommended to save the configuration file separately and introduce the corresponding configuration file in the component and reducer respectively.

Using Redux form to complete asynchronous form validation

Redux form utils provides us with the most basic functions to implement forms. However, in order to make the experience of filling in forms more friendly, we should do some basic form verification before submitting data to the server, such as filling in fields that cannot be empty. To implement complex form functions such as verification, you need to use Redux form.

In terms of use and configuration, there are not many differences between Redux form and Redux form utils. The only difference is that Redux form needs to mount an independent node in the state tree of Redux application. This means that the fields in all forms created with Redux form will be in a fixed position. For example, state.form.myform or state.form.myootherform are mounted under state.form:

import { createStore, combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';

const reducers = {
    //Other reducers
    //All form related reducers are attached to the form
    form: formReducer
};

const reducer = combineReducers(reducers);
const store = createStore(reducer);

After completing the basic configuration, let’s see how Redux form can help us complete the form verification function:

import React, { Component } from 'react';
import { reduxForm } from 'redux-form';

function validate(values) {
    if(values.name == null || values.name === '') {
        return {
            Name: 'please fill in the name'
        };
    }
}

@reduxForm({
    form: 'my-form',
    fields: ['name', 'address', 'gender'],
    validate
});

class Form extends Component {
    render(){
        const { name, address, gender } = this.props.fields;
        return (
          <form className="form">
            <input name="name" value={...name} />
            { name.error && <span>{name.error}</span> }
            <input name="address" value={...address} />
            <select {...gender}>    
              <option value="male" />
              <option value="female" />
            </select>
            < button type = "submit" > submit < / button >
          </form>
        );
    }
}

In the above form, we verified the non null name field when submitting, and added logic to display corresponding errors in the render method of the form component. Trigger verification, re rendering, form purity judgment and other processes are encapsulated by Redux form and transparent to users.

It can be seen that using Redux form to verify forms is very simple and easy to use, which largely fills the deficiency of Redux application in processing form applications at the framework level.


Reference: deep into react technology stack