Working with Forms in React

Nilson Jacques
Share

Almost every application needs to accept user input at some point, and this is usually achieved with the venerable HTML form and its collection of input controls. If you’ve recently started learning React, you’ve probably arrived at the point where you’re now thinking, “So how do I work with forms?”

This article will walk you through the basics of using forms in React to allow users to add or edit information. We’ll look at two different ways of working with input controls and the pros and cons of each. We’ll also take a look at how to handle validation, and some third-party libraries for more advanced use cases.

Uncontrolled Inputs

The most basic way of working with forms in React is to use what are referred to as “uncontrolled” form inputs, or uncontrolled components. What this means is that React doesn’t track the input’s state. HTML input elements naturally manage form state as part of the DOM, and so when the form is submitted we have to read the values from the DOM elements themselves.

In order to do this, React allows us to create a “ref” (reference) to associate with an element, giving access to the underlying DOM node. Let’s see how to do this:

class SimpleForm extends React.Component {
  constructor(props) {
    super(props);
    // create a ref to store the DOM element
    this.nameEl = React.createRef();
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(e) {
    e.preventDefault();
    alert(this.nameEl.current.value);
  }

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

As you can see above, for a class-based React component you initialize a new ref in the constructor by calling React.createRef, assigning it to an instance property so it’s available for the lifetime of the component.

In order to associate the ref with an input, it’s passed to the element as the special ref attribute. Once this is done, the input’s underlying DOM node can be accessed via this.nameEl.current.

Let’s see how this looks in a functional component:

function SimpleForm(props) {
  const nameEl = React.useRef(null);

  const handleSubmit = e => {
    e.preventDefault();
    alert(nameEl.current.value);
  };

  return (
     <form onSubmit={handleSubmit}>
       <label>Name:
         <input type="text" ref={nameEl} />
       </label>
       <input type="submit" name="Submit" />
     </form>
   );
}

There’s not a lot of difference here, other than swapping out createRef for the useRef hook.

Example: login form

function LoginForm(props) {
  const nameEl = React.useRef(null);
  const passwordEl = React.useRef(null);
  const rememberMeEl = React.useRef(null);

  const handleSubmit = e => {
    e.preventDefault();

    const data = {
      username: nameEl.current.value,
      password: passwordEl.current.value,
      rememberMe: rememberMeEl.current.checked,
    }

    // Submit form details to login endpoint etc.
    // ...
  };

  return (
     <form onSubmit={handleSubmit}>
       <input type="text" placeholder="username" ref={nameEl} />
       <input type="password" placeholder="password" ref={passwordEl} />
       <label>
         <input type="checkbox" ref={rememberMeEl} />
         Remember me
       </label>
       <button type="submit" className="myButton">Login</button>
     </form>
   );
}

View on CodePen

While uncontrolled inputs work fine for quick and simple forms, they do have some drawbacks. As you might have noticed from the code above, we have to read the value from the input element whenever we want it. This means we can’t provide instant validation on the field as the user types, nor can we do things like enforce a custom input format, conditionally show or hide form elements, or disable/enable the submit button.

Fortunately, there’s a more sophisticated way to handle inputs in React.

Controlled Inputs

An input is said to be “controlled” when React is responsible for maintaining and setting its state. The state is kept in sync with the input’s value, meaning that changing the input will update the state, and updating the state will change the input.

Let’s see what controlled components look like with an example:

class ControlledInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: '' };
    this.handleInput = this.handleInput.bind(this);
  }

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

  render() {
    return (
      <input type="text" value={this.state.name} onChange={this.handleInput} />
    );
  }
}

As you can see, we set up a kind of circular data flow: state to input value, on change event to state, and back again. This loop allows us a lot of control over the input, as we can react to changes to the value on the fly. Because of this, controlled inputs don’t suffer from the limitations of uncontrolled ones, opening up the follow possibilities:

  • instant input validation: we can give the user instant feedback without having to wait for them to submit the form (e.g. if their password is not complex enough)
  • instant input formatting: we can add proper separators to currency inputs, or grouping to phone numbers on the fly
  • conditionally disable form submission: we can enable the submit button after certain criteria are met (e.g. the user consented to the terms and conditions)
  • dynamically generate new inputs: we can add additional inputs to a form based on the user’s previous input (e.g. adding details of additional people on a hotel booking)

Validation

As I mentioned above, the continuous update loop of controlled components makes it possible to perform continuous validation on inputs as the user types. A handler attached to an input’s onChange event will be fired on every keystroke, allowing you to instantly validate or format the value.

Example: credit card validation

Let’s take a look at a real-word example of checking a credit card number as the user types it into a payment form.

The example uses a library called credit-card-type to determine the card issuer (such as Amex, Visa, or Mastercard) as the user types. The component then uses this information to display an image of the issuer logo next to the input:

import  creditCardType  from  "credit-card-type";

