aurelien brabant's logo

DOM form validation with React

If you have used HTML5 forms before, you should probably know that the browser, through the Document Object Model (DOM) API, gives you really simple and limited options to validate the data that has been typed in by the user.

<form action>
	<input name="email" type="email" />
	<input name="firstname" type="text" required />
	<button type="submit">Submit form</button>
</form>

Given this basic example, the browser will ask for a syntactically valid email address (which apparently seem to be any string that actually contains a @ character) and explicitly ask for a non-empty firstname text field.

DOM form validation example

This is very basic validation, and while it does not handle a large variety of scenarios, I think it may actually be sufficient for very basic use cases, as long as the feedback the user gets is explicit enough.

DOM form validation with ReactJS

Is it even possible ?!

Nowadays people tend to use modern libraries and frameworks to build their web applications for various reasons. A few days ago, I built the contact form of my main website and realized that I did not need any complex form validation for that one. As it's the only form on my website, it would have been overkill to install a third-party library like Formik to handle the form validation, so I decided I would go with the basic DOM validation API, at least for now.

Well, the thing is that given how forms are usually handled in React applications, triggering this basic validation was not that straightforward. After all, React supersedes the DOM from a developer perspective, and interacting with it directly has never been quite easy (in fact, we are not really meant to).

So, to clearly formalize the problem, let's take an example: below is a standard functional component that renders a form which has all its inputs controlled by the component's state:

import React, { useState } from "react";

type FormData = {
	email: string;
	firstname: string;
};

const ControlledFormExample: React.FC<{}> = () => {
	// store form's data in a single piece of state
	const [formData, setFormData] = useState<FormData>({
		email: "",
		firstname: "",
	});

	const handleFormSubmit = (ev: React.FormEvent) => {
		// prevent form default behaviour - including input validation!
		ev.preventDefault();

		console.log("submitted", formData);
	};

	const handleInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
		setFormData({ ...formData, [ev.target.name]: ev.target.value });
	};

	return (
		<form onSubmit={handleFormSubmit}>
			<input
				type="email"
				name="email"
				value={formData.email}
				onChange={handleInputChange}
			/>
			<input
				type="text"
				name="firstname"
				required
				value={formData.firstname}
				onChange={handleInputChange}
			/>
		</form>
	);
};

When building such a form with React, we usually want to respond to the onSubmit event and execute our submission logic at that time.

However, because we are building a single-page application, we don't want to fire the traditional form submission event provided by the DOM, which by default would make a request to the URL specified in the action attribute of the form and thus 'refresh' the page. To keep things handled by our application, we call ev.preventDefault() that, well, prevent the default form submission to happen. That said, the bad thing is that the DOM form validation is basically included into that default behavior we just prevented. That's why we don't have validation anymore...

The first thing you may think in that type of situation is that 'there must be a way'. And, while there is, it's absolutely not documented anywhere. Let me show you how to achieve that.

reportValidity to the rescue!

To solve our problem, we can make use of the reportValidity method which is provided by the HTMLFormElement DOM element.

const handleFormSubmit = (ev: React.FormEvent) => {
	ev.preventDefault();

	if (ev.target.reportValidity()) {
		// on validation success
	} else {
		// on validation error
	}
};

According to MDN here is what it actually does:

The HTMLFormElement.reportValidity() method returns true if the element's child controls satisfy their validation constraints. When false is returned, cancelable invalid events are fired for each invalid child and validation problems are reported to the user.

As showed above, we are just making a logical decision based upon the return value of reportValidity. Easy!

Type errors!

If you use Typescript to build your project, the above code will surely generate type errors. To keep things simple, it's because the target property of the React.FormEvent object does not implement a definition for the reportValidity method. I'm not exactly sure why this is the case while this is clearly documented online, but it is what it is. To fix that, you can go for the dirty way:

if ((ev.target as any).reportValidity());

While this work, this is violating type safety which is why we use Typescript for in the first place.

Alternatively, what we can do is that we can define our own type that basically adds the missing reportValidity method to the target object of the FormEvent. This is easily achieved through the use of Typescript's intersection operator:

type ValidatableFormEvent = React.FormEvent & { target: { reportValidity: () => boolean } };

const handleFormSubmit = (ev: ValidatableFormEvent) => {
	ev.target.reportValidity(); // OK
}

We are basically ending up with a React.FormEvent type that actually implements the missing method. No more type troubles!

reportValidity vs checkValidity

If you have followed the MDN link I have put above, you may have discovered that a really similar method called checkValidity also exists. In fact, these two methods basically do the same thing, except that checkValidity is not supposed to report any validation error to the user visually. That means no default error boxes. That also mean your probably want to use reportValidity most of the time, because if you want custom error validation, you would be much better served by standard React form manipulation libraries.

I found this codepen online that illustrates the difference.