2 common design patterns for forms in React are:
- using Controlled Components but it involves a lot of boilerplate code with a bunch of React states, often necessitating a Form library like Formik.
- using Uncontrolled Components with a bunch of React refs, trading off a lot of declarativity for not much fewer lines of code.
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!
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-specificdefaultValue
(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.
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.