React Forms - Basics

Chapter 21 18 mins

Learning outcomes:

  1. Tapping into form submission
  2. What are controlled components
  3. What are uncontrolled components
  4. Retrieving form data upon submission

Introduction

It's no surprise to any one of us that forms are one of the most used features of the web. They essentially power the fabric of many online businesses, helping them collect email signups, order placements, job applications, subscriptions, user accounts, and whatnot.

The practical use cases of forms are endless.

In the early days of the web, forms were mostly dealt with by backend languages, handling all the data, validating it, and then sending an appropriate feedback or response to the end user.

Today, this has changed quite a lot. Now, we have forms largely being handled on the frontend, with immediate feedback provided to users as they interact with input elements, and all this is made possible by the one and only, JavaScript language.

Since React is a JavaScript library meant to make creating user interfaces a lot more intuitive and scalable, it makes perfect sense for it to think about abstracting some of the complexity of forms in one way or the other.

In this unit, we shall see how React aims to specially treat all forms inputs as controlled components, which makes for easier access of the input data. Then we shall also touch upon the opposite of this, uncontrolled components, and why would anyone want to use them.

There is a lot to cover in this unit, so let's get started without further ado.

Tapping into form submission

In HTML, by default, when a form is submitted, either by pressing the Enter key while focus is inside a form control associated with the form or by pressing a submit button therein, an HTTP request is made.

This HTTP request, whose method depends upon the method attribute set in the <form> element, carries the data entered into the form back to the server.

However, these days it's more than common to tap into this normal behavior and prevent it from happening. These days, forms are quite frequently kept from dispatching HTTP requests by themselves; instead, JavaScript APIs — fetch() and XMLHttpRequest — are used to manually dispatch requests, sending the form data typically in the JSON format.

How exactly the data is sent or which APIs are used isn't important for now; what's important is how to prevent the default submission behavior of a form in React.

Fortunately, it's really easy to do so, thanks to the onSubmit event handler prop of a <form> element.

Let's see an example.

In the following code, we have a very basic form, with a text input field and a submit button:

function App() {
   return (
      <form>
         <input type="text"/>
         <button>Submit</button>
      </form>
   );
}

Currently, since there's no event handler configured on the <form>, submitting it would end up with the default HTML form submission behavior — dispatching an HTTP request to the given page denoted by the form's action.

When the action attribute isn't specified on a <form> element, it is assumed to be the current webpage.

Now, let's set up an onSubmit handler on the <form> and prevent the event's default action in there:

function App() {
   function handleSubmit(e) {
      e.preventDefault();
      alert('Form submitted');
   }
   return (
      <form onSubmit={handleSubmit}>
         <input type="text"/>
         <button>Submit</button>
      </form>
   );
}

Live Example

If we now submit the form, we don't get any browser reload triggered whatsoever by virtue of a form submission; the page remains as it is and an alert is made, reading 'Form submitted'. This confirms the prevention of the form's submission.

This is how forms are typically configured in React applications, although obviously there's no necessity of doing so.

Controlled components

We'll get back to forms in a while. Before that, we need to spare a few minutes in understanding controlled form components in React and getting familiar with one of the two different ways of directly interacting with form controls in React.

By design, React tries to control the data of form inputs and prevent their default event activation behaviors to bring about a unidirectional flow of data, with a single source of truth. (Don't worry, we'll learn what all this means later on in this unit.)

When form input components are configured this way in React, they are referred to as controlled components.

Understanding this design choice requires a little bit more detailed discussion, which we carry out separately in the next chapter, React Forms — Controlled Components.

So what makes a form component controlled?

Well, when a form input contains the value prop, React treats it as a controlled component.

As simple as that.

Most importantly though, once a component becomes controlled, it effectively becomes read-only unless we set up some value of manually changing its value prop. Usually, this is done by setting up an event handler, causing a state value to be changed, which is then fed back into the form input.

Let's consider an example to help understand this better.

In the following code, we have an App component with a state name and an input field whose value is set to name. The onChange handler serves to mutate this state value as soon as data is entered into the input field:

function App() {
   const [name, setName] = useState('');
   return (
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
   );
}

Live Example

