Better Programming

Advice for programmers.

Follow publication

Before You Reach for Another JS Form Library…

Isaiah Thomason
Better Programming
Published in
8 min readNov 7, 2023

Forms are an integral part of many web applications, and they’re a part that we often want to get right. Consequently, many developers find themselves reaching for an all-inclusive JavaScript form library on NPM before they try to build anything themselves. Unfortunately, many of the form libraries out there have two drawbacks:

  1. They’re often framework-specific. This means that whenever you need to use a different framework (e.g., Svelte instead of React) or you need to use pure JS, you’re forced to learn a new library and its new API.
  2. They’re often stateful when they don’t need to be. This results in performance hits to your application, particularly if you’re using React, which will re-render entire forms during state changes.

Some time ago, I wrote an article stating that you typically don’t need React state in your forms. But what if I told you that you might not even need a form library for any JS framework that you use?

Believe it or not, modern browsers have come a long way, and they have almost everything that you need to manage complex forms. Consequently, state management tools are completely unnecessary in modern forms, and many form libraries (stateful or not) are often unnecessary as well. If you reach for the browser’s API instead of another complex form library, you’ll find that you can improve your application’s performance and bundle size with little effort. As long as the DX of the browser’s API is sufficient, this is a big win!

Curious about how much the browser can actually do? Here are 9 use cases commonly found in complex forms that can easily be handled with pure JS:

1) Accessing a Form’s Data

It is unnecessary to keep track of a form’s data with state management. This is because you can access a form’s data easily with the FormData class. Simply call the constructor with the HTMLFormElement of interest, and you'll have all of your form's data available at your fingertips. (This includes form data belonging to Web Components.)

2) Accessing a Form’s Fields

All of the fields associated with a form (including Web Components that act like fields) can be accessed with the HTMLFormElement.elements property. Not only does this property allow you to access form controls by name

But it also functions as an enumerable, array-like object. This makes it very easy to search for a subset of fields based on specific criteria.

Remember that a field outside of a form element can still be associated with the form as long as the field has a form attribute that points to the owning form's id. (This attribute can also be used with Web Components.)

This means that no matter how complex your application’s component tree is, you can always associate form controls with your form of choice easily. Then, you can access those form controls by using the HTMLFormElement.elements property.

3) Accessing a Form from Its Fields

Not only does a form element have access to all of its controls, but each control has access to its owning form (even if the control is a Web Component).

This is powerful because it means that if you have access to a single form control anywhere in your application, then you also have access to the entire owning form’s data and its fields. This is far more flexible (and transferrable) than what typical state management tools (and even typical form libraries) have to offer, and it’s much simpler as well.

This native browser functionality is useful for several purposes. For example, if you only have access to the confirm-password field in a certain part of your application, you can still compare it to the corresponding password field by using the form property.

4) Validating Form Fields

You technically don’t need a library to validate your form fields either. Browsers already have functionality in place to validate your fields (including your Web Components). In my experience, this takes care of most use cases — even for large companies. Here’s a simple example.

Of course, the browser can’t predict every possible way that developers would want to validate their form fields, so there are cases where custom validation will be required. For example, you’ll need custom validation to verify that a confirm-password field matches its corresponding password field, or to make sure that a username is available during user sign-up. For these cases, you will need to write your own JavaScript logic to run this custom validation, and you will need to use setCustomValidity to mark the field as invalid to the browser. (Note that you can also set the ValidityState of Web Components.)

One of the many benefits of using the browser’s native form validation is that you can see whether or not your form is valid by calling form.checkValidity(). (Form controls — including Web Components — also have their own version of this method.)

5) Providing Error Messages for Form Fields

Browsers automatically provide localized error messages for your form fields whenever they fail constraint validation. The error message that the browser provides depends on the specific constraint that failed validation, and it is exposed by the field.validationMessage property. If you want to replace the browser's error message with your own (or if you need to mark a field as invalid after running custom validation logic), you can use setCustomValidity() (or setValidity() for Web Components).

When a browser reports field errors with its error bubbles, it will display whatever is in the field.validationMessage property. Of course, you're free to render the validationMessage to the DOM instead of relying on the browser's error bubbles.

Because we already have the validationMessage property, there isn't really a need for libraries or state management tools to keep track of the error messages associated with your fields.

