Recently, we are making our own component library in the project. As for forms, how to achieve a simpler Form scheme is a problem we have been discussing. We used to use ant-Design Form Form in the project before, and we think it is quite easy to use.

The React community has an incredible number of wheels:

  • Fly Ice Form solution – Ice FormBinder
  • React implements a highly concise Form component
  • NoForm – A better form solution
  • High-performance forms solution for complex scenarios – UForm
  • redux-form
  • final-form
  • Form Component Design Path – High ease of use – Fusion Form
  • .

The above form scheme mainly focuses on the following points:

  1. More convenient to do data collection, no handwritingvalueonChangeSome forms are increment functions (ant-design) or containers (FormBinder.FusionEtc.), register the child componentsvalue.onChangeSome are customFeildComponents (UForm), handles the related logic internally
  2. More efficient rendering, such as distributed field state management
  3. Simple, reduce the cost of learning
  4. Dynamic form rendering

About data collection and rendering

For form data collection, see bidirectional data binding. Here’s a discussion of bidirectional data binding:

  • What’s the reason react didn’t implement two-way binding?

And articles on implementing two-way data binding:

  • React bidirectional sugar binding using the Babel plugin
  • React add two-way data binding
  • Add two-way data binding for React

One is data collection and the other is rendering, which is called bi-directional data binding, summed up in three ways:

  1. You can convert code at compile time, inject assignment statements and component data listening methods, and this looks like a nice thing to write yourselfBabelThe plug-in
  2. Modify the virtual DOM at run time, for exampleant-design,iceAnd so on, really also pretty easy to use, the above listed article can be read, very meaningful
  3. handwrittenvalueonChangeUnless you only have one form in your system…

Set a goal first

When there are many forms in the system, it is impossible to tie value and onChange by hand. Even if ant-Design, ICE, etc., need to add additional functions or containers, so the goal is as follows:

import {Form,Input} form 'form';

export default class FormDemo extends Component<any, any> {
    public state = {
        value: {
            name: ' ',
            school: ' ',
        },
    }
    public onFormChange = (value) => {
        console.log(value);
        this.setState({
            value,
        });
    }
    public onFormSubmit = () => {
        // console.log('submit')
    }
    public render() {
        const me = this;
        const {
            value,
        } = me.state;
        return (
            <Form
                value={value}
                enableDomCache={false}
                onChange={me.onFormChange}
                onSubmit={me.onFormSubmit}
            >
                <div className="container">
                    <input
                        className="biz-input"
                        data-name="name"
                        data-rules={[{ max: 10, message: 'Maximum length 10'}}]type="text"
                    />
                    <Input
                        data-name="school"
                        data-rules={[{ max: 10, message: 'Maximum length 10'}}]type="text"
                    />
                    <Button type="primary" htmlType="submit"</div> </Form>)}}Copy the code
  1. Simple, close to the original, low learning cost
  2. Components are compatible with all implementationsvalue,onChangeComponents such asant-designForm component of
  3. Form validation, useant-designDesign, useasync-validatorLibrary to do

We can see that we are fans of Ant-Design. Frankly speaking, the big guys’ solutions are simple enough. Ant-design is the pioneer, Ice, Fusion and many other counterparts of Ant-Design, trying to give more concise solutions, and they are very simple. UForm uses a syntax similar to JSON Schema(JSchema) to write forms. UForm and final-form emphasize distributed Field management and high performance. However, these two schemes have certain learning costs. The implementation is naturally complex.

However, when I say our implementation, people will probably laugh because our implementation is so simple (facepap), simple to doubt life.

implementation

To achieve the above goal, it is obvious that the list of articles at the beginning of this article has been implemented by someone, but you need to add a new Babel plugin, I don’t know if you like it.

Our implementation uses runtime modification of the virtual DOM, which is not done at compile time. However, we do not add additional functions or containers to the component. We just use the Form container to implement it. Will there be any additional performance overhead?

Implement it first, then optimize it.