You might be thinking that how come the onChange handler gets fired as data is entered into the field, for the change event in JavaScript typically fires once an input field loses focus with its value changed.

Hmm. This is a really good observation.

Surprisingly, but sensibly as we feel, React improvises in when the onChange handler fires. In particular, onChange fires upon the occurrence of the input event on a form input, NOT the change event!

That why is this so, we shall find out in the next chapter.

Coming back to the code above, as soon as the onChange handler fires due to data being entered into the input field, the name state changes and thus the App component gets re-rendered. In this new render, the <input> element's value prop changes and thus it showcases the newly-entered value.

In short, a controlled component always relies upon three essential things:

  • A state value
  • The value prop assigned the state
  • An event handler changing the state

Let's consider another example, this time demonstrating a newsletter signup form.

We have two controlled components, one being an input field asking for the email, and the other a checkbox obtaining consent for sending out emails:

function App() {
   const [email, setEmail] = useState('');
   const [accepted, setAccepted] = useState('');
   return (
      <>
         <input type="text" value={email} onChange={e => setEmail(e.target.value)} />
         <label>
            <input type="checkbox" value={accepted} onChange={e => setAccepted(!accepted)} />
            Agree to terms.
         </label>
      </>
   );
}

Live Example

Using controlled components, the data of a form input is always stored in the state of the parent component (or at least one of the ancestor components).

Controlled components are part and parcel of the design ideology of React, or unidirectional data flow, and thus are the recommended approach of defining form inputs.

However, sometimes we might seek for React to let go off this extra control on our form inputs. In such cases, we really seek the other kind of form inputs in React — uncontrolled components.

Uncontrolled components

When form input components in React don't behave in the aforementioned way, that is, when React doesn't control their data, they are referred to as uncontrolled components.

If we wish to create an uncontrolled component, we have to hold on to our desire of adding the value prop to the form input component.

Since there are a couple of additional aspects of working with uncontrolled components as well, just like with controlled components, we discuss them in a separate upcoming chapter as well, React Forms — Uncontrolled Components.

Consider the following code, same as the one above except that we don't have any value prop on the <input>:

function App() {
   return (
      <input type="text" />
   );
}

If we try typing into the input here, we'll get the usual browser behavior — the entered data would appear right away inside the field.

Live Example

