4

What is a form, what can he do?

Forms are a familiar concept for us that can no longer be familiar. They are commonly used in various information input places. Through the form, users can submit data, modify data, or implement other more complex interactions. Today we are here to talk about how the various Form solutions used in development have evolved. According to the definition on Wikipedia: The function of web forms (English: WebForms) is to send the data entered by the user to the server for processing. Baidu Encyclopedia divides the form into form tags, form fields and form buttons, which are used to transmit data to the CGI script on the server or cancel input. You can also use the form buttons to control other processing tasks that define processing scripts.

The author's understanding of the role of the form is: the form provides the user with the ability to interact with the server through the UI view, and at the same time provides the ability to verify, error handling, and display different types of data. In middle and back-end projects, forms have become an important form of interaction. Users fill in data, convert them into JSON data structure through the form, and interact with the server.

Out of the original, towards React

Although our title is the React form solution, we still start with the HTML native form. First of all, the Form tag in HTML will obtain the corresponding user input according to the name of each form element, and the Form will automatically handle the submit event (the submit event is usually triggered by the input or button element with type=submit). The Form.novalidate attribute indicates that the form does not need to be validated when the form is submitted. If this attribute is not declared (the form needs to be validated). The default method is GET, and the default action is the current url. event.target.elements will return all form elements.

Next, let's take a look at React's implementation of the form. In fact, in a sense, the other form solutions introduced later in this article also belong to the encapsulation and expansion of the React solution.

In React, HTML form elements work differently from other DOM elements. This is because form elements usually maintain some internal state.

React starts like this in the form part, which leads to the first implementation: controlled components; of course, as we all know, there is also a second implementation: uncontrolled components. The biggest difference between a controlled component and an uncontrolled component is the maintenance of the internal state.

The controlled component does not maintain its own internal state. Props and callback functions are provided externally. The controlled component is controlled through props, and updates are notified to the parent component through onChange. Here is an official example:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('提交的名字: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          名字:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}

For uncontrolled components, the form data will be processed by the DOM node, and the DOM will be manipulated through Ref to obtain the value of the form and then be manipulated. The same example is implemented in an uncontrolled way:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

From the official attitude, controlled components will be more respected (uncontrolled components are placed in the advanced guidelines). Developers have a lot of comparisons with them. Below is a widely circulated comparison chart, but this picture is not completely accurate. For example, you may think that uncontrolled components can also achieve instant forms by binding events. In terms of verification and other characteristics, the author sees that controlled and uncontrolled are more of a design specification. It mainly depends on whether the parent component has control over the value of the child component. If there is control, the controlled component is uncontrolled.

image.png

After scolding seniors, it became an official recommendation

But the official solution is more rudimentary, and it does not provide the ability to verify, access fields, and process form submission. Therefore, the official also recommends the third-party solution Formik at the end of the controlled component chapter.

As an official form solution, Formik first clarified the main role of Formik at the beginning of the document: to help developers obtain form status, handle validation, error messages, and process form submissions. Then pointed out the shortcomings of the predecessor Redux-Form, the shortcomings can be summarized as: The form state is essentially short-lived and local, so tracking it in Redux (or any type of Flux library) is unnecessary; and one is not used The Redux project is unnecessary because it uses Redux-Form to introduce Redux; and Redux-Form will call your entire top-level Redux reducer when each form item changes. As Redux applications grow, performance will get worse and worse.

The Redux-Form process is shown in the figure below. Redux-Form uses formReducer to capture application operations to notify how to update the Redux store. When the user operates the input, the Focus action is distributed to the Action Creators, the Redux actions created by the Action Creators have been bound to the Dispatcher, and then the formReducer updates the corresponding state slice, and finally the state is passed back to the text input box.

image.png

But because all operations will trigger the Redux update process, this is too heavy (and the version before Redux-Form V6 is fully updated, and the performance is worrying). Based on the above shortcomings, Formik abandoned Redux and chose to maintain the state of the form itself. First look at an official example of Formik:

// Render Prop
 import React from 'react';
 import { Formik, Form, Field, ErrorMessage } from 'formik';
 
 const Basic = () => (
   <div>
     <h1>Any place in your app!</h1>
     <Formik
       initialValues={{ email: '', password: '' }}
       validate={values => {
         const errors = {};
         if (!values.email) {
           errors.email = 'Required';
         } else if (
           !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
         ) {
           errors.email = 'Invalid email address';
         }
         return errors;
       }}
       onSubmit={(values, { setSubmitting }) => {
         setTimeout(() => {
           alert(JSON.stringify(values, null, 2));
           setSubmitting(false);
         }, 400);
       }}
     >
       {({ isSubmitting }) => (
         <Form>
           <Field type="email" name="email" />
           <ErrorMessage name="email" component="div" />
           <Field type="password" name="password" />
           <ErrorMessage name="password" component="div" />
           <button type="submit" disabled={isSubmitting}>
             Submit
           </button>
         </Form>
       )}
     </Formik>
   </div>
 );
 
 export default Basic;

Formik's main design thinking is to help developers reduce template code. Formik is equipped with some additional components: <Form />, <Field /> and <ErrorMessage /> to track values ​​or handle error messages. These components use React Context to get the state and methods of the parent <Formik />.​

Formik also handles the submission form. Calling (e) on line 794 or submitForm on line 729 in 16139db345c214 source code (both methods are provided as attributes in Formik) will trigger the form submission. When calling one of these methods, Formik first touches all fields, and then sets isSubmitting to true. Then perform verification, set isValidating to true, run all field-level checksum validationSchema asynchronously, and deeply merge the execution results to determine whether there is an error. If there is an error, cancel the submission, set isValidating to false, set the error message, and set isSubmitting to false. If there is no error, set isValidating to false and execute the submission.

Formik also uses FastField for performance optimization, but in fact, after reading the FastField.tsx source code 75 lines, you will find that it is simply a few key status values, errors, touched in the form combined with the length of props and isSubmitting And so on, the key fields are shallowly compared through shouldComponentUpdate, which has no effect on some complex scenes such as linkage transformation.

image.png

In summary, Formik got rid of Redux. In addition, it allows developers to write less template code and optimize performance to a certain extent, so it is not surprising to be officially recommended. But in terms of code architecture, it does not support finer update granularity, and there is still a lot of room for improvement.

The Redux-Form author’s later new work, React-final-form, has regained its performance in terms of performance. Now the Redux-Form documentation also reminds developers to give priority to React-final-form at the very beginning. The only scenario that is worth using Redux-Form is if you need to tightly couple form data with Redux, especially if you need to subscribe to it but do not plan to modify it from the application part. But before studying React-final-form, let's take a look at how Antd from Ant does it.

Antd 3.x-Go forward in groping

First of all, let's start with Antd 3.x, or first look at the official example:

import { Form, Icon, Input, Button } from 'antd';

function hasErrors(fieldsError) {
  return Object.keys(fieldsError).some(field => fieldsError[field]);
}

class HorizontalLoginForm extends React.Component {
  componentDidMount() {
    // To disable submit button at the beginning.
    this.props.form.validateFields();
  }

  handleSubmit = e => {
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Received values of form: ', values);
      }
    });
  };

  render() {
    const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;

    // Only show error after a field is touched.
    const usernameError = isFieldTouched('username') && getFieldError('username');
    const passwordError = isFieldTouched('password') && getFieldError('password');
    return (
      <Form layout="inline" onSubmit={this.handleSubmit}>
        <Form.Item validateStatus={usernameError ? 'error' : ''} help={usernameError || ''}>
          {getFieldDecorator('username', {
            rules: [{ required: true, message: 'Please input your username!' }],
          })(
            <Input
              prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
              placeholder="Username"
            />,
          )}
        </Form.Item>
        <Form.Item validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}>
          {getFieldDecorator('password', {
            rules: [{ required: true, message: 'Please input your Password!' }],
          })(
            <Input
              prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
              type="password"
              placeholder="Password"
            />,
          )}
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit" disabled={hasErrors(getFieldsError())}>
            Log in
          </Button>
        </Form.Item>
      </Form>
    );
  }
}

