Lightweight Forms Validation in React
How to use native HTML forms validation in React and customize the validation messages.
Posted on by Jan Vlnas
When you encounter form validation in React, you don't need to immediately reach out for some forms library. Try the native forms validation with validation constraints API – you can customize the look of validation messages and their contents. Play with the final result and check out the example repository.
Form with native validation
You have a simple form on your website. Perhaps it's a login form or a newsletter sign-up – a few fields and a submit button. You don't have any complex interaction logic, so just grab form contents with FormData
in onSubmit
handler and send them to the backend.
Let's create a form with a single e-mail field. Including HTML attributes for client-side validation.
function onSubmit(e) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// Do something with the data
console.log(Object.fromEntries(formData.entries()));
}
function Form() {
return (
<form onSubmit={onSubmit}>
<p>
<label>
Your e-mail
<input type="email" name="email" required />
</label>
</p>
<p>
<button type="submit">Submit</button>
</p>
</form>
);
}
Now, when I try to submit an empty or invalid form, the browser gives me a nice pop-up message with default styling.
Rendering the validation message
Maybe you don't like how the browser's pop-up looks. Perhaps you want it to look the same in all browsers, or you want to put the validation error somewhere else. At this point, you may be considering to do the validation logic yourself or to reach out for some React forms library.
But the Constraint validation API provides a good abstraction to start with. Nowadays is also well-supported in browsers.
When a field fails the validation, it triggers an invalid
event. The error message can be read from the input field's validationMessage
property.
function Input() {
// Passed into input's onInvalid prop
const invalidHandler = (e) => {
// e.target is the input
const validationMessage = e.target.validationMessage;
// prints: 'Please fill out this field.'
console.log(validationMessage);
};
return (
<input onInvalid={invalidHandler} type="email" name="email" required />
);
}
Now that we have access to this message, we can show it to the user. For example, we can store it in a local state and render it. But we also need to prevent the browser from showing the pop-up message – this is done with e.preventDefault()
.
function Input(props) {
const [validationMessage, setValidationMessage] = useState();
const invalidHandler = (e) => {
const validationMessage = e.target.validationMessage;
setValidationMessage(validationMessage);
e.preventDefault();
};
return (
<>
<input onInvalid={invalidHandler} type="email" name="email" required />
<span className="validation-message">{validationMessage}</span>
</>
);
}
Resetting the state
There's one issue with this solution: the validation error is being shown even after the field is fixed.
This is because the invalid
handler is triggered only with the form submission. We can listen for additional field events, like blur
or change
, to update or hide the validation message.
function Input(props) {
const [validationMessage, setValidationMessage] = useState();
const invalidHandler = (e) => {
const validationMessage = e.target.validationMessage;
setValidationMessage(validationMessage);
e.preventDefault();
};
return (
<>
<input
onInvalid={invalidHandler}
onChange={invalidHandler}
type="email"
name="email"
required
/>
<span className="validation-message">{validationMessage}</span>
</>
);
}
This is a bare minimum to use native HTML forms validation with React. You can play with the result in a sandbox and here's a repository.
Advantages and disadvantages
If you have only a small amount of simple forms, native HTML validation can get you quickly to usable results.
The biggest advantages of this approach are fewer dependencies and progressive enhancement – the validation will work even if the client fails to load a JavaScript. Probably not a big concern with React, but totally viable if you are using server-side rendering (for example, with frameworks like Next.js or Remix). Only the backend must be able to accept a form submitted without JavaScript.
On the other hand, there are some disadvantages.
For starters, the default validation messages respect the browser's locale, not of the page. The result may be a bit confusing. For example, if you use a custom input pattern
with explanation, you can end up with mixed languages in validation message (although it seems like the title
is displayed only by Firefox).
Different browsers provide different validation messages, so if you need a tight control over copy, you must provide the messages yourself. You can read field's validity
property, which gives you the validity state. The article about the constraints validation API from CSS Tricks includes a convenient hasError
function to get you started. On the other hand, most libraries give you the same validation messages across all browsers.
Since constraints validation API is tied to HTML, it is harder to share validation logic with the backend. For example, the Formik forms library uses Yup library for data validation. You can use the same validation schema both on the client, and the server.
For a forms-heavy application I wouldn't hesitate to pick some popular library, like React Hook Form, React Final Form, or Formik. But for a simple website with a single “Contact Us” form? I'd try to keep things light.
Resources
- Forms Validation series on CSS Tricks was an extremely valuable resource, despite being a bit dated. Especially Part 2 goes deep into the JavaScript logic of forms validation.
- MDN provides an up-to-date tutorial on Client-side form validation using vanilla JavaScript.
- If you want to learn more about use of
FormData
in React, check out FormData with React Hooks and Fetch by Matt Boldt, and Creating forms in React in 2020 by Kristofer Selbekk.