Building forms using React — everything you need to know

Published Apr 30, 2018Last updated May 20, 2018
Building forms using React — everything you need to know

Forms are integral to any modern application. They serve as a basic medium for users to interact with your app. Developers rely on forms for everything: securely logging in the user, searching and filtering the product list, booking a product, and building a cart, etc. More complex applications built for enterprises are usually more form-intensive, with the input fields spanning over multiple tabs. On top of that, you have to consider the validation logic that needs to be deployed.

In this tutorial, we're going to look at how React handles forms. We'll cover not just the basics, but also form validation and best practices too — even experienced developers get certain details wrong.

Let's get started.

Creating a Form — Controlled Component vs. Uncontrolled Component

React offers a stateful, reactive approach to building forms. Unlike other DOM elements, HTML form elements work differently in React. The form data, for instance, is usually handled by the component rather than the DOM, and is usually implemented using controlled components. The image below perfectly describes how controlled components work in React.

react.png

The form’s structure is similar to those of the usual HTML forms. However, each input element gets a component of its own, which we call dumb components. The container component is responsible for maintaining the state. The difference is, we're using a callback function to handle form events and then using the container’s state to store the form data. This gives your component better control over the form control elements and the form data.

The callback function is triggered on events, including change of form control values, or on form submission. The function then pushes the form values into the local state of the component and the data is then said to be controlled by the component. Because we are using the value attribute on the form element, the value displayed will be the value of this.state.value.

There is another technique, popularly known as uncontrolled components, for creating input forms. This is more like traditional HTML forms because the input form data is stored inside the DOM and not within the component. Elements like <input>and <textarea> maintain their own state, which they update when the input values change. You can query the DOM for the value of an input field using a ref.

Here is an example from the official docs that demonstrate how uncontrolled components work.

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

  handleSubmit(e) {
    alert('The value is: ' + this.input.value);
    e.preventDefault();
  }

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

Here, the <input> component is responsible for storing its state. The ref attribute creates a reference to the DOM node accessible and you can pull this value when you need it — when you're about to submit the form in the example.

React recommends using controlled components over refs to implement forms. Refs offer a backdoor to the DOM, which might tempt you to use it to do things the jQuery way. Controlled components, on the other hand, are more straightforward — the form data is handled by a React component. However, if you want to integrate React with a non-React project, or create a quick and easy form for some reason, you can use a ref. The rest of this tutorial is going to focus on controlled components.

React Form Demo

Here is the codesandobx demo for the form that we’ll be creating today.
@React Form Demo

You can also grab a copy of the code from my GitHub repo. Clone the repo, run npm install, and then run npm start.

Structuring a Form

React's composition model lets you organize your code into smaller reusable components. Each component exists as an independent functional unit and an hierarchy of components can be used to represent a specific feature. This structure works particularly well with forms. You can create custom components for <input>, <textarea>, <select>, etc. and reuse them to compose a FormContainer component.

Note: although it could be tempting to use a form library instead, chances are high that you might come across obstacles when you need to add custom behavior and validation. Creating a reusable form component from scratch will help you bolster your understanding of React forms.

import React, { Component } from 'react';  
import './styles.css';  
import FormContainer from './containers/FormContainer';

class App extends Component {  
  render() {
    return (
      <div className="container">
        <h3>React Form</h3>
        <FormContainer />
      </div>
    );
  }
}

export default App; 

FormContainer is a container component that renders all of the form elements and handles all of the business logic. We call it a container component because it takes care of updating the state of the form, handling form submission, and making API calls/dispatching Redux actions. The dumb components or presentational components are concerned with how things look and contain the actual DOM markup. These components receive data and callbacks exclusively as props. I've covered more about it on my Stateful vs. Stateless components in React tutorial.

Let's move on and create a container:

import React, {Component} from 'react';  