First of all, all sub-virtual DOM nodes need to be traversed, depth first, to determine whether the node has data-name or name attributes, if so, to attach value and onChange attributes to the component, such as checkbox, radio, select and other components, special processing.

Bind value and onChange core code (with deletion) as follows:

public bindEvent(value, childList) {
    const me = this;
    if(! childList || React.Children.count(childList) === 0) {return;
    }
    React.Children.forEach(childList, (child) => {
        if(! child.props) {return;
        }
        const { children, onChange } = child.props;
        const bind = child.props['data-name'];
        const rules = child.props['data-rules']; Const valuePropName = me.getValuePropName(child);if (bind) {
            child.props[valuePropName] = value[bind];
            if(! onChange) { child.props.onChange = me.onFieldChange.bind(me,bind, valuePropName);
            }
        }
        me.bindEvent(value, children);
    });
}
Copy the code

OnFieldChange code:

public onFieldChange(fieldName, valuePropName, e) {
    const me = this;
    const {
        onChange = () => null,
        onFieldChange = () => null,
    } = me.props;
    let value;
    if (e.target) {
        value = e.target[valuePropName];
    } else{ value = e; } me.updateValue(fieldName, value, () => { onFieldChange(e); const allValues = me.state.formData.value; onChange(allValues); })}Copy the code

Even if the above code does what we want it to do, we don’t have to manually bind value and onChange.

Presentation:

The async-Validator library is used to implement form validation. The configuration is the same as that of Ant-Design. To display validation error messages, the FormItem container is added and used in a similar way to Ant-Design.

The React Context API is used to implement FormItem. See the React Context API for more details.

As with Ant-Design, components that implement value and onChange interfaces can be used here, not just native HTML components.

Concerns about performance

Using the above code to achieve what we want to achieve, however, there is still a question mark: is there a performance problem with deep traversal of child nodes every time we render?

The answer is: very little

By test, form controls up to 1000 will feel no difference. The Diff algorithm is also expensive for React.

However, in order to improve performance, we have optimized it to include virtual DOM caching.

If we cache the virtual DOM created after the first rendering, we don’t need to recreate it for the second rendering, nor do we need to deeply traverse the nodes to add value and onChange, but in order to update the value, we need to get the reference with the data-name node. If you want to update a component with a data-name value as its key, you can update the component’s virtual DOM property directly. If you want to update a component with a data-name value as its key, you can update the component’s virtual DOM property directly.

With the above optimization, performance can be doubled.

However, virtual DOM caching is not possible if components in the form are dynamically displayed or hidden, so we provide a property enableDomCache, which can be a Boolean value or a function that takes the previous form value and allows the user to compare the current value to the previous value to determine whether to use the cache for the next rendering. However, it should only be considered for performance issues, most of which are not. EnableDomCache is set to false by default,

Example:

import {Form} form 'form';

export default class FormDemo extends Component<any, any> {
    state = {
        value: {
            name: ' ',
            school: ' ',
        },
    }
    onFormChange = (value) => {
        this.setState({
            value,
        });
    }
    onFormSubmit = () => {
        // console.log('submit')}enableDomCache=(preValue)=>{
        const me=this;
        const {
            value,
        } = me.state;

        if(preValue.showSchool! ==value.showSchool){return false;
        }

        return true;
    }
    render(){
        const me=this;
        const {
            value,
        } = me.state;
        return (
            <Form 
                value={value}
                enableDomCache={me.enableDomCache}
                onChange={me.onFormChange} 
                onSubmit={me.onFormSubmit}
            >
                <input 
                    data-name={`name`} 
                    data-rules={[ { max: 3, message: 'Maximum length 3',}}]type="text" 
                />
                {
                    value.showSchool&&(
                        <input 
                            data-name={`school`} 
                            data-rules={[ { max: 3, message: 'Maximum length 3',}}]type="text" 
                        />
                    )
                }
            </Form>
        )
    }
}

Copy the code

Thoughts on distributed management of fields