const WrappedHorizontalLoginForm = Form.create({ name: 'horizontal_login' })(HorizontalLoginForm);

ReactDOM.render(<WrappedHorizontalLoginForm />, mountNode);

In fact, after reading this code, there will be some questions, such as: why use Form.create() to wrap the form and why use getFieldDecorator to wrap the data. To solve these problems, we must pay attention to the underlying dependency of Antd 3.x Form on rc-form.

First, let's take a look at what Form.create() does. There is a static method create in the code of the component Form. When create is called, it will return createDomForm in rc-form. This method takes some methods in mixin as parameters (mixin has built-in properties and methods such as getFieldDecorator as shown in the figure below), and passes in the createBaseForm method to generate a new container through high-level component decorate.

image.png

Then copy the static properties of the wrapped component to the new component. The execution of life cycle events is mainly to initialize the default field through getInitialState. The render function returns the original component (the attributes of the Form component have been injected). The returned new component is wrapped by argumentContainer, which uses the hoistStatics method of the hoist-non-react-statics library. This method is used to combine some static methods of the passed-in component with the new component. In this way, the initialization of the Form is completed, and rc-form creates a fieldStore corresponding to the component instance to store various states of the current Form.

image.png

Then there is the getFieldDecorator responsible for data management. When the user uses the getFieldDecorator method to pass the parameters such as key, initial value, and verification rules, it will create form information through getFieldProps and return a cloned input back to the fieldsStore ( source code line 225), And bind the default onChange event.

image.png

If validation is required, onCollectValidate will be triggered, the result will be saved to the fieldsStore through validateFieldsInternal, and finally the form component with two-way data binding will be returned.

image.png

When onChange or onBlur is triggered for two-way binding form items, the corresponding onCollect method is called (for example, onCollectValidate is used for validation), and onCollectCommon is called internally to trigger the corresponding onChange, and the updated value in the event is obtained to generate a new field. Then setFields calls forceUpdate to set the updated value. The data flows back and the end user sees the new value.​

rc-form seems to take over a lot of details, reducing the burden on developers. But setFields calls forceUpdate to set the updated value will bring the global rendering of the entire Form, so performance will be a big problem, and this has been resolved in Antd 4.x, and will be introduced in the "next" chapter.

Reference documents

  1. https://reactjs.org/docs/forms.html
  2. https://github.com/redux-form/redux-form#%EF%B8%8F-attention-%EF%B8%8F
  3. https://formik.org/docs/overview
  4. https://github.com/formium/formik
  5. https://3x.ant.design/components/form-cn/
  6. https://github.com/mridgway/hoist-non-react-statics

Author: ES2049 / Dibao

Articles may be reproduced freely, but please keep description link .
You are very welcome to join ES2049 Studio if you are passionate. Please send your resume to <caijun.hcj@alibaba-inc.com>.


ES2049
3.7k 声望3.2k 粉丝