Working with forms in React.js using basic tools

Introduction


During my work on React.js, I often had to deal with form processing. Redux-Form , React-Redux-Form went through my hands , but none of the libraries satisfied me fully. I did not like that the state of the form is stored in the reducer , and each event passes through the action creator . Also, according to Dan Abramov, "the state of the form is inherently ephemeral and local, so you do not need to keep track of it in Redux (or any Flux library)."


I note that in React-Redux-Form there is a LocalForm component that allows you to work without redux, but in my opinion, it makes no sense to install a library of 21.9kB in size and use it for less than half.


I am not against these libraries, in specific cases they are irreplaceable. For example, when a third-party component that is not associated with a form depends on the data entered. But in my article I want to consider forms that do not need redux.


I began to use the local state of the component, while there were new difficulties: the amount of code increased, the components lost readability, a lot of duplication appeared.


The solution was the concept of High Order Component. In short, HOC is a function that receives components as input and returns it updated with integration of additional or modified props. You can read more about HOC on the official website React.js . The goal of using the HOC concept was to split the component into two parts, one of which would be responsible for the logic, and the second for the mapping.


Form creation


As an example, we will create a simple feedback form, in which there will be 3 fields: name, email, phone.


For simplicity, use the Create-React-App . Install it globally:


npm i -g create-react-app

then create your application in the pure-form folder


create-react-app pure-form

Additionally , we will install prop-types and classnames , they will be useful to us in the future:


npm i prop-types classnames -S

Create two folders / components and / containers . In the / components folder will be all the components responsible for the display. In the / containers folder , the components responsible for the logic.


In the / components folder, create an Input.jsx file , in which we will declare a common component for all inputs. It is important at this stage not to forget to prescribe ProptTypes and defaultProps with high quality , to provide the ability to add custom classes, and also to inherit it from PureComponent for optimization.
The result is:


import React, { PureComponent } from'react';
import cx from'classnames';
import PropTypes from'prop-types';
classInputextendsPureComponent{
  render() {
    const {
      name,
      error,
      labelClass,
      inputClass,
      placeholder,
      ...props
    } = this.props;
    return (
      <labelclassName={cx('label', !!labelClass && labelClass)}
        htmlFor={`id-${name}`}
      ><spanclassName="span">{placeholder}</span><inputclassName={cx(
            'input',
            !!inputClass && inputClass,
            !!error && 'error'
          )}
          name={name}id={`id-${name}`}
          onFocus={this.handleFocus}onBlur={this.handleBlur}
          {...props}
        />
        {!!error && <spanclassName="errorText">{error}</span>}
      </label>
    );
  }
}
Input.defaultProps = {
  type: 'text',
  error: '',
  required: false,
  autoComplete: 'off',
  labelClass: '',
  inputClass: '',
};
Input.propTypes = {
  value: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  placeholder: PropTypes.string.isRequired,
  error: PropTypes.string,
  type: PropTypes.string,
  required: PropTypes.bool,
  autoComplete: PropTypes.string,
  labelClass: PropTypes.string,
  inputClass: PropTypes.string,
};
export default Input;

Next, in the / components folder, create a Form.jsx file in which the component containing the form will be declared. We will receive all methods for working with it through props, as well as the value for inputs, so state is not needed here. We get:


import React, { Component } from'react';
import PropTypes from'prop-types';
import Input from'./Input';
import FormWrapper from'../containers/FormWrapper';
classFormextendsComponent{
  render() {
    const {
      data: { username, email, phone },
      errors,
      handleInput,
      handleSubmit,
    } = this.props;
    return (
      <div className="openBill">
        <form className="openBillForm" onSubmit={handleSubmit}>
          <Input
            key="username"
            value={username}
            name="username"
            onChange={handleInput}
            placeholder="Логин"
            error={errors.username}
            required
          />
          <Input
            key="phone"
            value={phone}
            name="phone"
            onChange={handleInput}
            placeholder="Телефон"
            error={errors.phone}
            required
          />
          <Input
            key="email"
            value={email}
            type="email"
            name="email"
            onChange={handleInput}
            placeholder="Электронная почта"
            error={errors.email}
            required
          />
          <button type="submit" className="submitBtn">
            Отправить форму
          </button>
        </form>
      </div>
    );
  }
}
Form.propTypes = {
  data: PropTypes.shape({
    username: PropTypes.string.isRequired,
    phone: PropTypes.string.isRequired,
    email: PropTypes.string.isRequired,
  }).isRequired,
  errors: PropTypes.shape({
    username: PropTypes.string.isRequired,
    phone: PropTypes.string.isRequired,
    email: PropTypes.string.isRequired,
  }).isRequired,
  handleInput: PropTypes.func.isRequired,
  handleSubmit: PropTypes.func.isRequired,
};
export default FormWrapper(Form);