/* Import Components */
import CheckBox from '../components/CheckBox';  
import Input from '../components/Input';  
import TextArea from '../components/TextArea';  
import Select from '../components/Select';
import Button from '../components/Button'

class FormContainer extends Component {  
  constructor(props) {
    super(props);

    this.state = {
      newUser: {
        name: '',
        email: '',
        age: '',
        gender: '',
        expertise: '',
        about: ''

      },

      genderOptions: ['Male', 'Female', 'Others'],
      skillOptions: ['Programming', 'Development', 'Design', 'Testing']

    }
    this.handleFormSubmit = this.handleFormSubmit.bind(this);
    this.handleClearForm = this.handleClearForm.bind(this);
  }

  /* This life cycle hook gets executed when the component mounts */

  handleFormSubmit() {
    // Form submission logic
  }
  handleClearForm() {
    // Logic for resetting the form
  }
  render() {
    return (
      <form className="container" onSubmit={this.handleFormSubmit}>

        <Input /> {/* Name of the user */}
        <Input /> {/* Input for Age */} 
        <Select /> {/* Gender Selection */}
        <CheckBox /> {/* List of Skills (eg. Programmer, developer) */}
        <TextArea /> {/* About you */}
        <Button /> { /*Submit */ }
        <Button /> {/* Clear the form */}
      </form>
    );
  }
}

export default FormContainer;
}

First, we've imported the dumb components from the components directory. The components include <Input>, <Select>, <CheckBox>, <TextArea>, and <Button>.

Then, we have initiated the state for storing the user data and the UI data. Two methods — handleFormSubmit() and handleClearForm() — have been created to handle the form logic. The render method renders all of the input fields and buttons required for our sign up form.

Composing the Dumb Components

We've laid out the structure of the form. Next, we need to compose the child components. Let's go through the components one by one.

<Input />

The <Input /> component displays a one-line input field. The input type could be either text or number. Let's have a look at the props that we need to create an <Input /> component.

  • type — The type prop determines whether the input field rendered is of type, text, or number. For instance, if the value of type is equal to number, then <input type="number" /> will be rendered. Otherwise, <input type="text" /> gets rendered.
  • title — The value of the title prop will be displayed as a label of that particular field.
  • name — This is the name attribute for the input.
  • value — The value (either text or number) that should be displayed inside the input field. You can use this prop to give default value.
  • placeholder — An optional string that you can pass so that the input field displays a placeholder text.
  • handleChange — A control function that gets triggered when the input control element's value changes. The function then updates the state of the parent component and passes the new value through the value prop.

Here's the code for the <Input/> component. Note that we're using stateless functional components here.

const Input = (props) => {
    return (  
  <div className="form-group">
    <label htmlFor={props.name} className="form-label">{props.title}</label>
    <input
      className="form-input"
      id={props.name}
      name={props.name}
      type={props.type}
      value={props.value}
      onChange={props.handleChange}
      placeholder={props.placeholder} 
    />
  </div>
)
}

export default Input;

You can further expand the list of possible attributes and add them as props. Here is what the component's declaration looks like:

<Input type={'text'}
               title= {'Full Name'} 
               name= {'name'}
               value={this.state.newUser.name} 
               placeholder = {'Enter your name'}
               handleChange = {this.handleFullName}
               /> {/* Name of the user */}
               

The handleChange callback takes care of updating the state and the updated value propagates through props.value. I am going to name the callback function as handleFullName.

/* FormContainer.jsx */

//...
  handleFullName(e) {
   let value = e.target.value;
   this.setState( prevState => ({ newUser : 
        {...prevState.newUser, name: value
        }
      }))
  }
//...

The setState accepts either an object or an updater function with the following signature.

(prevState, props) => stateChange

The prevState object holds the up-to-date value of the previous state. We are going to merge the updated values with the previous state.

Note: in JavaScript, class methods are not bound by default. You will need to bind it manually. What does that mean? You will need to add a binding into the constructor for each class method and the binding will look like this:

