A Better Guide To Forms in React
Tags: webdev, react, javascript, nextjs
Published: 2023-10-11 03:01:40

The React ecosystem is lush with libraries, articles, videos, and more resources for every web development topic you could imagine. However, over time, many of these resources become outdated with modern best practices.

Working on a complex dynamic form recently for an AI side project, and researching good guides to React forms, I realized that most of the resources about building forms are outdated, often by many years.

This article will explain modern best practices for building forms in React, how to build dynamic forms, how forms relate to React Server Components, and more. Finally, after gaining an understanding of those topics, I’ll conclude with an explanation of what I found lacking in other guides, and recommendations from my experience with React.

## Controlled vs Uncontrolled

The key to understanding React forms is the idea of “controlled” vs “uncontrolled” inputs, which are two different methods of building forms in React.

Controlled forms store each input’s value in React state, and then set the value of each input on each re-render from that state. If another function updates the state, that will immediately be reflected in the components’ values. If your React logic does not render the form inputs, the values will still be maintained in your app’s state. Controlled inputs tend to give you more options in your React logic, such as complex, non-HTML-standard form validation as the user types (maybe checking password strength), code manipulation of the values of the inputs (such as hyphenating phone numbers as the user types).

They look something like this:

```jsx
import React, { useState } from 'react';

function ControlledForm() {
  const [value, setValue] = useState('');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const handleSubmit = () => {
    // no "submit event" param needed above ^
    sendInputValueToApi(value).then(() => /* Do something */)
  };

  return (
    <>
      <input type="text" value={value} onChange={handleChange} />   
      <button onClick={handleSubmit}>Send</button>
	</>
}
```

Note that it would be more semantically correct to wrap the input in a `<form>`, and give the input a name, but it’s not functionally **needed** because **the data is already saved in a state**, so we don’t need a real `onSubmit` event and our input doesn’t need to be accessed directly when the button is pressed.

There are a few downsides:

1. You may not want to re-render the form every time the user types.
2. You need to write a lot of code to manage complex forms, because you wind up with a lot of states as your form grows in size. That leads to boilerplate useState and state-setting, which expands your code. 
3. It’s harder to build dynamic forms where the number of fields is variable, because you can’t conditionally use hooks like useState. To fix this problem: (a) Your entire form state likely becomes a single, huge object, (b) Then all children components will re-render on every input change due to using an object as a state rather than primitives. (c) The only way around this, memoization, adds a lot more boilerplate.

This can lead to performance challenges with bigger forms like spreadsheets, tables, etc.

```jsx
import React, { useState } from "react";

function CumbersomeForm() {
  const [formData, setFormData] = useState({
    firstName: "",
    lastName: "",
    email: "",
    address: "",
    // ... potentially many more individual properties
  });

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

  return (
    <>
      <label>First Name:</label>
      <input
        type="text"
        name="firstName"
        value={formData.firstName}
        onChange={handleChange}
      />
      <label>Last Name:</label>
      <input
        type="text"
        name="lastName"
        value={formData.lastName}
        onChange={handleChange}
      />
      <label>Email:</label>
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
      />
      <label>Address:</label>
      <input
        type="text"
        name="address"
        value={formData.address}
        onChange={handleChange}
      />
      {/* ... potentially many more individual input fields */}
    </>
  );
}
```

On the other hand, **uncontrolled** inputs do **not** save the input values in React state. Instead, uncontrolled forms use the native, built-in `<form>` functionalities of vanilla HTML and JavaScript to manage data. So, for example, instead of setting a state with the input’s value every time it’s changed, and then setting its value prop, the browser manages the input value. Our React component never needs or uses the values - when the component is rendered, React will add the onsubmit event listener to the form, and when the submit button is pushed, our handleSubmit function runs. Instead of using React state, it’s much closer to the way a plain HTML form would work without any JavaScript.

