You May Not Need Controlled Form Components

A common design pattern for forms in React is using Controlled Components - but involves a lot of boilerplate code. Here's another way.

react Posted:

2 common design patterns for forms in React are:

But a lower friction way to handle form inputs is to use HTML name attributes. As a bonus, your code often turns out less React specific!

Twitter discussion here.

Bottom Line Up Front

You can access HTML name attributes in event handlers:

// 31 lines of code
function NameForm() {
  const handleSubmit = (event) => {
    event.preventDefault();
    if (event.currentTarget.nameField.value === 'secretPassword') {
      alert('congrats you guessed the secret password!')
    } else if (event.currentTarget.nameField.value) {
      alert('this is a valid submission')
    }
  }
  const handleChange = event => {
    let isDisabled = false
    if (!event.currentTarget.nameField.value) isDisabled = true
    if (event.currentTarget.ageField.value <= 13) isDisabled = true
    event.currentTarget.submit.disabled = isDisabled
  }
  return (
    <form onSubmit={handleSubmit} onChange={handleChange}>
      <label>
        Name:
        <input type="text" name="nameField" placeholder="Must input a value"/>
      </label>
      <label>
        Age:
        <input type="number" name="ageField" placeholder="Must be >13" />
      </label>
      <div>
        <input type="submit" value="Submit" name="submit" disabled />
      </div>
    </form>
  );
}

Codepen Example here: https://codepen.io/swyx/pen/rNVpYjg

And you can do everything you'd do in vanilla HTML/JS, inside your React components.

Benefits:

  • This is fewer lines of code
  • a lot less duplicative naming of things
  • Event handler code works in vanilla JS, a lot more portable
  • Fewer rerenders
  • If SSR'ed, works without JS, with action attributes, (thanks Brian!)
  • you can supply a default value with value, as per native HTML, instead of having to use the React-specific defaultValue (thanks Li Hau!)

Controlled vs Uncontrolled Components

In the choice between Controlled and Uncontrolled Components, you basically swap a bunch of states for a bunch of refs. Uncontrolled Components are typically regarded to have less capabilities - If you click through the React docs on Uncontrolled Components you get this table:

feature uncontrolled controlled
one-time value retrieval (e.g. on submit)
validating on submit
field-level validation
conditionally disabling submit button
enforcing input format
several inputs for one piece of data
dynamic inputs

But this misses another option - which gives Uncontrolled Components pretty great capabilities almost matching up to the capabilities of Controlled Components, minus a ton of boilerplate.

Uncontrolled Components with Name attributes

You can do field-level validation, conditionally disabling submit button, enforcing input format, etc. in React components, without writing controlled components, and without using refs.

This is due to how Form events let you access name attributes by, well, name! All you do is set a name in one of those elements that go in a form:

<form onSubmit={handleSubmit}>
  <input type="text" name="nameField" />
</form>

and then when you have a form event, you can access it in your event handler:

const handleSubmit = event => {
  alert(event.currentTarget.nameField.value) // you can access nameField here!
}

That field is a proper reference to a DOM node, so you can do everything you'd normally do in vanilla JS with that, including setting its value!

const handleSubmit = event => {
  if (event.currentTarget.ageField.value < 13) {
     // age must be >= 13
     event.currentTarget.ageField.value = 13
  }
  // etc
}

And by the way, you aren't only restricted to using this at the form level. You can take advantage of event bubbling and throw an onChange onto the <form> as well, running that onChange ANY TIME AN INPUT FIRES AN ONCHANGE EVENT! Here's a full working form example with Codepen:

// 31 lines of code
function NameForm() {
  const handleSubmit = (event) => {
    event.preventDefault();
    if (event.currentTarget.nameField.value === 'secretPassword') {
      alert('congrats you guessed the secret password!')
    } else if (event.currentTarget.nameField.value) {
      alert('this is a valid submission')
    }
  }
  const handleChange = event => {
    let isDisabled = false
    if (!event.currentTarget.nameField.value) isDisabled = true
    if (event.currentTarget.ageField.value <= 13) isDisabled = true
    event.currentTarget.submit.disabled = isDisabled
  }
  return (
    <form onSubmit={handleSubmit} onChange={handleChange}>
      <label>
        Name:
        <input type="text" name="nameField" placeholder="Must input a value"/>
      </label>
      <label>
        Age:
        <input type="number" name="ageField" placeholder="Must be >13" />
      </label>
      <div>
        <input type="submit" value="Submit" name="submit" disabled />
      </div>
    </form>
  );
}

Codepen Example here: https://codepen.io/swyx/pen/rNVpYjg