That's because React doesn't control the data of the <input> element here (as there's no value prop set on it).

Moving on, in order to retrieve the value of an uncontrolled component inside its parent, we customarily use a ref.

This is demonstrated as follows:

function App() {
   const inputRef = useRef();
   return (
      <>
         <input ref={inputRef} type="text" />
         <button onClick={() => alert(inputRef.current.value)}>Get value</button>
      </>
   );
}

Live Example

In the following section, we shall see another application of refs albeit a lot less common in real-world apps containing complex forms.

Retrieving form data upon submission

As promised above, let's now get back to forms.

We saw above how to prevent the default form submission behavior in React by employing an onSubmit handler on a <form> element and calling preventDefault() therein.

Now once a form is submitted and its default action prevented, the very next logical step is to retrieve all the data in the form inside the onSubmit handler.

There are mainly four ways to do so, in order of reducing usage frequency:

  • Use controlled components, storing the value of each form input as part of the state of its form, in one single object.
  • Use the FormData interface, processing the form data using its methods.
  • Use the <form>'s elements property to find the form inputs we're interested in and then retrieve their values. This approach usually requires setting name on form inputs.
  • Use refs in order to get the DOM nodes of uncontrolled form inputs and then retrieve their values by a usual property access (such as value, or checked, and so on).

Let's see each of these one-by-one.

We'll be demonstrating each approach using the following form:

function App() {
   function handleSubmit(e) {
      e.preventDefault();
   }
   return (
      <form onSubmit={handleSubmit}>
         <input type="text" name="firstName" placeholder="First name" />
         <input type="text" name="lastName" placeholder="Second name" />
         <br/>
         <label>
            <input type="radio" name="gender" value="male" /> Male
         </label>
         <label>
            <input type="radio" name="gender" value="female" /> Female
         </label>
         <br/>
         <label>
            <input type="checkbox" name="accepted" /> Accept terms and conditions.
         </label>
         <br/>
         <button>Submit</button>
      </form>
   );
}

There are two text inputs, two radio inputs, and one checkbox input.

Live Example

Controlled components

Using controlled components, we start off by creating an object containing all the properties corresponding to the different inputs in the form.

This object is filled with initial values for each of the form inputs.

For example, we want the first name field in the form to begin with an empty value, henceforth we'd have a firstName property on this state object set to an empty string (''). Checkboxes typically have Boolean values.

Consider the following code:

import { useState } from 'react';

function App() {
const [data, setData] = useState({ firstName: '', lastName: '', gender: '', accepted: false, });
function handleSubmit(e) { e.preventDefault(); } return ( <form onSubmit={handleSubmit}> <input type="text" name="firstName" placeholder="First name" /> <input type="text" name="lastName" placeholder="Second name" /> <br/> <label> <input type="radio" name="gender" value="male" /> Male </label> <label> <input type="radio" name="gender" value="female" /> Female </label> <br/> <label> <input type="checkbox" name="accepted" /> Accept terms and conditions. </label> <br/> <button>Submit</button> </form> ); }

The names of the properties customarily resemble the names of the form inputs. For example, the firstName property of the data state corresponds to the first <input> element whose name is "firstName".

Next up, each form input's value is set to the corresponding property from this state object and/or an onChange handler is set in order to update it.

In the following code, we add the value and/or onChange props to the form inputs while also re-formatting the code for better readability:

function App() {
   const [data, setData] = useState({
      firstName: '',
      lastName: '',
      gender: '',
      accepted: false,
   });
   function handleSubmit(e) {
      e.preventDefault();
   }
   return (
      <form onSubmit={handleSubmit}>
         <input
            type="text"
            name="firstName"
            placeholder="First name"
            value={data.firstName}
            onChange={e => setData({ ...data, firstName: e.target.value })}
         />
         <input
            type="text"
            name="lastName"
            placeholder="Second name"
            value={data.lastName}
            onChange={e => setData({ ...data, lastName: e.target.value })}
         />
         <br/>
         <label>
            <input
               type="radio"
               name="gender"
               value="male"
               checked={data.gender === 'male'}
               onChange={e => setData({ ...data, gender: e.target.value })}
            /> Male
         </label>
         <label>
            <input
               type="radio"
               name="gender"
               value="female"
               checked={data.gender === 'female'}
               onChange={e => setData({ ...data, gender: e.target.value })}
            /> Female
         </label>
         <br/>
         <label>
            <input
               type="checkbox"
               name="accepted"
               checked={data.accepted}
               onChange={e => setData({ ...data, accepted: !data.accepted })}
            /> Accept terms and conditions.
         </label>
         <br/>
         <button>Submit</button>
      </form>
   );
}

Using ES6 object destructuring, it's really easy to update the data state, modifying only the concerned property in there.

Here's the final code, logging all the form data inside the handleSubmit() function:

function App() {
   const [data, setData] = useState({
      firstName: '',
      lastName: '',
      gender: '',
      accepted: false,
   });
   function handleSubmit(e) {
      e.preventDefault();
console.log(data); } return ( <form onSubmit={handleSubmit}> ... </form> ); }

Live Example

Many form handling libraries in React, including the well-known Formik and React Hook Form, rely on this very approach for handling form data, i.e. they create form inputs as controlled components and then write all form handling logic around that.

Benefits and disadvantages

The biggest benefit of this approach is that the data of the form is readily and easily available within the containing component.

The only disadvantage (which isn't really a disadvantage per se) is that there's a lot of boilerplate code to be written for each form input. In particular, each form input needs value/checked and an onChange handler updating the corresponding property in the state object.

<form>'s elements property

In the HTML DOM, a <form> element's node has an elements property available on it that holds an object containing all the input elements of the <form>.

Most importantly, this elements object has named properties corresponding to the names of given form inputs. Using this object and its named properties, we can extract the data from a given form upon its submission.

Below we demonstrate this for the form given above:

function App() {
function handleSubmit(e) { e.preventDefault(); const formInputElements = e.target.elements; const data = {}; data.firstName = formInputElements.firstName.value; data.lastName = formInputElements.lastName.value; data.gender = formInputElements.gender.value; data.accepted = formInputElements.accepted.checked; console.log(data); }
return ( <form onSubmit={handleSubmit}> <input type="text" name="firstName" placeholder="First name" /> <input type="text" name="lastName" placeholder="Second name" /> <br/> <label> <input type="radio" name="gender" value="male" /> Male </label> <label> <input type="radio" name="gender" value="female" /> Female </label> <br/> <label> <input type="checkbox" name="accepted" /> Accept terms and conditions. </label> <br/> <button>Submit</button> </form> ); }

All in all, using the elements property of the <form> DOM element, we are able to granularly retrieve any of its inputs' values.

Live Example

The DOM is a behemoth in terms of utilities — there are more utilities in it than we can imagine!

Over to the second approach — using the FormData interface.

The FormData interface

Inside the onSubmit handler of a <form>, we can instantiate a FormData instance by calling the FormData() constructor and providing it the DOM node of the <form> we wish to process.

This FormData instance can then be used to iterate over the entire set of input name-value pairs in the form.

Here's how we'd extract data from the form shown above:

function App() {
function handleSubmit(e) { e.preventDefault(); const formData = new FormData(e.target); const data = {}; for (let [key, value] of formData) { data[key] = value; } console.log(data); }
return ( <form onSubmit={handleSubmit}> <input type="text" name="firstName" placeholder="First name" /> <input type="text" name="lastName" placeholder="Second name" /> <br/> <label> <input type="radio" name="gender" value="male" /> Male </label> <label> <input type="radio" name="gender" value="female" /> Female </label> <br/> <label> <input type="checkbox" name="accepted" /> Accept terms and conditions. </label> <br/> <button>Submit</button> </form> ); }

The for...of loop allows us to iterate over a FormData instance, going through each of its input key-value pairs.

Live Example

Do keep in mind that empty radio and checkbox inputs don't show up in a FormData object. This means that we need to do some extra processing if we want to have all the keys corresponding to these inputs in our form.

FormData is a really handy API because it can also be used to process file data in forms, again very common in modern-day web apps.

Use multiple refs

Perhaps the least common of these methods is the one utilizing refs. For each form input, we require one ref.

Here's the code to access the input data in the form above:

function App() {
const firstNameInputRef = useRef(); const lastNameInputRef = useRef(); const maleGenderInputRef = useRef(); const femaleGenderInputRef = useRef(); const acceptedInputRef = useRef(); function handleSubmit(e) { e.preventDefault(); const data = { firstName: firstNameInputRef.current.value, lastName: lastNameInputRef.current.value, gender: (maleGenderInputRef.current.checked ? 'male' : femaleGenderInputRef.current.checked ? 'female' : ''), accepted: acceptedInputRef.current.checked }; console.log(data); }
return ( <form onSubmit={handleSubmit}> <input ref={firstNameInputRef} type="text" name="firstName" placeholder="First name" /> <input ref={lastNameInputRef} type="text" name="lastName" placeholder="Second name" /> <br/> <label> <input ref={maleGenderInputRef} type="radio" name="gender" value="male" /> Male </label> <label> <input ref={femaleGenderInputRef} type="radio" name="gender" value="female" /> Female </label> <br/> <label> <input ref={acceptedInputRef} type="checkbox" name="accepted" /> Accept terms and conditions. </label> <br/> <button>Submit</button> </form> ); }

Live Example

As you'd agree, the biggest disadvantage of this approach is that we need to create as many refs as there are inputs (with as many calls to useRef). What if we have 100 inputs? You can see where this is going.

Plus, there is also boilerplating to be done with refs — creating a separate ref for each form input and then setting the ref on the form input (using ref).

And this completes all the four different approaches to retrieving form data upon submission.

Honestly speaking, which approach would work best depends upon the use case at hand and on the comfort of the developer as well. But obviously there are logical factors affecting such decisions as well.

For example, there is very little point in using refs (the last approach discussed above). Similarly, if we are dealing with files, we might be better off at using FormData which handles all the complexity of generating file payload for an HTTP request.

Without any doubt, the de-facto in React is to use controlled form components, with the data of each component readily available to us via a unified state object.