Kristofer Selbekk Bekker. Bulldog owner. Dad. React enthusiast. 🎩🥂🍔

Creating forms in React in 2020

5 min read 1497

Input fields. Text areas. Radio buttons and checkboxes. These are some of the main interaction points we, as developers, have with our users. We put them front and center, users fill them out as best as they can, and with any luck, they’ll send it back to you without any validation errors.

Form handling is an integral part of a large number of web apps, and it’s one of the things React does best. You have a lot of freedom to implement and control those input controls how you want, and there are plenty of ways to achieve the same goal. But is there a best practice? Is there a best way to do things?

This article will show you a few different ways to handle form values in React. We’ll look at useState, custom Hooks, and, finally, no state at all!

Note that we will create a login form with an email and a password field in all of these examples, but these techniques can be used with most types of forms.

Please keep accessibility in mind

Although it doesn’t directly relate to the topic at hand, I want to make sure you remember to make your forms accessible to all. Add labels to your input, set the correct aria-tags for when the input is invalid, and structure your content semantically correct. It makes your form easier to use for everyone, and it makes it possible to use for those that require assistive technologies.

Handling forms with the useState Hook

To get us started, let’s have a look at how I typically handle form state. I keep all fields as separate pieces of state, and update them all individually, which looks something like this:

function LoginForm() {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    api.login(email, password);
  }
  return (
    <form onSubmit={handleSubmit}>
      <div>
      <label htmlFor="email">Email</label>
      <input
        type="email"
        id="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      </div>
      <div>
      <label htmlFor="password">Password</label>
      <input
        type="password"
        id="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      </div>
    </form>
  );
}

First, we create two distinct pieces of state — email and password. These two variables are then passed to their respective input field, dictating the value of that field. Whenever something in a field changes, we make sure to update the state value, triggering a re-render of our app.

This works fine for most use cases and is simple, easy to follow, and not very magical. However, it’s pretty tedious to write out every single time.

Creating a custom Hook

Let’s make a small refactor, and create a custom Hook that improves our workflow slightly:

const useFormField = (initialValue: string = "") => {
  const [value, setValue] = React.useState(initialValue);
  const onChange = React.useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value),
    []
  );
  return { value, onChange };
};

export function LoginForm() {
  const emailField = useFormField();
  const passwordField = useFormField();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    api.login(emailField.value, passwordField.value);
  };
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          type="email"
          id="email"
          {...emailField}
        />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          type="password"
          id="password"
          {...passwordField}
        />
      </div>
    </form>
  );
}

We create a custom Hook useFormField that creates the change event handler for us, as well as keeps the value in state. When we use this, we can spread the result of the Hook onto any field, and things will work just as it did.

We made a custom demo for .
No really. Click here to check it out.

Handling lots of fields

One downside with this approach is that doesn’t scale as your form grows. For login fields, that’s probably fine, but when you’re creating user profile forms, you might want to ask for lots of information! Should we call our custom Hook over and over again?

Whenever I stumble across this kind of challenge, I tend to write a custom Hook that holds all my form state in one big chunk. It can look like this:

function useFormFields<T>(initialValues: T) {
  const [formFields, setFormFields] = React.useState<T>(initialValues);
  const createChangeHandler = (key: keyof T) => (
    e: React.ChangeEvent<HTMLInputElement>,
  ) => {
    const value = e.target.value;
    setFormFields((prev: T) => ({ ...prev, [key]: value }));
  };
  return { formFields, createChangeHandler };
}

export function LoginForm() {
  const { formFields, createChangeHandler } = useFormFields({
    email: "",
    password: "",
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    api.login(formFields.email, formFields.password);
  };
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          type="email"
          id="email"
          value={formFields.email}
          onChange={createChangeHandler("email")}
        />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          type="password"
          id="password"
          value={formFields.password}
          onChange={createChangeHandler("password")}
        />
      </div>
    </form>
  );
}

With this useFormFields Hook, we can keep on adding fields without adding complexity to our component. We can access all form state in a single place, and it looks neat and tidy. Sure, you might have to add an “escape hatch” and expose the underlying setState directly for some situations, but for most forms, this’ll do just fine.

An alternative approach

So handling the state explicitly works well, and is React’s recommended approach in most cases. But did you know there’s another way? As it turns out, the browser handles form state internally by default, and we can leverage that to simplify our code!

Here’s the same form, but letting the browser handle the state:

export function LoginForm() {
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const formData = new FormData(e.target as HTMLFormElement);
    api.login(formData.get('email'), formData.get('password'));
  };
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          type="email"
          id="email"
          name="email"
        />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          type="password"
          id="password"
          name="password"
        />
      </div>
      <button>Log in</button>
    </form>
  );
}

Now, that looks simple! Not a single Hook in sight, no setting the value, and no change listeners either. The best part is that it still works as before – but how?

You might have noticed we’re doing something a bit different in the handleSubmit function. We are using a built-in browser API called FormData. FormData is a handy (and well supported) way to get the field values from our input fields!

We get a reference to the form DOM element via the submit event’s target attribute and create a new instance of the FormData class. Now, we can get all fields by their name attribute by calling formData.get(‘name-of-input-field’).

This way, you never really need to handle the state explicitly. If you want default values (like if you’re populating initial field values from a database or local storage), React even provides you with a handy defaultValue prop to get that done as well!

We often hear “use the platform” used as a slight, but sometimes the platform just comes packing a punch.

When to use what

Since forms are such an integral part of most web applications, it’s important to know how to handle them. And React provides you with a lot of ways to do just that.

For simple forms that don’t require heavy validations (or that can rely on HTML5 form validation controls), I suggest that you just use the built-in state handling the DOM gives us by default. There are quite a few things you can’t do (like programmatically changing the input values or live validation), but for the most straightforward cases (like a search field or a login field like above), you’ll probably get away with our alternative approach.

When you’re doing custom validation or need to access some form data before you submit the form, handling the state explicitly with controlled components is what you want. You can use regular useStateHooks, or build a custom Hook solution to simplify your code a bit.

It’s worth noting that React itself recommends that you use controlled components (handling the state explicitly) for most cases – as it’s more powerful and gives you more flexibility down the line. I’d argue that you’re often trading simplicity for flexibility you don’t need.

Whatever you decide to use, handling forms in React has never been more straightforward than it is today. You can let the browser handle the simple forms while handling the state explicitly when the situation requires it. Either way – you’ll get the job done in less lines of code than ever before.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Kristofer Selbekk Bekker. Bulldog owner. Dad. React enthusiast. 🎩🥂🍔

6 Replies to “Creating forms in React in 2020”

  1. Nice post, wish when I read all these forms in react blogs though that they’d include nice ways the include validation

  2. Great article 👍
    There is a small typo in the first example. You entered username.. probably ment email

  3. I think, the approach you used for get data using FormData, it’s antipattern in react. Please see controlled components vs uncontrolled components.

Leave a Reply