this.handleFullName = this.handleFullName.bind(this)

Alternatively, you can use class fields to do binding outside the constructor. The feature is still in experimental phase, so you will need to install the babel plugin transform-class-properties to support it.

The next input field is going to be for age. The logic of the handleAge will be similar to that of the handleFullName method.

/* FormContainer.jsx */

handleAge(e) {
       let value = e.target.value;
   this.setState( prevState => ({ newUser : 
        {...prevState.newUser, age: value
        }
      }), () => console.log(this.state.newUser))
  }

This method updates the state of this.state.newUser.age. Although this approach is okay, you can refactor the code and create a generic handler method that works for all <Input /> components.

/* FormContainer.jsx */

handleInput(e) {
     let value = e.target.value;
     let name = e.target.name;
     this.setState( prevState => {
        return { 
           newUser : {
                    ...prevState.newUser, [name]: value
                   }
        }
     }, () => console.log(this.state.newUser)
     )
 }

handleInput() will replace both handleFullName() and handleAge(). The only change we've made is to extract the value of name from form variable and then use that data to set the state. So, the value of the name prop should be same as the key of the property in the state.

Next up, <Select />.

<Select />

The <Select /> component displays a list of drop-down items. Usually, there will be a placeholder text or a default value for the drop-down. Here are the props for the <Select />:

  • title — The value of the title prop be displayed as label of the <select> element.
  • name — The name attribute for the <select> element.
  • options — An array of available options. For instance, we are using the <select /> to display a drop-down list of gender options.
  • value — The value prop can be used to set the default value of the field.
  • placeholder — A short string that populates the first <option> tag.
  • handleChange — A control function that gets triggered when the input control element's value changes. The function then updates the state of the parent component and passes the new value through the value prop.

Let's have a look at the actual code for the <Select /> component.

/*Select.jsx*/

const Select = (props) => {
    return(
        <div className="form-group">
            <label htmlFor={props.name}> {props.title} </label>
            <select
              name={props.name}
              value={props.value}
              onChange={props.handleChange}
              >
              <option value="" disabled>{props.placeholder}</option>
              {props.options.map(option => {
                return (
                  <option
                    key={option}
                    value={option}
                    label={option}>{option}
                  </option>
                );
              })}
            </select>
      </div>)
}

export default Select;

The first option tag is populated with the placeholder string. The rest of the options are mapped from the array that we passed on as props. While using the map method to iterate through DOM elements, remember to add a key attribute that's unique. This helps React keep track of DOM updates. If you leave out the key attribute, you will see a warning in your browser and might encounter performance issues down the road.

Now, let's have a look at the callback function. The logic for method is similar to that of the generic handleInput that we created earlier. We can actually plug in that handler method as a prop and everything should work as expected.

<Select title={'Gender'}
       name={'gender'}
       options = {this.state.genderOptions} 
       value = {this.state.newUser.gender}
       placeholder = {'Select Gender'}
       handleChange = {this.handleInput}
/> {/* Age Selection */}

<CheckBox/>

Checkboxes might appear a bit more complicated because arrays are involved. But both <CheckBox> and <Select> are similar in terms of props. The major difference lies in how the state is updated. Let's have a look at the props first.

  • title — Already covered.
  • name — Already covered.
  • options — An array of available options. The array is usually composed of strings that end up being the label and the value of each checkbox.
  • selectedOptions — An array of selected values. If the user had selected certain choices beforehand, the selectedOptions array would be populated with those values. This is synonymous to the <Select /> component's value prop.
  • handleChange — Already covered.

Here's the CheckBox component.

/* CheckBox.jsx */

const CheckBox = (props) => {

    return( <div>
    <label for={props.name} className="form-label">{props.title}</label>
    <div className="checkbox-group">
      {props.options.map(option => {
        return (
          <label key={option}>
            <input
              className="form-checkbox"
              id = {props.name}
              name={props.name}
              onChange={props.handleChange}
              value={option}
              checked={ props.selectedOptions.indexOf(option) > -1 }
              type="checkbox" /> {option}
          </label>
        );
      })}
    </div>
  </div>
);

}

