Three ways to build React forms

Part 1– basic forms with default validation

There’s no “right way” to build forms in React and there are a lot of options. It’s simple enough to have a working form, but what about validation? Can you rely on the browser’s default validation, or should you write your own state handlers, or drop in a form library?

In this series we’ll explore three approaches to building a form in React. If you’re a React developer with experience creating components, but haven’t built large, multi-input forms with custom validation, this series is for you! Also, we’ll be using (Formspree), but the principles should still apply if you’re using your own form backend.

Three approaches

We’re going to discuss three approaches to building a form in react:

  1. A basic form with default browser validation
  2. A controlled component form with custom validation
  3. A form that uses a popular library, Formik

The main tradeoffs between these three approaches have to do with Validation.

Option 1, a basic form, is great if you need a quick form and are comfortable with the validation provided by the browser. Just remember that the default validation styles will be different for each browser, and some browsers offer better validation than others. We’ll discuss this approach in depth later in this post.

Option 2, a controlled form, is better if you want more control of your form’s UX, but you’re still building a relatively simple form. As you’ll see, this option gets unwieldy fast. This approach is covered in Part 2.

Option 3, using a library, may be best if you have a complex form and you want control of the UX. However the form design and functionality depend on the library you choose. We’ll explore a popular library in depth, Formik, and discuss a few alternatives. This approach will be covered in Part 3.

Note: in this series we’re not covering back-end or server validation. This tutorial assumes you’re using the form to collect information, not perform business logic. If you’re incorporating your form into an application, or require strictly validated data, server side validation is important. However, that’s outside the scope of this series.

Option 1: A basic contact form

As a starting point, let’s create a simple HTML contact form in React. The form will submit data to a server via AJAX and handle the response message. We’ll use the popular Axios library to simplify AJAX state management.

We’ll be creating what the React documentation calls an “uncontrolled component” as opposed to a “controlled component”. The main difference is that we’ll lean on the browser’s HTML form implementation to handle things like form state, validation and errors.

This example uses React Hooks, specifically useState, to track the state of the form as it’s requesting data from the server. If you’re not familiar with react hooks, there are a good overview here to get you started.

Here’s the code!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import React, { useState } from "react";
import axios from "axios";

function MyForm() {
  const [serverState, setServerState] = useState({
    submitting: false,
    status: null
  });
  const handleServerResponse = (ok, msg, form) => {
    setServerState({
      submitting: false,
      status: { ok, msg }
    });
    if (ok) {
      form.reset();
    }
  };
  const handleOnSubmit = e => {
    e.preventDefault();
    const form = e.target;
    setServerState({ submitting: true });
    axios({
      method: "post",
      url: "https://formspree.io/YOUR_FORM_ID",
      data: new FormData(form)
    })
      .then(r => {
        handleServerResponse(true, "Thanks!", form);
      })
      .catch(r => {
        handleServerResponse(false, r.response.data.error, form);
      });
  };
  return (
    <div>
      <h1>Contact Us</h1>
      <form onSubmit={handleOnSubmit}>
        <label htmlFor="email">Email:</label>
        <input id="email" type="email" name="email" required />
        <label htmlFor="message">Message:</label>
        <textarea id="message" name="message"></textarea>
        <button type="submit" disabled={serverState.submitting}>
          Submit
        </button>
        {serverState.status && (
          <p className={!serverState.status.ok ? "errorMsg" : ""}>
            {serverState.status.msg}
          </p>
        )}
      </form>
    </div>
  );
};

export default MyForm;

A few notes:

  • This code uses Formspree to receive the form submission, but it’s not required. If you choose to use Formspree, go to https://formspree.io/create to get your own form ID to replace YOUR_FORM_ID on line 24. Otherwise, update the url and handleServerResponse method to accommodate your form backend.
  • On line 25 above, we pass the full form data as an object to Axios. Axios will convert this into a multipart/form-data object before submitting. Formspree understands this payload, but if you’re using a different form backend, you may need to convert the data into JSON first.
  • We’re using built-in browser validation. The type="email" attribute on line 39 ensures that the input is a valid email address, and the required attribute ensures that the email is filled-in.
  • The server response is displayed below the form on line 46. This assumes that the server will provide a human readable error message if something goes wrong, which Formspree does. If you’re using a different form backend, you may need to customize the error message here.


Here’s what it looks like to fill out and submit this form. Spoiler, it’s really basic!

The downsides of an uncontrolled form

The main disadvantage to this approach is that validation is left up to the browser. This isn’t as bad as it sounds. Browsers contain lots of hooks for customizing the validation. This is important because, by default, each browser’s UX is different. Here are a few popular browser form validations styles:

Chrome Validation
Firefox Validation
Safari Validation

To create more consistent validation, you can hook into the :valid and :invalid css pseudo classes.

You can also validate different types of values. See the Mozilla input documentation for input types and validation rules for each. You can even perform custom regex validation with the pattern attribute. Unfortunately there are still nuances and quirks between browsers that could result in an inconsistent experience. For example, type="email" in Chrome will block test@example but Safari allows it to pass. If you’re just building a contact-us form, these inconsistencies may be acceptable.

A related disadvantage is that an uncontrolled component doesn’t allow for custom UX. React has no knowledge of, or ability to manipulate, the form’s state. Beyond validation, this makes it difficult to perform custom form rendering. If you want to implement custom input components, update the form with each keypress, or hide and reveal inputs based on user actions, this approach won’t get you very far.

To build more sophisticated forms, the first step is to take control of the form’s state by creating a “controlled component” form. We’ll be discussing that in Part 2. If you want to skip ahead check out Part 3, which contains a more complex form with custom validation using Formik and Yup.


Got Feedback?