6) Reporting Error Messages to Users

You can report the error messages associated with your form’s fields at any moment by calling form.reportValidity() or field.reportValidity (again, also available to Web Components). When a form is invalid, form.reportValidity() will call attention to the first invalid field belonging to the form by focusing the field and displaying its validationMessage in a bubble. Similarly, when a field is invalid, field.reportValidity() will call attention to the field by focusing it and displaying its validationMessage in a bubble. (Note that reportValidity does nothing if the related form/field is valid.)

The browser will automatically block form submissions for forms with invalid fields. If a user attempts to submit an invalid form, the browser will call form.reportValidity() instead. If you prefer to handle this logic yourself, you can apply the novalidate attribute to your form and run your own logic in a submission handler. (The novalidate attribute does not disable form validation. It simply prevents the browser from blocking form submissions on its own.)

7) Identifying Fields That Will Not Participate in Validation

There are situations where a field should not cause a form to fail validation. For example, a field is not relevant for form submission or form validation if it is disabled. The field.willValidate property can be used to identify fields (or Web Components) that will not participate in form validation.

8) Identifying Visited Fields

If we’re being honest, a “visited” or “touched” field is simply a field that has been blurred. Form validation libraries often use state management to keep track of visited fields, but this isn't necessary when we already have custom data attributes to store this information for us.

data-* attributes also make it easy to reset any fields that are marked as visited. For example, we might want to do this whenever a form is reset.

9) Identifying Dirty Fields

“Dirty Fields” are simply fields which no longer match their default values. Form validation libraries often use state management to keep track of dirty fields. However, browsers already give you a way to do this since they expose the default values of fields through properties like defaultValue, defaultChecked (e.g., for radio buttons and checkboxes), and defaultSelected (e.g., for <select> elements). Consequently, all that's needed to check whether or not a field is dirty is something like

When combined with data-* attributes (e.g., data-dirty), this approach gives us another easy way to keep track of an entire form's dirty fields without state management. A well-designed event handler can efficiently get the job done for us.

Note: Since Web Components define their own custom properties, each individual Web Component can choose how it wants to expose a defaultValue property (or something similar).

Where the Browser Falls Short: Reporting Accessible Error Messages

Perhaps the only drawback of relying solely on the browser (without any additional JavaScript) is that the error messages which the browser displays in bubbles do not typically provide a good user experience, nor are they sufficiently accessible. Moreover, the browser can only display one error message bubble at a time. Thankfully, this doesn’t mean that you need to opt out of the browser’s features altogether. Instead, you can create helper functions that automatically display accessible error messages for you.

Situations like these — where the browser lacks convenient solutions for important problems — are where it makes sense to reach for a form library (or build your own tool). The library that you choose shouldn’t increase your bundle size with features already provided by the browser. And ideally, it should be framework agnostic and stateless so that you can reuse the same API in various kinds of JS applications (instead being locked into, say, React).

The FormValidityObserver, which is provided by the small @form-observer/core NPM Package, meets all of these criteria. It functions as a performant, stateless, framework-agnostic tool that empowers you to report accessible error messages to your users with ease. If you decide to use a package instead of writing your own form validation logic, @form-observer/core would be one of my top library recommendations.

For convenience, the Form Observer package also has framework integrations (e.g., @form-observer/react) which make the core tool function more comfortably in your framework of choice. All of these integrations have the same API, so you’ll never have to learn a new set of tools if you change frameworks.

Hopefully you gained deeper insight into all that you can do with your forms using pure JavaScript. And I’ve only covered the basics! If you check out MDN’s documentation, you’ll learn more tricks that are possible with modern browsers. Web Components have their own impressive features as well.

To conclude, here’s my recommendation: The next time you work on a form in a web app, try using the native features first. Resist the urge to reach for a library if your problem is already solved by the browser. If your problem isn’t solved by the browser, consider reaching for a specialized tool that solves your specific problem without adding bloat or taking away your app’s flexibility — like the FormValidityObserver.

There are multiple disadvantages that come with using state management tools or bloated libraries for simple form-related tasks. The Form Observer’s Philosophy, which extends this article, documents those drawbacks. So it’s always best to try to stick as close to the browser as possible. It will take you much farther than you would expect.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response