React Forms

February 27, 2020

This article is a written version of Rithm School’s React Forms lecture.

The goals of this lecture are to:

  1. Build forms in React
  2. Understand what Controlled Components are

Forms

HTML form elements work differently than other elements within React.

Form elements naturally keep some internal state, without any additional help on our part.

We've seen this in the past when working with vanilla JavaScript in the DOM: when a user enters an input value, we can reach into that input and grab the value stored in that element's internal state.

Say we have the following form element:

<form>
  <label for="fullName">Full Name:</label>
  <input name="fullName" />
  <button>Add</button>
</form>

It would be convenient to have a JavaScript function that both has access to the data the user entered and handles the submission of the form.

We'll accomplish this end using what is referred to as controlled components.

Controlled Components

As we pointed out above, form elements in HTML (like <input>, <textarea>, and <select>) maintain their own state and update it based on the user's input.

In React, mutable state is kept in the state of a component and only updated with the function returned to useState().

How can we use React to control the form's input state?

Single Source of Truth

In React, we want to make our state the "single source of truth."

In the context of forms, React would then control not only what is shown in the input (the value of the component), but also handle what happens when the user changes the input.

With control established over both aspects of an element (the display and the handling of the input changes), we have created what we refer to as a “controlled component”.

How Controlled Forms Work

Let's take a look at an example form:

<form>
  <label htmlFor="firstName">First Name:</label>
  <input id="firstName"
         name="firstName"
         value={formData.firstName}
         onChange={handleChange} />
</form>

Since the value attribute of our input is programatically set, the displayed value at any given moment will be what formData.firstName is set to in the component's state.

Meanwhile, in our component, our formData and handleChange function is defined as follows:

const NameForm = () => {
  // ...
  const [formData, setFormData] = useState({
    firstName: "",
    lastName: ""
  });

  const handleChange = (evt) => {
    setFullName(evt.target.value);
  }

  // ...
}

handleChange will run on every keystroke that the user makes, updating the value of firstName in the component's state. As a result, the displayed value will update as the user types.

With a controlled component, every state mutation will have an associated handler function. This makes it easy to modify or validate user input.

A Note on Labels

Labels (<label>) play an important role in forms: when associated with an input element that has an id with the same value, a user can click on the label and the corresponding input will be autofocused.

Because we are working in JavaScript, for is a reserved keyword. This means that we lose the ability to use for on our JSX elements.

Instead, we need to use the htmlFor attribute in place of for, just like we have to use className for class.

If you forget this, the console will warn you accordingly.

Handling Multiple Inputs

Before we look at how to elegantly handle multiple form inputs, let's first revisit some ES6 functionality.

Computed Property Names

In ES6, we got some new exciting new object enchancements, one of which included the ability to create objects with dynamic keys.

This feature is called computed property names.

To demonstrate this, take a look at how we'd create a dynamic object key in ES5:

var instructorData = {};
var instructorCode = "elie";
instructorData[instructorCode] = "Elie Schoppik";

Using ES6, we can now accomplish the same in a more readable way:

let instructorCode = "elie";
let instructorData = {
    // propery computed inside the object literal
    [instructorCode]: "Elie Schoppik"
};
Applying Computed Property Names to React Forms

Rather than making a separate onChange handler for every single input in our form, we can take advantage of computed property names and make a generic function that can be used for multiple inputs.

We'll start by adding the HTML name attribute to each JSX input element and let the handler function decide the corresponding key in the component state to update using the event.target.name value:

const [formData, setFormData] = useState({
  firstName: "",
  lastName: ""
});

const handleChange = evt => {
  const { name, value } = evt.target;
  setFormData(fData => ({
    ...fData,
    [name]: value
  }));
};

In the code above, you can see that when the onChange event is triggered, that event is passed to our handleChange event.

We are then, by way of destructuring, reaching into that evt.target and pulling out the name and value property values.

Finally, we are using that key and value to overwrite the existing key/value pair and updating the formData object in our component's state.

Passing Data Upward to a Parent Component

In React, we generally have downward data flow: parent components pass information down to simpler, child components.

That said, it is also common for form components to manage their own state.

So, what's the correct design choice here?

Often, we'll define a dedicated onSubmit function within our parent component, which we'll then pass down as a prop to our child component to use. This function will be connected to and update the parent's state when it's called.

In the child component, when the form is submitted and the onSubmit event is triggered, this passed function will be called.

With this setup, our child component remains sufficiently "dumb", knowing only to pass its own data into the function that was handed down from its parent.

Example: Shopping List

To illustrate the above, let's take a look at an example.

Say we have a shopping list application that features a ShoppingList and an AddItemForm, used to add items to the list.

In our ShoppingList, we have defined the function that will be passed down to our AddItemForm:

const addItem = item => {
  let newItem = { ...item, id: uuid() };
  setItems(items => [...items, newItem]);
};

In the child form component, AddItemForm, we grab that function and associated it with the onSubmit attribute on our form.

When the form is submitted, we pass in the formData from our state, which will then be used in the parent component, ShoppingList, to update the items on the list:

const handleSubmit = evt => {
  evt.preventDefault();
  addItem(formData);
  setFormData(INITIAL_STATE);
};

Keys and UUIDs

We've learned that, generally speaking, using an index as a key prop isn't a great idea.

But what do we do if there is no natural, unique key available for use?

In this case, we'll reach for a utility that create a Universally Unique Identifier (UUID).

To install in our project, we just run:

$ npm install uuid

To use the uuid module, we first import it and then call it in the place we'd like a unique id to be created:

import uuid from "uuid/v4";
  
/** Add new item object to cart. */
const addItem = item => {
  let newItem = { ...item, id: uuid() };
  setItems(items => [...items, newItem]);
};

Uncontrolled Components

Anytime that React is not in control of the state of an input, that input is known as an uncontrolled component.

Some inputs and external libraries will require this.

Validation

Validation plays a big role in a positive user experience. Users want to know if their actions are getting them closer to their intended outcome, and, if not, exactly what they need to adjust in order to reach those ends.

Form validation or any other validation on the UI is not a substitute for server-side validation. It is purely to create a better, more helpful experience for the user.

The utility that we recommend using for this purpose is Formik.

Testing Forms

To test typing into form inputs, we use fireEvent.change.

When we use this, we'll need to mock the evt.target.value in order to tell the React testing library what to place in the input.

For controlled components, the state will automatically update.

For example:

it("can add a new item", function() {
  const { getByLabelText, queryByText } = render(<ShoppingList />);

  // no items yet
  expect(queryByText("ice cream: 100")).not.toBeInTheDocument();

  const nameInput = getByLabelText("Name:");
  const qtyInput = getByLabelText("Qty:");
  const submitBtn = queryByText("Add a new item!")

  // fill out the form
  fireEvent.change(nameInput, { target: { value: "ice cream" }});
  fireEvent.change(qtyInput, { target: { value: 100 }});
  fireEvent.click(submitBtn);

  // item exists!
  expect(queryByText("ice cream: 100")).toBeInTheDocument();
});