```jsx
function UncontrolledForm() {
  
  const handleSubmit = (event) => {
    event.preventDefault();

    const formData = new FormData(event.target);
    const inputValue = formData.get('inputName');

    sendInputValueToApi(inputValue).then(() => /* Do something */)  
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="inputName" />
      <button type="submit">Send</button>
    </form>
  );
}
```
A benefit of this approach is that there’s less boilerplate. Compare these two inputs:
```jsx
// Controlled

const [value, setValue] = useState('');

const handleChange = (event) => {
    setValue(event.target.value);
};
...
<input type="text" value={value} onChange={handleChange} />

// Uncontrolled

<input type="text" name="inputName" />
```
The difference is significant even with 1 input, but becomes even more clear with many more inputs. Compare the longer controlled form above with this one:
```jsx
import React from "react";

function UncontrolledForm() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    for (let [key, value] of formData.entries()) {
      console.log(`${key}: ${value}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>First Name:</label>
      <input type="text" name="firstName" />

      <label>Last Name:</label>
      <input type="text" name="lastName" />

      <label>Email:</label>
      <input type="email" name="email" />

      <label>Address:</label>
      <input type="text" name="address" />
      {/* ... potentially many more individual input fields */}
      <button type="submit">Submit</button>
    </form>
  );
}
```
This form has less boilerplate than the controlled version, and we don’t need to manage multiple hook calls or a large complex state object. In fact, there is no state here at all. This form could have hundreds or thousands of children, and they will never cause each other to re-render. With this method, forms perform better, have less boilerplate, and are easier to reason about.

The downside of uncontrolled forms is that you don’t have direct access to each input’s value. This can make custom validation more difficult, and makes direct input value manipulation impossible.

## Caveats and Compromises
### (Don't) useRef
Many articles recommend using a ref on each input in uncontrolled forms instead of using `new FormData()` which I believe is because the [FormData API](https://developer.mozilla.org/en-US/docs/Web/API/FormData) is lesser-known. However, it’s been a standard for almost a decade and has been [supported by all major browsers for many years](https://caniuse.com/?search=formdata). I strongly recommend that you **do not use a useRef call for each form input**, as this introduces some of the same challenges and boilerplate as useState calls.

However, there are some use-cases where a ref can help.
1. Focus Management - programmatically focus on form elements, especially when dealing with dynamic form fields.
```jsx
function MyForm() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <form>
      <input ref={inputRef} type="text" />
      <button type="button" onClick={focusInput}>
        Focus Input
      </button>
    </form>
  );
}
```
2. Calling Methods on Child Components’ HTML Elements - for example, if your form is broken into subcomponents and you need to focus on one of them from your parent form, or a sibling of the subcomponent, you need to pass around a ref from the parent, like this:
```jsx
const ChildComponent = React.forwardRef((props, ref) => (
  <input ref={ref} type="text" />
));