The line checked={ props.selectedOptions.indexOf(option) > -1 } might be confusing if you've never used JavaScript's indexOf method before. indexOf checks whether a particular item exists within an array and returns its index. Assuming that option holds a string, it checks whether the string exists within the selectedOptions and if the item doesn't exist in the array, it will return -1. This is the easiest way to populate values to a checkbox group in a form.

Since we need to push an array into the state, which is more complicated than the usual handleInput(), let's create a new method for handling checkboxes.

handleSkillsCheckBox(e) {

    const newSelection = e.target.value;
    let newSelectionArray;

    if(this.state.newUser.skills.indexOf(newSelection) > -1) {
      newSelectionArray = this.state.newUser.skills.filter(s => s !== newSelection)
    } else {
      newSelectionArray = [...this.state.newUser.skills, newSelection];
    }

      this.setState( prevState => ({ newUser:
        {...prevState.newUser, skills: newSelectionArray }
      })
      )
}

The user can interact with the checkbox in two ways — check an item, or uncheck an existing item. This user interaction corresponds to two actions — adding an item into the array, or removing an existing item from the array.

The newSelection variable has the value of the newly selected (or deselected) item. We compare it with the existing selection of items stored at this.state.newUser.skills. We're again going to rely on indexOf to check whether the string stored in newSelection is already of the array.
If it's part of the array, the condition falls true and the new selection item is filtered out and stored in newSelection. Otherwise, the newSelection item is concatenated into the array using spread operator.

Finally, the state is updated using this.setState.

<TextArea />

I am going to leave this as an exercise for the reader. This is fairly similar to the <Input /> component that we created earlier. The <textarea /> element should accept additional props for rows and columns. The code for the TextArea component is available in the sandbox demo for reference.

<Button />

Buttons are easiest of the lot. You can keep the <Button /> component fairly simple and easy. Here is are the list of props that a button requires:

  • title — Text for the button.
  • action — Callback function
  • style — Style objects can be passed as props.

Here's the <Button/> in action:

/*Button.jsx */
const Button = (props) => {
    console.log(props.style);
    return(
        <button 
            style= {props.style} 
            onClick= {props.action}>    
            {props.title} 
        </button>)
}

export default Button;

Form Actions — handleClearForm and handleFormSubmit

We've nearly reached the end of the tunnel. The last step is to compose the form actions. Since the FormContainer component maintains the state, the form action methods will go there.

The handleClearForm method will clear the state and set it back to its initial values.

handleClearForm(e) {

      e.preventDefault();
      this.setState({ 
        newUser: {
          name: '',
          age: '',
          gender: '',
          skills: [],
          about: ''
        },
      })
  }

The line e.preventDefault() prevents the page from being refreshed on form submission, which is the default form behavior.

The handleFormSubmit() method takes care of making AJAX requests to the server. The data that needs to be sent is available at this.state.newUser. There are many libraries that you can use to make AJAX calls. I am going to use fetch here.

handleFormSubmit(e) {
    e.preventDefault();
    let userData = this.state.newUser;

    fetch('http://example.com',{
        method: "POST",
        body: JSON.stringify(userData),
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
      }).then(response => {
        response.json().then(data =>{
          console.log("Successful" + data);
        })
    })
  }   

That's it!

Summary

In this article, we've covered everything that you need to know about building forms in React. React has component-based architecture and the components are meant to be reusable. We've gone ahead and created components for the input elements, such as: <input/>, <select />, <textArea/>, etc. You can further customize the components according to your requirements by passing more props.

I hope you've had a good read. What are your thoughts about building forms using React? If you have anything to share, let us know in the comments.

Discover and read more posts from Manjunath
get started