Names only work on button, textarea, select, form, frame, iframe, img, a, input, object, map, param and meta elements, but that's pretty much everything you use inside a form. Here's the relevant HTML spec - (Thanks Thai!) so it seems to work for ID's as well, although I personally don't use ID's for this trick.

So we can update the table accordingly:

feature uncontrolled controlled uncontrolled with name attrs
one-time value retrieval (e.g. on submit)
validating on submit
field-level validation
conditionally disabling submit button
enforcing input format
several inputs for one piece of data
dynamic inputs 🤔

Almost there! but isn't field-level validation important?

setCustomValidity

Turns out the platform has a solution for that! You can use the Constraint Validation API aka field.setCustomValidity and form.checkValidity! woot!

Here's the answer courtesy of Manu!

const validateField = field => {
  if (field.name === "nameField") {
    field.setCustomValidity(!field.value ? "Name value is required" : "");
  } else if (field.name === "ageField") {
    field.setCustomValidity(+field.value <= 13 ? "Must be at least 13" : "");
  }
};

function NameForm() {
  const handleSubmit = event => {
    const form = event.currentTarget;
    event.preventDefault();

    for (const field of form.elements) {
      validateField(field);
    }

    if (!form.checkValidity()) {
      alert("form is not valid");
      return;
    }

    if (form.nameField.value === "secretPassword") {
      alert("congrats you guessed the secret password!");
    } else if (form.nameField.value) {
      alert("this is a valid submission");
    }
  };
  const handleChange = event => {
    const form = event.currentTarget;
    const field = event.target;

    validateField(field);

    // bug alert:
    // this is really hard to do properly when using form#onChange
    // right now, only the validity of the current field gets set.
    // enter a valid name and don't touch the age field => the button gets enabled
    // however I think disabling the submit button is not great ux anyways,
    // so maybe this problem is negligible?
    form.submit.disabled = !form.checkValidity();
  };
  return (
    <form onSubmit={handleSubmit} onChange={handleChange}>
      <label>
        Name:
        <input type="text" name="nameField" placeholder="Must input a value" />
        <span className="check" role="img" aria-label="valid">
          ✌🏻
        </span>
        <span className="cross" role="img" aria-label="invalid">
          👎🏻
        </span>
      </label>
      <label>
        Age:
        <input type="number" name="ageField" placeholder="Must be >13" />
        <span className="check" role="img" aria-label="valid">
          ✌🏻
        </span>
        <span className="cross" role="img" aria-label="invalid">
          👎🏻
        </span>
      </label>
      <div>
        <input type="submit" value="Submit" name="submit" disabled />
      </div>
    </form>
  );
}

Codesandbox Example here: https://codesandbox.io/s/eloquent-newton-8d1ke

More complex example with cross-dependencies: https://codesandbox.io/s/priceless-cdn-fsnk9

So lets update that table:

feature uncontrolled controlled uncontrolled with name attrs
one-time value retrieval (e.g. on submit)
validating on submit
field-level validation
conditionally disabling submit button
enforcing input format
several inputs for one piece of data
dynamic inputs 🤔

I am leaving dynamic inputs as an exercise for the reader :)

React Hook Form

If you'd like a library approach to this, BlueBill's React Hook Form seems similar, although my whole point is you don't NEED a library, you have all you need in vanilla HTML/JS!

So When To Use Controlled Form Components?

If you need a lot of field-level validation, I wouldn't be mad if you used Controlled Components :)

Honestly, when you need to do something higher powered than what I've shown, eg when you need to pass form data down to a child, or you need to guarantee a complete rerender when some data is changed (i.e. your form component is really, really big). We're basically cheating here by directly mutating DOM nodes in small amounts, and the whole reason we adopt React is to not do this at large scale!

In other words: Simple Forms probably don't need controlled form components, but Complex Forms (with a lot of cross dependencies and field level validation requirements) probably do. Do you have a Complex Form?

Passing data up to a parent or sibling would pretty much not need Controlled Components as you'd just be calling callbacks passed down to you as props.

Here's Bill's take:

I love this topic. Let's take a step back — everything is achievable with vanilla Javascript. For me the thing that React and other libraries offer is a smoother development experience, and most importantly making projects more maintainable and easy to reason with.

Here are some of my thoughts, hopefully people give equal opportunity to controlled and uncontrolled. They both have trade-offs. Pick the right tool to make your life easier.

Let React handle the re-render when it's necessary. I wouldn't say it's cheating on React instead letting React be involved when it needs to be.

Uncontrolled inputs are still a valid option for large and complex forms (and that's what I have been working with over the years professionally). It doesn't matter how big your form is it can always be breake apart and have validation applied accordingly.

References


Webmentions

See comments on Dev.to
Loading...