function MyForm() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <form>
      <ChildComponent ref={inputRef} />
      <button type="button" onClick={focusInput}>
        Focus Input
      </button>
    </form>
  );
}
```
3. Other, non-form-specific ref use-cases - such as saving previous values with a useEffect, or measuring the physical size of an element like a textarea that a user can expand

### Mixing Controlled and Uncontrolled
In many cases, you may need to control one or a few inputs. Manipulating a phone number as the user types it in is a great example. In those cases, even if you’re using an uncontrolled form, you can just use one controlled input. In that case, don’t use the state to access the input’s value in form submission - continue using `new FormData(...)` - just use the state to manage the display of the input in question.

```jsx
function MixedForm() {
  const [phoneNumber, setPhoneNumber] = useState("");

  const handlePhoneNumberChange = (event) => {
    // Some function to format the phone number
    const formattedNumber = formatPhoneNumber(event.target.value);
    setPhoneNumber(formattedNumber);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    for (let [key, value] of formData.entries()) {
      console.log(`${key}: ${value}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name:</label>
      <input type="text" name="name" />

      <label>Email:</label>
      <input type="email" name="email" />

      <label>Phone Number:</label>
      <input
        type="tel"
        name="phoneNumber"
        value={phoneNumber}
        onChange={handlePhoneNumberChange}
      />

      <label>Address:</label>
      <input type="text" name="address" />

      <button type="submit">Submit</button>
    </form>
  );
}

// Helper function for phone number formatting (just an example)
function formatPhoneNumber(number) {
  // Format the phone number as desired. This is just a basic example.
  return number.replace(/\D/g, "").slice(0, 10);
}
```
Note: minimize states. In this example, you wouldn’t want a useState for saving the raw phone number, and a separate useState for saving the formatted phone number, with an effect to sync them - that would cause unnecessary re-renders.

Speaking of optimizing re-renders, we can also move the controlled input to its own component to minimize re-renders of the rest of the form.

```jsx
const PhoneInput = () => {
  const [phoneNumber, setPhoneNumber] = useState("");

  const handlePhoneNumberChange = (event) => {
    // Some function to format the phone number
    const formattedNumber = formatPhoneNumber(event.target.value);
    setPhoneNumber(formattedNumber);
  };

  return (
    <input
      type="tel"
      name="phoneNumber"
      value={phoneNumber}
      onChange={handlePhoneNumberChange}
    />
  );
};

function MixedForm() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    for (let [key, value] of formData.entries()) {
      console.log(`${key}: ${value}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name:</label>
      <input type="text" name="name" />

      <label>Email:</label>
      <input type="email" name="email" />

      <label>Phone Number:</label>
      <PhoneInput />

      <label>Address:</label>
      <input type="text" name="address" />

      <button type="submit">Submit</button>
    </form>
  );
}

// Helper function for phone number formatting (just an example)
function formatPhoneNumber(number) {
  // Format the phone number as desired. This is just a basic example.
  return number.replace(/\D/g, "").slice(0, 10);
}
```
If you’re used to controlled inputs, then on a first glance at the code above, you might think, “how does the parent form know the value of the child phone input if it’s not passing in some state setter or a ref?” To understand that, **remember that when the React code is rendered as HTML, the browser will only “see” the HTML for the form and the inputs inside, including the input which the PhoneInput renders** - the structure of our React components has no impact on the functionality of our rendered HTML. Then, that input value will be included in the FormData like any other field in the form. This is the power of encapsulation and composition of components in React. We can localize the re-renders to the minimum-affected JSX, while the DOM all still comes together as native HTML.

This is also useful for building dynamic forms. If you were. managing a state object using controlled components, you'd need to add and remove properties manually when form fields were created or removed. On the other hand, with uncontrolled components, you can just render/unrender inputs with unique names, and their values will automatically appear in the FormData on-submit.

## But wait… how do I validate input values in uncontrolled inputs?
If you thought of this, you’re not alone! When validation is required before submitting a form, React devs tend to jump to using controlled components with state.

Many developers don’t realize that you don’t always need React or custom JavaScript for these types of validations - in fact, the web platform has them built in as attributes on form inputs! Check out this page on MDN for the details: [https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation)

Without any JS, you can require input values, set length requirements, and set format requirements using regular expressions (AI tools like ChatGPT are great for helping with complex Regex)

## Error Handling
On a related topic, devs tend to reach for controlled components when we need to show some validation errors on the client-side. However, instead of using controlled components and setting values on every input change, when the requirements allow it I would prefer to use uncontrolled components and manage error handling in my onSubmit function. This minimizes the amount of state and state updates in your forms. Maybe it looks like this:

```jsx
function UncontrolledForm() {
  const [errors, setErrors] = useState({});

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);

    let validationErrors = {};

    // Custom validation: Ensure the email domain is "example.com"
    const email = formData.get("email");
    if (email && !email.endsWith("@example.com")) {
      validationErrors.email = "Email must be from the domain example.com.";
    }

    if (formData.get("phoneNumber").length !== 10) {
      validationErrors.phoneNumber = "Phone number must be 10 digits.";
    }

    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
    } else {
      // Handle the successful form submission, e.g., sending formData to a server
      console.log(Array.from(formData.entries()));
      setErrors({}); // Clear any previous errors
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name:</label>
      <input type="text" name="name" required />
      {errors.name && <div className="error">{errors.name}</div>}

      <label>Email (must be @example.com):</label>
      <input type="email" name="email" required />
      {errors.email && <div className="error">{errors.email}</div>}

      <label>Phone Number (10 digits):</label>
      <input type="tel" name="phoneNumber" required pattern="\d{10}" />
      {errors.phoneNumber && <div className="error">{errors.phoneNumber}</div>}

      <button type="submit">Submit</button>
    </form>
  );
}