If every field change on a form causes the entire form to be re-rendered, it’s not perfect, so the idea of distributed field management comes up.

Consider adding a Redux store to the form. Each form item component subscribes to the store and maintains its own data state. Form items do not affect each other, so the form fields are distributed and the store stores the latest form data.

Most of the time, however, users won’t notice the difference even if they re-render. Ant-design is used to re-render the virtual DOM. React diff does not render the DOM completely.

Use shouldComponentUpdate for heavy components in a form. If you have used Redux, use shouldComponentUpdate. Connect high-order components are compared internally to control whether the components are updated.

Also, the influence of the controlled components and uncontrolled components, if the form itself is controlled components, so its properties change, must cause itself to render, so if you want to better performance, it is best to use a controlled component patterns, this still depends on the specific needs, because at present most of the time, can choose the global state, Uncontrolled components do not update because of external state changes, so there is the possibility of UI state and global state inconsistency. If changes to form data are only controlled by the form itself, it is safe to use uncontrolled mode.

In addition, shouldComponentUpdate can be used to optimize the component itself, whether controlled or uncontrolled.

About form nesting

From the discussion in the previous article, it’s easy to remember that forms can be nested as long as the form itself conforms to the Value onChange interface, as follows:

import {Form,Input} form 'form';

export default class FormDemo extends Component {
    render(){
        const me=this;
        const {
            value,
        } = me.state;
        return (
            <Form value={value} onChange={me.onFormChange} onSubmit={me.onFormSubmit} >
                <input  data-name="name" type="text" />
                <Input  data-name="school" type="text" />
                <Form name="children1">
                    <input  data-name="name" type="text" />
                    <Input  data-name="school" type="text" />
                    <Form name="children2">
                        <input  data-name="name" type="text" />
                        <Input  data-name="school" type="text" />
                        <Form name="children3">
                            <input  data-name="name" type="text" />
                            <Input  data-name="school" type="text" />
                        </Form>
                         <Form name="children4">
                            <input  data-name="name" type="text" />
                            <Input  data-name="school" type="text" />
                        </Form>
                    </Form>
                </Form>
            </Form>
        )
    }
}
Copy the code

Presentation:

Although form nesting is implemented, there are problems with this implementation. Data changes of subforms will be passed up the onChange method step by step. When the data volume is large and the nesting level is deep, there will be performance problems.

Nested form data changes demo:

It is better to be similar to the distributed management of fields, where each Form is only responsible for its own rendering and does not cause other forms to re-render. To improve performance, we have optimized the FormGroup container, which can iterate through Form nodes and build reference relationships between Form nodes. Create a unique ID for each Form. The state of all forms is managed by the state of the FormGroup.

When the state is flattened, changes to each form will only cause itself to be re-rendered and will not affect other forms.

Presentation:

However, the above optimization is limited to the uncontrolled state, because in the controlled state, the external attribute still needs to pass the value to the FormGroup, while the internal value and the value structure passed by the attribute are inconsistent. One is a flat structure and the other is a tree structure, and the conditions for converting the tree structure to a flat structure are not sufficient. The value conversion cannot be done because the nested structure of the form is not known.

In short, simple tree structures can be done without formGroups. More complex, consider using FormGroup and setting defaultValue instead of value to use uncontrolled modes.

The last

This paper tries to build a more concise form solutions, the method of using deep traversal child node value for child component assignment and registered onChange event, more close to the original form of writing, more concise, and depth with the method of virtual cache DOM traversal children this way has carried on the performance optimization, trying to realize nested form, And use FormGroup container data update flat, I don’t know if you have harvest.

The last

This looks a lot like Vue, doesn’t it? React doesn’t have as many commands as Vue does, so there are a lot of ways to simplify the form section. However, the above approach is similar to ast parsing.

Then write the following code:

<Template>
    <div v-if={true}>
        {name}
    </div>
    <div v-show={true}>
        <div/>
    </div>
</Template>

Copy the code

The article is for reference only, to provide solutions to the problem, welcome to comment, thank you!