Using Forms in React

By Dave Ceddia

No matter what kind of app you’re writing, there’s a good chance you need at least one form.

Forms in React are often a pain, filled with verbose and boilerplate-y code.

Let’s look at how to make forms in React with less pain.

In this article we’ll be focusing on using plain React, with no libraries. You’ll learn how forms really work, so you can confidently build them yourself. And if later you choose to add a form library, you’ll know how they work under the hood.

We’re going to cover:

  • How to create React forms without installing any libraries
  • The two styles of inputs in React forms
  • When to use Controlled vs. Uncontrolled inputs
  • An easy way to get values out of uncontrolled inputs

How to Create Forms with Plain React

Let’s dive right in. We’re going to build a simple contact form. Here’s the first iteration, a standalone component called ContactForm that renders a form:

function ContactForm() {
  return (
    <form>
      <div>
        <label htmlFor="name">Name</label>
        <input id="name" type="text" />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" type="email" />
      </div>
      <div>
        <label htmlFor="message">Message</label>
        <textarea id="message" />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

You don’t need to install a library to do any of this. React has built-in support for forms, because HTML and the DOM have built-in support for forms. At the end of the day, React is rendering DOM nodes.

In fact, for small forms, you probably don’t need a form library at all. Something like Formik or react-hook-form is overkill if all you need is a simple form.

There’s no state in here yet, and we’re not responding to form submission, but this component will already render a form you can interact with. (If you submit it, the page will reload, because submission is still being handled in the default way by the browser)

React Forms vs. HTML Forms

If you’ve worked with forms in plain HTML, a lot of this will probably seem familiar.

There’s a form tag, and labels for the inputs, same as you’d write in HTML.

Each label has an htmlFor prop that matches the id on its corresponding input. (That’s one difference: in HTML, the label attribute would be for. React uses htmlFor instead.)

If you haven’t done much with plain HTML, just know that React didn’t make this stuff up! The things React does are pretty limited, and the way forms work is borrowed from HTML and the DOM.

Two Kinds of Inputs: Controlled vs. Uncontrolled

Inputs in React can be one of two types: controlled or uncontrolled.

An uncontrolled input is the simpler of the two. It’s the closest to a plain HTML input. React puts it on the page, and the browser keeps track of the rest. When you need to access the input’s value, React provides a way to do that. Uncontrolled inputs require less code, but make it harder to do certain things.

With a controlled input, YOU explicitly control the value that the input displays. You have to write code to respond to keypresses, store the current value somewhere, and pass that value back to the input to be displayed. It’s a feedback loop with your code in the middle. It’s more manual work to wire these up, but they offer the most control.

Let’s look at these two styles in practice, applied to our contact form.

Controlled Inputs

With a controlled input, you write the code to manage the value explicitly.

You’ll need to create state to hold it, update that state when the value changes, and explicitly tell the input what value to display.

To update our contact form to use controlled inputs, we’ll need to add a few things, highlighted here:

function ContactForm() {
  const [name, setName] = React.useState('');
  const [email, setEmail] = React.useState('');
  const [message, setMessage] = React.useState('');

  function handleSubmit(event) {
    event.preventDefault();
    console.log('name:', name);
    console.log('email:', email);
    console.log('message:', message);
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name</label>
        <input
          id="name"
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

We’ve added 3 calls to useState to create 3 variables to hold the inputs’ values. They’re initially empty, ''.

Each input has gained a couple new props, too.

  • value tells the input what to display. Here, we’re passing the value from the corresponding state variable.
  • onChange is a function, and gets called when the user changes the input. It receives the event (commonly called e or event, but you can name it anything), and we take the input’s current value (e.target.value) and save it into state.

Notice how manual this is. With every keypress, our onChange gets called, and we explicitly setWhatever, which re-renders the whole ContactForm with the new value.

This means that with every keypress, the component will re-render the whole form.

For small forms this is fine. Really, it’s fine. Renders are fast. Rendering 3 or 5 or 10 inputs with every keypress is not going to perceptibly slow down the app.

If you have a form with tons of inputs though, this re-rendering might start to matter, especially on slower devices. At this point you might need to look into optimizations, in order to limit the re-renders to only the inputs that changed.

Or, consider how you could streamline the form so there are fewer inputs shown at once. If React isn’t happy about re-rendering 100 inputs on every keypress, I’d imagine your users aren’t very happy with seeing 100 inputs on a page either 😂

Alternatively…

Uncontrolled Inputs

If you do nothing beyond dropping an <input> in your render function, that input will be uncontrolled. You tell React to render the input, and the browser does the rest.

Uncontrolled inputs manage their own value. Just like with a plain HTML form, the value is kept in the input’s DOM node. No need to manually track it.

In the first code sample on this page, all the inputs were uncontrolled, because we weren’t passing the value prop that would tell them what value to display.

But if we’re not actively tracking the value… how can we tell what the value is?

Here’s where “refs” come in.

What is a “ref”?

React takes your JSX and constructs the actual DOM, which the browser displays. Refs tie these two representations together, letting your React component get access to the DOM nodes that represent it.

A ref holds a reference to a DOM node.

Here’s why that matters: The JSX you write is merely a description of the page you want to create. What you really need is the underlying DOM input, so that you can pull out the value.

So, to get the value from an uncontrolled input, you need a reference to it, which we get by assigning a ref prop. Then you can read out the value when the form is submitted (or really, whenever you want!).

Let’s add refs to our contact form inputs, building upon the “bare form” example from earlier:

function ContactForm() {
  const nameRef = React.useRef();
  const emailRef = React.useRef();
  const messageRef = React.useRef();

  return (
    <form>
      <div>
        <label htmlFor="name">Name</label>
        <input
          id="name"
          type="text"
          ref={nameRef}
        />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          ref={emailRef}
        />
      </div>
      <div>
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          ref={messageRef}
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

We did a couple things here:

  • created 3 refs with the useRef hook
  • bound the refs to the inputs with the ref prop

When the component is first rendered, React will set up the refs. nameRef.current will then refer to the name input’s DOM node, emailRef.current will refer to the email input, and so on.

These refs hold the same values as the ones you’d get if you ran a document.querySelector('input[id=name]') in your browser console. It’s the browser’s raw input node; React is just passing it back to you.

The last piece of the puzzle is how to get the values out of the inputs.

Uncontrolled inputs are the best choice when you only need to do something with the value at a specific time, such as when the form is submitted. (If you need to inspect/validate/transform the value on every keypress, use a controlled input)

We can create a function to handle form submission, and print out the values:

function ContactForm() {
  const nameRef = React.useRef();
  const emailRef = React.useRef();
  const messageRef = React.useRef();

  function handleSubmit(event) {
    event.preventDefault();
    console.log('name:', nameRef.current.value);
    console.log('email:', emailRef.current.value);
    console.log('message:', messageRef.current.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name</label>
        <input
          id="name"
          type="text"
          ref={nameRef}
        />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          ref={emailRef}
        />
      </div>
      <div>
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          ref={messageRef}
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

Your handleSubmit function can then do whatever you need with those values: validate them, asynchronously POST them to a server, etc.

Notice we’re calling event.preventDefault() at the top. Without this, submitting the form would refresh the page.

Controlled vs. Uncontrolled: Which to Use?

Let’t go over some pros and cons of each style of input so you can decide which you want to use.

(You might’ve heard that controlled inputs are a “best practice” which of course would imply uncontrolled inputs are NOT! 😱 I’ll address this near the end.)

When and Why to Use Controlled Inputs

Of the two styles, controlled inputs are the more “React-y way” of doing things, where UI reflects state. By changing the state, you change the UI. If you don’t change the state, the UI stays the same. You don’t meddle with the underlying input in an imperative, mutable way.

This makes controlled inputs perfect for things like:

  • Instantly validating the form on every keypress: useful if you want to keep the Submit button disabled until everything is valid, for instance.
  • Handling formatted input, like a credit card number field, or preventing certain characters from being typed.
  • Keeping multiple inputs in sync with each other when they’re based on the same data

The buck stops with you, dear developer. Want to ignore some weird character the user typed? Easy, just strip it out.

function EmailField() {
  const [email, setEmail] = useState('');

  const handleChange = e => {
    // no exclamations allowed!
    setEmail(e.target.value.replace(/!/g, ''));
  }

  return (
    <div>
      <label htmlFor="email">Email address</label>
      <input
        id="email"
        value={email}
        onChange={handleChange}
      />
    </div>
  );
}

There are plenty of use cases where you want to react to every keypress and handle it somehow. Controlled inputs are good for that.

But there are some downsides.

Controlled Inputs are More Complex

As we’ve already seen, you have to manually manage the value of the input, which means you need (a) state to hold it and (b) a change handler function, and you need those for every input.

You can work around part of this problem by combining the inputs into one state object:

function MultipleInputs() {
  const [values, setValues] = useState({
    email: '',
    name: ''
  });

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

  return (
    <>
      <div>
        <label htmlFor="email">Email address</label>
        <input
          id="email"
          name="email"
          value={values.email}
          onChange={handleChange}
        />
      </div>
      <div>
        <label htmlFor="name">Full Name</label>
        <input
          id="name"
          name="name"
          value={values.name}
          onChange={handleChange}
        />
      </div>
    </>
  );
}

It’s nicer, but it’s still code you need to write.

Boilerplate like this is one of the reasons React form libraries are so popular – but again, if you have 2 or 3 inputs on a page, I would argue that saving a few lines of tedium is not worth adding a form library.

Controlled Inputs Re-render on Every Keypress

Every time you press a key, React calls the function in theonChange prop, which sets the state. Setting the state causes the component and its children to re-render (unless they’re already optimized with React.memo or PureComponent).

This is mostly fine. Renders are fast. For small-to-medium forms you probably won’t even notice. And it’s not that rendering a piddly little input is slow… but it can be a problem in aggregate.

As the number of inputs grows – or if your form has child components that are expensive to render – keypresses might start to feel perceptibly laggy. This threshold is even lower on mobile devices.

It can become a problem of death-by-a-thousand-cuts.

If you start to suspect this problem in your app, fire up the Profiler in the React Developer Tools and take a measurement while you bash on some keys. It’ll tell you which components are slowing things down.

Uncontrolled Inputs Don’t Re-render

A big point in favor of using uncontrolled inputs is that the browser takes care of the whole thing.

You don’t need to update state, which means you don’t need to re-render. Every keypress bypasses React and goes straight to the browser.

Typing the letter 'a' into a form with 300 inputs will re-render exactly zero times, which means React can pretty much sit back and do nothing. Doing nothing is very performant.

Uncontrolled Inputs Can Have Even Less Boilerplate!

Earlier we looked at how to create references to inputs using useRef and pass them as the ref prop.

You can actually go a step further and remove the refs entirely, by taking advantage of the fact that a form knows about its own inputs.

function NoRefsForm() {
  const handleSubmit = e => {
    e.preventDefault();
    const form = e.target;
    console.log('email', form.email, form.elements.email);
    console.log('name', form.name, form.elements.name);
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email address</label>
        <input
          id="email"
          name="email"
        />
      </div>
      <div>
        <label htmlFor="name">Full Name</label>
        <input
          id="name"
          name="name"
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

The inputs are properties on the form itself, named by their id AND their name. Yep, both.

They’re also available at form.elements. Check it out:

function App() {
  const handleSubmit = (e) => {
    e.preventDefault();
    const form = e.target;
    console.log(
      form.email,
      form.elements.email,
      form.userEmail,
      form.elements.userEmail);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="userEmail">Email address</label>
        <input id="userEmail" name="email" />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

This prints the same input 4 times:

<input id="userEmail" name="email"></input>
<input id="userEmail" name="email"></input>
<input id="userEmail" name="email"></input>
<input id="userEmail" name="email"></input>

So we can leave off the redundant name prop from the input, if we don’t need it for anything else.

(we need to keep the id because the label’s htmlFor refers to that)

The form.elements array is useful if you need to loop over every input, like if you have a bunch of dynamically-generated ones or something.

Accessible Form Labels

Every input should have a label. Label-less inputs make trouble for screenreaders, which makes trouble for humans… and placeholder text unfortunately doesn’t cut it.

The two ways to do labels are:

Label Next To Input (2 sibling elements)

Give the input an id and the label an htmlFor that matches, and put the elements side-by-side. Order doesn’t matter, as long as the identifiers match up.

<label htmlFor="wat">Email address</label>
<input id="wat" name="email" />

Input Inside Label

If you wrap the input in a label, you don’t need the id and the htmlFor. You’ll want a way to refer to the input though, so give it an id or a name.

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

If you need more control over the style of the text, you can wrap it in a span.

Visually Hidden, But Still Accessible

You can hide the label with CSS if you need to.

Most of the big CSS frameworks have a screenreader-only class, often sr-only, that will hide the label in a way that screenreaders will still be able to read it. Here’s a generic sr-only implementation.

One nice thing about labels is, once you have them associated correctly, the browser will translate clicks on the label as clicks on the input. This is most noticeable with radio buttons – when the label is set up right, clicking the text will select the radio, but otherwise, it’ll frustratingly ignore you.

For more specifics see Lindsey’s post An Introduction to Accessible Labeling

Reduce Form Boilerplate With Small Components

So you’ve added your labels, but these inputs are getting longer and more repetitive…

<div>
  <label htmlFor="email">Email Address</label>
  <input name="email" id="email">
</div>

You can easily move this to a component, though!

function Input({ name, label }) {
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input name={name} id={name}>
    </div>
  );
}

Now every input is simple again.

<Input name="email" label="Email Address"/>

And if you’re using uncontrolled inputs, you can still use the trick of reading the values off the form, no refs or state required.

Is It a Best Practice to Use Controlled Inputs?

As of this writing, the React docs have a recommendation about inputs:

In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.

They go on to say that uncontrolled inputs are the easy way out:

It can also be slightly less code if you want to be quick and dirty. Otherwise, you should usually use controlled components.

The docs don’t exactly explain their reasoning, but my hunch is their recommendation stems from the fact that controlled inputs closely follow the state-driven approach, which is React’s whole reason for existing. Uncontrolled inputs are then treated as an “escape hatch” for when the state-driven approach won’t work for whatever reason.

I agreed with this line of thinking for a while, but I’m starting to have second thoughts.

I’m coming around to the idea that uncontrolled inputs might actually be the better default.

So this might get me some flak, but I’m going to say it anyway:

If uncontrolled inputs work for your case, use ‘em! They’re easier, and faster.

I don’t think I’m alone in this. The popular react-hook-form library uses uncontrolled inputs under the hood to make things fast. And I’ve seen some React thought leaders questioning why we don’t use uncontrolled inputs more often, too. Maybe it’s time to give it some thought!

Are Uncontrolled Inputs an Antipattern?

Uncontrolled inputs are a feature like any other, and they come with some tradeoffs (which we covered above), but they’re not an antipattern.

I tend to reserve the word “antipattern” for techniques that will come back to bite you later on. React has antipatterns like

  • mutating state instead of using immutability
  • duplicating values from props into state, and trying to keep them in sync
  • performing side effects in the body of a component function, instead of in a useEffect hook

These are things that sometimes appear to work just fine, but are ultimately the wrong way to do it, and will cause bugs down the road.

Uncontrolled inputs are a bit unconventional today, but using them is not “doing it wrong”. It’s a matter of picking the right tool for the job. If you go in knowing their limitations, and knowing your use case, then you can be pretty confident in your choice.

Go Make Forms!

I hope this overview of forms in React was helpful! There’s lots more I could cover but honestly this was too long already 😅 If you want to see more on forms, let me know in the comments.