Controlled vs uncontrolled forms in React

·

3 min read

The form

Let's say you have a simple form in React with an input and a button.

const submitForm = (e) => {
  e.PreventDefault();
  //do something with input
}

return (
  <form onSubmit={submitForm}>
    <label htmlFor="myInput">
    <input id="myInput" />
    <button>Submit</button>
  </form>
)

Just to note, whereas in HTML you use label for, in React it's label htmlFor

Controlled form

When you submit the form you want to do something with what's in the input. The 'React' method of doing this is to use a state hook:

const [inputState, setInputState] = useState('');

const submitForm = (e) => {
  e.PreventDefault();
  console.log(inputState);
}

return (
  <form>
    <label htmlFor="myInput">
    <input
      id="myInput"
      value={inputState}
      onChange={(e) => setInputState(e.target.value} />
    <button>Submit</button>
  </form>
)

This is called a controlled form because React is controlling the form's value. For every character the user types we set the state to be the whole input and show that input in the input field.

The alternative is an uncontrolled form. There are two ways of doing this, one with useRef and one which looks even more like ordinary JavaScript.

Uncontrolled form: useRef

One of the things that useRef does is to refer to a DOM element, so you can refer to it the same way you do in JavaScript.

const inputRef = useRef();

const submitForm = (e) => {
  e.PreventDefault();
  console.log(inputRef.value);
}

return (
  <form>
    <label htmlFor="myInput">
    <input id="myInput" ref={inputRef} />
    <button>Submit</button>
  </form>
)

To me it feels a bit like cheating at React, because I'm making it more like JavaScript and therefore going with what I know. But in reality there's nothing wrong with this method.

Uncontrolled form: JavaScript

Sometimes you don't have to re-invent the wheel.

const submitForm = (e) => {
  e.PreventDefault();
  console.log(e.currentTarget.myInput);
}

return (
  <form>
    <label htmlFor="myInput">
    <input id="myInput" />
    <button>Submit</button>
  </form>
)

Simple, right? I like this one because it feels 'normal' ie what you do in JavaScript. And it involves less coding.

Although I did find that Jest/React Testing Library couldn't find e.currentTarget.myInput - I had to refer to e.currentTarget.children[1] instead (the first child is the label).

Which to use

If you have something this small and simple then it depends a bit on personal preference and what fits in with whatever else you have in your app.

If you have a complicated input that you want to validate as it goes or only enable the submit button once they've typed a certain number of characters, then a controlled form allows you to control that.

However, the trouble with a controlled form is that every time the onChange handler updates, the whole thing re-renders. Which wouldn't be too much of a problem in this simple example because there's not a lot to re-render. But if your form is long with lots of inputs all causing re-renders on every character typed, it's going to slow everything down.

If you're wondering why I am submitting the form rather than clicking the button, I have a post about that.