Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Working With Forms in React.js Using The Basic Toolkit

DZone's Guide to

Working With Forms in React.js Using The Basic Toolkit

A tutorial on using React.js with its built-in toolkit and how to use the concept of High Order Components in your JavaScript code.

· Web Dev Zone ·
Free Resource

Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.

Introduction

During my work with React.js, I often had to deal with the processing of forms. I tried using Redux-Form, React-Redux-Form, but none of the libraries suited all my needs. 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 track it in Redux (or any Flux library)."

I note that React-Redux-Form has a LocalForm component that allows developers to work without redux, but, in my opinion, it's useless to install a 21.9KB library and use it for less than half the time.

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

I started using the local state component, and new difficulties arose: the amount of code increased, the components lost readability, and a lot of duplication appeared.

The solution to the problems was the concept of High Order Components. Briefly, HOC is a function that receives a component on the input and returns it, updated, with the integration of additional or changed props. More information about HOC can be found on the official website of React.js. The purpose of using the concept of HOC was to split the component into two parts, one of which would be responsible for the logic, and the second - for the mapping.

Creating a Form

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. Establish it globally:

npm i -g create-react-app

Then create your application in a clean form folder:

create-react-app pure-form

In addition, we set prop-types and class names, as they will be useful in the future:

npm i prop-types classnames -S

Create two folders: /components and /containers. The /components folder will contain all the components that are responsible for the mapping. In the /containers folder, the components responsible for the logic.

In the /components folder, create the file Input.jsx, in which we declare the common component for all the intuitions. It is important at this stage not to forget to qualitatively register ProptTypes and defaultProps, 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';

class Input extends PureComponent {
  render() {
    const {
      name,
      error,
      labelClass,
      inputClass,
      placeholder,
      ...props
    } = this.props;
    return (
      <label
        className={cx('label', !!labelClass && labelClass)}
        htmlFor={`id-${name}`}
      >
        <span className="span">{placeholder}</span>
        <input
          className={cx(
            'input',
            !!inputClass && inputClass,
            !!error && 'error'
          )}
          name={name}
          id={`id-${name}`}
          onFocus={this.handleFocus}
          onBlur={this.handleBlur}
          {...props}
        />
        {!!error && <span className="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 the file Form.jsx, in which the composition containing the form will be declared. All methods for working with it will be obtained through props, as well as the value for the intuitions, so the 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';

class Form extends Component {
  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 = "Login"
            error = {errors.username}
            required
          />
          <Input
            key = "phone"
            value = {phone}
            name = "phone"
            onChange = {handleInput}
            placeholder = "Phone"
            error = {errors.phone}
            required
          />
          <Input
            key = "email"
            value = {email}
            type = "email"
            name = "email"
            onChange = {handleInput}
            placeholder = "Email"
            error = {errors.email}
            required
          />
          <button type = "submit" className = "submitBtn">
            Submit form
          </ 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);

Creating an HOC

In the /containers folder, create the file FormWrapper.jsx. We declare a function that takes the WrappedComponent component as its 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, as this will simplify the debugging process.

In the WrappedForm class, we create a state: isFetching. This will act as a flag to control asynchronous requests, data (an object with a value of the intuitions), and errors (an object for storing errors). The declared state will be passed to WrappedComponent. In this way, the storage of the form state storage is carried out to the upper level, which makes the code more readable and transparent.

export default function Wrapper(WrappedComponent) {
  return class FormWrapper extends Component {
    state = {
      isFetching: false,
      data: {
        username: '',
        phone: '',
        email: '',
      },
      errors: {
        username: '',
        phone: '',
        email: '',
      },
    };

    render() {
      return <WrappedComponent {...this.state} />;
    }
  };
}

But such an implementation is not universal, because for each form you will have to create your own wrapper. You can improve this system and put HOC inside one more function that will generate initial state values.

import React, { Component } from 'react';

export default function getDefaultValues(initialState, requiredFields) {
  return function Wrapper(WrappedComponent) {
    return class WrappedForm extends Component {
      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, the attributes and methods on 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, declare the initial state values and pass them to the HOC:

const initialState = {
    username: '',
    phone: '',
    email: '',
};

export default FormWrapper(initialState, initialState)(Form);

Let's create a handleInput method for processing the values entered into the input. It receives an event, from which we take value and name and pass them to setState. Since the values of the intuitions are stored in the data object, in setState we call the function. Simultaneous to saving the received value, we zero the error store of the field being changed. We get:

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

Now create a handeSubmit method to process the form and output the data to the console, but, before that, you need to pass validation. We will only validate the required fields, that is, all the keys of 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);
    }
};

Use the reduce method to list all the required fields. At each iteration, the validate method is invoked, to which we pass the name/value pair. Inside the method, the correctness of the entered data is checked, which results in the return of the boolean type. If at least one pair of values fails validation, the variable isValid will be false, and the data in the console will not be displayed, that is, the form will not be processed. Here we consider the simple case of checking for a nonempty form. Here's the validate method:

validate = (name, value) => {
     if (! value.trim ()) {
       this.setState (
         ({errors}) => ({
           errors: {
             ... errors,
             [name]: 'field must not be empty',
           },
         }),
         () => false
       );
     } else {
       return true;
     }
};};

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

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

As a result, we get a ready-made feedback form, with simple validation and error reporting. In doing so, we have removed the logical part from the component responsible for the mapping.

Conclusion

So, we have considered a basic example of creating an HOC for form processing. When creating the form, only simple intuitions were used, without complex elements, such as drop-down lists, checkboxes, radio buttons, and others. If they are available, you may have to create additional methods for handling events.

What’s the best way to boost the efficiency of your product team and ship with confidence? Check out this ebook to learn how Sentry's real-time error monitoring helps developers stay in their workflow to fix bugs before the user even knows there’s a problem.

Topics:
javascrip ,react.js ,forms ,tutorial ,web dev

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}