Validate like a Native with React
Validating a form doesn’t always require a lot of code. With HTML5 constraint validation, you can describe form validation rules with HTML attributes on the form fields.
For example, to enforce that an <input>
element should not be empty, add the required
attribute to the element:
<input id="name" name="name" required/>
You can use HTML5 constraint validation even with React. Let’s look at an example.
Pseudo-classes to the rescue
We’ll focus on an <input>
field the user needs to fill. After the user presses the Submit
button, we want to highlight the empty field and show a message containing the validation error below it.
To limit the amount of JavaScript code to a minimum, we’ll steal a few techniques from Bootstrap to highlight errors and display messages with CSS.
The browser can generate error messages automatically but you can’t change their look, so we’ll disable them with the novalidate
attribute on the <form>
element:
<form noValidate>
</form>
The browser won’t show any error messages, but it will validate the data and add :valid
and :invalid
pseudo-classes to the inputs.
We’ll use these pseudo-classes to highlight the fields that contain errors and display our own error messages.
Dive in the components
If we styled the :invalid
pseudo-class directly, the input field would be highlighted before the user has finished typing. To avoid premature error highlighting, the Bootstrap CSS adds a red border to invalid input fields with the .form-control
class only when a parent element has the .was-validated
class.
.was-validated .form-control:invalid {
border-color: #dc3545;
}
We can control when the error highlighting appears by toggling the .was-validated
class on the form component.
Let’s create a Form
component with a validated
prop. When validated is true
, add the .was-validated
class to the <form>
element.
function Form({ validated, innerRef, children, ...otherProps }) {
const formClasses = classNames({
'was-validated': validated,
'needs-validation': !validated
});
return (
<form noValidate className={formClasses} ref={innerRef} {...otherProps}>
{children}
</form>
);
}
To display the <input>
field, use a React presentational component that creates an <input>
element with a label and the .form-control
class.
export default function Input({ label, ...inputProps }) {
return (
<React.Fragment>
<label htmlFor={inputProps.id}>{label}</label>
<input className="form-control" {...inputProps} />
</React.Fragment>
);
}
Presentational components like this one avoid repetition without obscuring the application logic.
Let’s move on to the error messages. We don’t want to show error messages until the user has tried to submit.
Elements with the .invalid-feedback
class are set to display: none
by default:
.invalid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 80%;
color: #dc3545;
}
The .invalid-feedback
elements gets displayed when a sibling has the :invalid
pseudo class and a parent element has the .was-validated
class:
.was-validated .form-control:invalid ~ .invalid-feedback {
display: block;
}
If you add the .invalid-feedback
class to a <div>
, the <div>
will show only when next to an input field which contains errors:
export default function Feedback({ children }) {
return <div className="invalid-feedback">{children}</div>;
}
Our styles and markup are ready, now let’s see how to activate the error highlighting and the error mesages.
Red Alert
To highlight all errors after the user attempts to submit, we’ll listen to the submit
event and toggle the .was-validated
class on the <form>
element.
We want to keep validation logic outside the form presentational components, in a component called App
. App
will have a single state variable, validated
, initialized to false
:
class App extends React.Component {
state = {
validated: false
};
}
In App
’s render()
function, pass state.validated
as the validated
prop to Form
:
render() {
return (
<Form
validated={this.state.validated}
>
<Input required />
</Form>
);
}
Errors will be highlighted when this.state.validated
becomes true
. We want to wait until the user submits the form to toggle this.state.validated
, so let’s dive into the submission logic.
Ready to submit!
When the user presses the submit button, we’ll switch this.state.validated
to true
. In the App
component, create an event handler for the submit event.
class App extends React.Component {
submit = event => {
if (/* form is invalid */) {
event.preventDefault();
this.setState(() => { validated: true });
}
};
We check if the form is valid; if it’s not, we highlight the errors, else we proceed with normal submission.
To check whether the form is valid, you could store a reference to the form DOM node and call checkValidity()
.
event.preventDefault()
prevents the form from submitting immediately.
Then bind the event listener to the submit
event on the form.
render() {
return (
<Form
onSubmit={this.submit}
validated={this.state.validated}
>
<Input required />
</Form>
);
}
If the user corrects the errors, the error messages disappear as the browser removes the :invalid
pseudo-class from the input fields.
Do we need custom validation after all?
We’ve manages to display validation errors by toggling just one CSS class, but we could improve the user experience.
It would be better to highlight the error as soon as the user leaves the field, but to achieve that we would need to track state on a per field basis, so either make each input field stateful, wrap it in a stateful component or store whether the user has visited each field in the main app state.
This kind of code is quite repetitive and verbose, so I prefer to use a dedicated solution like final-form
.
Another issue comes up if you want to use the same validation logic on the server. The HTML5 constraints API buries the validation rules in the markup.
Further reading
For learning React fundamentals, try React for Real ;-).
The components ended up looking a lot like Reactstrap, which is nicd if you want some React components with the Bootstrap CSS classes already applied.