Create HOC


In the / containers folder, create the FormWrapper.jsx file . We declare inside a function that takes the WrappedComponent component as an argument and returns the WrappedForm class . The render method of this class returns a WrappedComponent with props integrated into it. Try to use the classic function declaration, this will simplify the debugging process.


In the WrappedForm class , create a state : isFetching - a flag to control asynchronous requests, data - an object with value inputs, errors - an object to store errors. The declared state is passed to the WrappedComponent . Thus, the takeaway of the form state storage to the upper level is implemented, which makes the code more readable and transparent.


exportdefaultfunctionWrapper(WrappedComponent) {
  returnclassFormWrapperextendsComponent{
    state = {
      isFetching: false,
      data: {
        username: '',
        phone: '',
        email: '',
      },
      errors: {
        username: '',
        phone: '',
        email: '',
      },
    };
    render() {
      return<WrappedComponent {...this.state} />;
    }
  };
}

But this implementation is not universal, because for each form you have to create your own wrapper. You can upgrade this system and put the HOC inside another function that will form the initial state values.


import React, { Component } from'react';
exportdefaultfunctiongetDefaultValues(initialState, requiredFields) {
  returnfunctionWrapper(WrappedComponent) {
    returnclassWrappedFormextendsComponent{
      state = {
        isFetching: false,
        data: initialState,
        errors: requiredFields,
      };
      render() {
        return<WrappedComponent {...this.state} {...this.props} />;
      }
    };
  };
}

In this function, you can pass not only the initial state values , but generally any parameters. For example, attributes and methods on the basis of which you can create a form in Form.jsx . An example of such an implementation will be the topic for the next article.


In the Form.jsx file, we will declare the initial state values and pass them to the HOC:


const initialState = {
    username: '',
    phone: '',
    email: '',
};
exportdefault FormWrapper(initialState, initialState)(Form);

Create a handleInput method to handle the values ​​entered in the input. It receives an event , from which we take value and name and pass them to setState . Since the values ​​of the inputs are stored in the data object , the setState function is called. Simultaneously with the preservation of the obtained value, reset the error store of the variable field. We get:


handleInput = event => {
  const { value, name } = event.currentTarget;
  this.setState(({ data, errors }) => ({
    data: {
      ...data,
      [name]: value,
    },
    errors: {
      ...errors,
      [name]: '',
    },
  }));
};

Now we will create a handeSubmit method for processing the form and output the data to the console, but before that we need to pass validation. We will validate only the required fields, that is, all the keys of the object this.state.errors. We get:


handleSubmit = e => {
    e.preventDefault();
    const { data } = this.state;
    const isValid = Object.keys(data).reduce(
        (sum, item) => sum && this.validate(item, data[item]),
        true
    );
    if (isValid) {
      console.log(data);
    }
};

Using the reduce method, let's iterate through all the required fields. At each iteration, the validate method is called , in which we pass the name , value pair . Inside the method, validation of the entered data is performed, the results of which return a boolean type. If at least one pair of values ​​fails validation, then the isValid variable will become false and the data will not be output to the console, that is, the form will not be processed. Here is a simple case - a test for a non-empty form. Validate method :



validate = (name, value) => {
    if (!value.trim()) {
      this.setState(
        ({ errors }) => ({
          errors: {
            ...errors,
            [name]: 'поле не должно быть пустым',
          },
        }),
        () => false
      );
    } else {
      returntrue;
    }
};

Both the handleSubmit and handleInput methods must be passed to the WrappedComponent :


render() {
    return (
        <WrappedComponent
            {...this.state}
            {...this.props}
            handleInput={this.handleInput}handleSubmit={this.handleSubmit}
        />
    );
}

As a result, we will get a ready feedback form, with simple validation and error output. In this case, we learned the logical part of the component responsible for the display.


Conclusion


So, we have reviewed the basic example of creating a HOC for form processing. When creating the form, only simple inputs were used, without complex elements, such as drop-down lists, checkboxes, radiobuttons and others. If available, you may have to create additional event handling methods.


Questions and comments please write in the comments to the article or me in the mail.

A complete example can be found here: pure react form .

Thanks for attention!


Also popular now: