swyxio / swyxdotio

This is the repo for swyx's blog - Blog content is created in github issues, then posted on swyx.io as blog pages! Comment/watch to follow along my blog within GitHub
https://swyx.io
MIT License
342 stars 45 forks source link

You May Not Need Controlled Form Components #373

Closed swyxio closed 2 years ago

swyxio commented 2 years ago

source: devto devToUrl: "https://dev.to/swyx/you-may-not-need-controlled-form-components-4e16" devToReactions: 39 devToReadingTime: 7 devToPublishedAt: "2020-03-09T23:29:43.747Z" devToViewsCount: 341 title: You May Not Need Controlled Form Components subtitle: Using the Name attribute in React Forms published: true description: A common design pattern for forms in React is using Controlled Components - but involves a lot of boilerplate code. Here's another way. category: tutorial tags: Tech, React, Webdev, JavaScript slug: no-controlled-forms displayed_publish_date: "2020-03-08"

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:

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