Form validation/submission in vanilla React (no libraries)

cuginoale

Alessio Carnevale

Posted on August 23, 2022

Form validation/submission in vanilla React (no libraries)

Form validation/submission in vanilla React (no libraries)

You don’t need a fancy library when you have HTML5 and Constraint API.

Link to part-two: a not so trivial example


The official React documentation suggests 3 possible ways to handle form submission/validation:

  • Controlled components
  • Uncontrolled components
  • Fully-fledged solutions (3rd party libs)

But none of these 3 methods are particularly appealing to me.

Controlled components: I personally don’t like controlled components as it involves manual state management that, most of the times, leads to unneeded and inefficient re-renderings.

Uncontrolled components: React docs suggests implementing uncontrolled components using a ref to get form values from the DOM but then doesn’t provide much info on what the best practises are to extract the data and validate it.

Either way, we are left with the non trivial task of implementing the logic to validate and collect the form data.

These days there are plenty of 3rd party libraries that do exactly that... but do we really need one?


The goal

Let’s build a simple form like this one:

Interactive demo available here.

We want to build a form implementing the following requirements:

  • Name and Email are mandatory
  • Email should represent a valid email address
  • Address is optional, no constraints
  • Tel is optional but if entered it should represent a formally correct UK phone number

On submit the form gets validated and should show validation errors like this:

In case of validation errors the focus should be moved to the first invalid field.


HTML5 and Constraint API

(Source MDN): HTML5 already has the ability to validate most user data without relying on JavaScript. This is done by using validation attributes on form elements.

  • required: Specifies whether a form field needs to be filled in before the form can be submitted.
  • minlength and maxlength: Specifies the minimum and maximum length of textual data (strings).
  • min and max: Specifies the minimum and maximum values of numerical input types.
  • type: Specifies whether the data needs to be a number, an email address, or some other specific preset type.
  • pattern: Specifies a regular expression that defines a pattern the entered data needs to follow.

If the data entered in a form field follows all of the rules specified by the above attributes, it is considered valid. If not, it is considered invalid.

Most browsers support the Constraint Validation API, which consists of a set of methods and properties that enable checking values that users have entered into form controls, before submitting the values to the server.

With these tools at our disposal we can build custom components for easy and efficient form management.


The solution

The building blocks of our solution are the following two custom components:

  • <Form />
  • <TextInput />

<Form />

The key attribute here is noValidate.

The novalidate attribute turns off the browser's automatic validation error messages. Without that our form would look like this:

default validation feedback

All the magic happens in the handleSubmit callback:

  • we stop the form submission with e.preventDefault()
  • we make a note of the form validity state with isValid
  • we add the “submitted” className to improve the UX (more on this in a moment)
  • we move the focus to the first invalid field, if any
  • we collect the form data and call the onSubmit callback, if valid

The last one is another key point of this solution: collecting the form data using the FormData object. There is no need to manually go through the form elements and extract their values.

“The FormData interface provides a way to easily construct a set of key/value pairs representing form fields and their values” —( source: MDN)

<TextInput />

The TextInput component is responsible for rendering the input field, the label and the possible validation error.

You can see the code here, but essentially this component sets an internal state variable with the validation error string coming from the constraint API and resets it on blur if the error has been fixed:

Implementation

The code to implement the form in the example looks like this:

Things to notice:

  • We set the required attribute on Name and Email
  • type=”email” makes sure Constraint API checks the input is a valid email address
  • Address as no validation rules
  • Tel is not required but we set the pattern attribute to check the input against the regex (setting type=tel doesn’t enforce any built in validation)

Whenever we need to add another field to the form we just add a new TextInput and we set the relevant attributes to it.

No need to update schemas or add yet another useState to track the value of the new field, lets just offload the heavy lifting to the built-in API!

Improving the UX

The final touch to improve the UX is to show validation errors only after the form gets submitted.

Using just the :invalid pseudo class in our CSS to style incorrect fields would cause red highlighted input boxes and error messages to appear as soon as the page loads… and we don’t want to scream in the face of a user that his/her input is incorrect before they even have the chance to type a character in!

For this reason we add the .submitted className to the form and style the TextInput with this:

i18n

One great thing to keep in mind is that the constraint API validation messages are localised by default — i.e. they come in the locale the user OS is set to!

If you are happy with the default messages then there is nothing else you need to worry about: your English, Spanish, Italian or Chinese users will get the messages in their own language.

The TextInput component provided in the example allows also for basic validation message customisation via the errorText attribute:

This is just an example to show how one could customise those messages.

Finally

Once the form is formally correct we can actually submit the from in the onSubmit callback.

We receive a FormData object which can then be easily sent using the fetch() or XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data" , or we can transform it in more familiar key/value object:

to produce the following:

Final words

There is beauty in simplicity and form validation/submission doesn’t get much simpler than this IMHO 🙂

Any comment / feedback is much appreciated!

Link to part-two: a not so trivial example

💖 💪 🙅 🚩
cuginoale
Alessio Carnevale

Posted on August 23, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related