export default UncontrolledForm;
```

There’s one more big benefit to using uncontrolled components.

## Forms in React Server Components

React Server Components (RSC) uses server-side frameworks to render some of your components and thereby reduce the amount of JavaScript that web browsers download to run your website. This can drastically improve the performance of your website.

RSC has a big impact on the way we write forms, because, for the first time, **if our form inputs do not use state, they can be rendered to HTML on the server and ship 0 JavaScript to the browser**. This means that **uncontrolled** forms can be interactive without JavaScript, making them function earlier rather than waiting for JS to download and execute - in effect, it makes your website feel snappier, there’s no delay to interactivity.

Using Next.js, you can also use Server Actions in your forms, so you don’t need to write an API for your form to interact with - all you need is a handler function. You can find out more info about this topic from the [Next.js docs](https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations) or [Lee’s excellent explainer video here](https://www.youtube.com/watch?v=dDpZfOQBMaU&ab_channel=LeeRobinson).

If you build a mixed form with some controlled inputs in an RSC-rendered form, **make sure to encapsulate your controlled components into a separate client-component**, similar to the `<PhoneInput />` component from earlier, but in its own file. This ensures that only the minimum amount of JavaScript needed will be bundled. It may look like this:
```jsx
// page.jsx
import { PhoneInput } from "./PhoneInput";

export default function Page() {
  async function create(formData: FormData) {
    "use server";

    // ... use the FormData
  }

  return (
    <form action={create}>
      <label>Name:</label>
      <input type="text" name="name" />

      <label>Email:</label>
      <input type="email" name="email" />

      <label>Phone Number:</label>
      <PhoneInput />

      <label>Address:</label>
      <input type="text" name="address" />

      <button type="submit">Submit</button>
    </form>
  );
}

// PhoneInput.jsx
"use client";

// Helper function for phone number formatting (just an example)
function formatPhoneNumber(number) {
  // Format the phone number as desired. This is just a basic example.
  return number.replace(/\D/g, "").slice(0, 10);
}

import { useState } from "react";

export const PhoneInput = () => {
  const handlePhoneNumberChange = (event) => {
    // Some function to format the phone number
    const formattedNumber = formatPhoneNumber(event.target.value);
    setPhoneNumber(formattedNumber);
  };
  const [phoneNumber, setPhoneNumber] = useState("");

  return (
    <input
      type="tel"
      name="phoneNumber"
      value={phoneNumber}
      onChange={handlePhoneNumberChange}
    />
  );
};
```
## Form Libraries
There are many great form libraries in the React ecosystem, designed for controlled inputs. Recently I’ve been using React Hook Form for those applications, though I’ve been leaning more heavily on uncontrolled inputs so I don’t need a library to manage form state. (Edit: some popular options are React Hook Form, Formik, and Informed)

## Conclusions, Comparisons, and Recommendations
This article was motivated by the confusing, out-dated, and/or misleading articles that show up near the top of the Google search results for “react forms.” 

- One of those articles says “the more idiomatic React way would be using controlled forms” - I don’t think either controlled or uncontrolled inputs are more “React idiomatic.” In reality, both types have their place, as discussed above. In fact, many older articles seem to recommend controlled inputs, for similarly vague or misleading reasons. I would argue that using uncontrolled inputs is actually more “web idiomatic” by nature of relying on less JavaScript, which allows uncontrolled inputs to shine in React Server Components.
- Not one of the top articles uses FormData. For uncontrolled forms, at least two recommend multiple useRef calls, making your code inflexible and more boilerplatey.
- Some of the top articles still use class components with no mention of functional components.

Some thoughts in conclusion:

1. In my experience, most forms are a mixture of uncontrolled and controlled inputs. The reason we have both of those options is flexibility and we shouldn’t be dogmatic. We can use them both together, as in the React Server Components example above (although server components are not needed for that example).
2. Today, I favor uncontrolled inputs wherever I can. I believe this simplifies the code structure and performance.
3. Use “new FormData(...)” in your onSubmit functions instead of using refs. Seriously.
4. Use composition and encapsulation for controlled inputs to minimize the impact of state updates on other components, and rely on the rendered, composed DOM for submit events.

_I hope this article helps someone! Feel free to leave questions in the comments._