function CreditCardForm(props) {
  const [cardNumber, setCardNumber] = React.useState("");
  const [cardTypeImage, setCardTypeImage] = React.useState(
    "card-logo-unknown.svg"
  );

  const handleCardNumber = (e) => {
    e.preventDefault();

    const value = e.target.value;
    setCardNumber(value);

    let suggestion;

    if (value.length > 0) {
      suggestion = creditCardType(e.target.value)[0];
    }

    const cardType = suggestion ? suggestion.type : "unknown";

    let imageUrl;

    switch (cardType) {
      case "visa":
        imageUrl = "card-logo-visa.svg";
        break;
      case "mastercard":
        imageUrl = "card-logo-mastercard.svg";
        break;
      case "american-express":
        imageUrl = "card-logo-amex.svg";
        break;
      default:
        imageUrl = "card-logo-unknown.svg";
    }

    setCardTypeImage(imageUrl);
  };

  return (
    <form>
      <div className="card-number">
        <input
          type="text"
          placeholder="card number"
          value={cardNumber}
          onChange={handleCardNumber}
        />
        <img src={cardTypeImage} alt="card logo" />
      </div>
      <button type="submit" className="myButton">
        Login
      </button>
    </form>
  );
}

The input’s onChange handler calls the creditCardType() function with the current value. This returns an array of matches (or an empty array) which can be used to determine which image to display. The image URL is then set to a state variable to be rendered into the form.

You can use some of the numbers from here to test the input.

Form Libraries

As you might have noticed, there’s a certain amount of boiler-plate code when working with forms, especially having to wire up the inputs with their state values and handlers. As you might expect, there are a variety of third-party libraries out there to help take the pain out of dealing with larger and more complex forms.

To give you some idea of what using a form library is like, let’s take a quick look at one called Fresh. The aim of this library is to cover 90% of your common use cases with a simple and easy-to-use API. Here’s an example of a profile editing form you might find in a web app:

import { Form, Field } from "@leveluptuts/fresh";

const securityQuestions = [
  "What is your mother's maiden name?",
  "What was the name of your first pet?",
  "What was the name of your first school?"
];

const handleSubmit = (data) => console.log(data);

function UserProfileForm() {
  return (
    <Form formId="user-profile" onSubmit={handleSubmit}>
      <Field required>First Name</Field>
      <Field required>Last Name</Field>
      <Field required type="email">
        Email
      </Field>

      <Field required type="select" options={securityQuestions}>
        Security Question
      </Field>
      <Field required>Security Answer</Field>

      <Field type="textarea">Bio</Field>
    </Form>
  );
}

Fresh provides some custom components to make creating forms very straightforward. The Field component takes care of wiring up data binding on the form inputs, converting the label you provide into a camel-case property name for the input’s value. (For example, “Last Name” becomes lastName in the form’s state.)

The Form component wraps all the fields, and takes an onSubmit callback which receives the form data as an object. Below is an example of the output from the form:

{
  firstName: "Bill",
  lastName: "Gates",
  email: "billg@microsoft.com",
  securityQuestion: "What was the name of your first pet?",
  securityAnswer: "Fluffy",
  bio: "Bill Gates is a technologist, business leader, and philanthropist. He grew up in Seattle, Washington, with an amazing and supportive family who encouraged his interest in computers at an early age."
}

As you can see, libraries like this can really speed up working with forms and make your components much less verbose. For anything more than a basic, small form, I’d recommend choosing one that fits your needs, as it will save you time in the long run.

Conclusion

You should now have a solid understanding of how forms and inputs can be used within React. You should know the difference between controlled and uncontrolled inputs, and the pros and cons of each, being aware that the tight update loop of a controlled input allows you a lot of options for formatting and validating the values on the fly. Lastly, you should be aware that there are form libraries available that prevent you from having to add a lot of repetitive and verbose boilerplate code to your React forms, which will help you be a more productive developer.

FAQs About Working with Forms in React

What is a form in React?

A form in React is an essential UI element that allows users to input and submit data. It typically consists of various input fields, checkboxes, radio buttons, and buttons for submitting and resetting data.

How do I create a simple form in React?

To create a simple form in React, you can use the <form> element and include various input elements like <input>, <textarea>, and <select>. You can manage the form’s state using React component state or a state management library like Redux.

How can I handle form input changes in React?

You can handle form input changes by adding event handlers to your form elements, such as onChange for text inputs, checkboxes, and radio buttons. These event handlers should update the component’s state with the new values.

What are controlled and uncontrolled components in React forms?

Controlled components are React form elements whose values are controlled by React state. Uncontrolled components store their state in the DOM, not in React. A controlled component is the recommended way to handle form elements in React.

How can I submit a form in React?

To submit a form in React, you can use the <form> element’s onSubmit event. You should prevent the default behavior of the event and then handle the form data submission, usually by making an HTTP request to a server or performing some other action.

How can I validate form input in React?

You can validate form input by adding validation logic to your event handler, checking if the input in the form fields meets specific criteria (e.g., required fields, email format, etc.). You can also use validation libraries like Yup or Formik to simplify form validation.

What is formik and when should I use it?

Formik is a popular form handling library for React. It simplifies form state management, validation, and submission. You should use Formik when you want to streamline form development and take advantage of its built-in features, especially for complex forms.

How can I handle form submission errors and display them to the user?

When a form submission results in an error, you can handle it by catching the error in your submission function and then displaying an error message to the user, typically using conditional rendering or an error message component.

How can I reset a form in React?

ou can reset a form by calling the reset method on the form element or by setting the form’s state back to its initial values. Ensure that you also clear any error messages or validation messages when resetting the form.

What are the best practices for handling forms in React?

Some best practices include using controlled components, validating input, providing clear error messages, debouncing input for better performance, and considering the user experience in terms of feedback and accessibility.