Stop duplicating form states

Forms are a staple of Web development ever since the World Wide Web took off. We realized not only we could display documents, but we could also fill them up—it’s as if we view and fill up documents in an office or any commercial establishment. With the rise of dynamic HTML where we could implement real-time behaviors in the browser, we started using technologies such as AJAX in the hopes of improving an already-functional way of submitting forms into something that we believe has better user experience. And it did give us a way of sending and receiving more compact data that does not require us to fetch the same document over and over.

Forms IRL. Credit to Scott Graham on Unsplash.

Fast forward to today where user interactivity is king: we’ve turned simple Web sites into artisanal works, more colorful and lively as ever. Just like we’ve had trends in any traditional media, we also had trends in software which we can anticipate to iterate over several months. Skeuomorphic design went away, but was resurrected in the recent years as something hybrid over its flat design successors. We went away with gradients, and yet we are back adding gradient fills in hard-hitting headings.

iOS design language comparison, which also applies to Web sites at that time. Credit to Clearbridge Mobile.

Even software engineering paradigms were part of this chasing-the-trend phenomenon. From DOM-heavy libraries such as jQuery, we had AngularJS with two-way data-binding. Then we realized it’s too expensive and we opted to make data flow in one direction. This was the heyday of React (back when it was not yet licensed as MIT), and seeing it can adopt this promising architecture, it went ahead and eventually it succeeded in being the library most developers would be accustomed with. We had Redux, an implementation of the Flux design it was based on. Then we had more of these state management libraries popping into existence, and among these came certain libraries that help you manage form behavior based on the exact interactivity designers aim for—form state libraries.

The default experience with forms

Going back to the early days of the Web, inputting values to forms and submitting them requires sending a request to the server, which in turn responds with the document including the feedback of the server, such as when there are errors in some fields, or when the request has been successfully processed.

Old-style forms. Credit to Landon Curt Noll.

Typically, Web pages are heavy to load in a user agent. Not only does the document itself have to be loaded, but resources that are linked from the document such as styles, scripts, images, and any other relevant assets must be included in this single request. Since they typically reside in different URLs, browsers have been made to load these resources in parallel. This is a common problem in places where there is less bandwidth, even more so with clients having slower network speed. And like what was mentioned before, technologies like AJAX were developed (originally it was made just to load XML data, hence the X in AJAX), and with this, Web sites can now send more compact data, and receive a similarly compact response which could be used to update the view in the browser.

Improved user interactivity, more requirements

Eventually, Web sites have to validate these requests in order to provide a more comprehensive experience. Sending requests and receiving errors which would have been easy to rectify would have been an inferior experience to some. And that’s why some Web sites opted to have client-side validation to make sure whatever goes in the server are valid—this is to minimize back-and-forth requests between the server and the client. It’s why the modern Web API now contains baked-in utilities for handling input validity, and if the developer chooses to, could use it for custom business-logic validations.

Input with invalid value using native validation.

Yet, besides the Web sites being built, there have been software being made in order to simplify this process of managing user input, validating them, and providing behavior to still deliver the best user experience possible.

Reusable code for everyone

Web sites get made more and more each day. And there are no signs of it stopping—it only goes faster and faster. In order to adapt to this pace of software development, it is reasonable to use whatever tools are already made previously by developers solving the same problems. This is why we have frameworks to use for focusing on just building user-facing software, and alongside these frameworks are libraries we can mix and match and plug into our projects.

Code related to forms can get quite unwieldy when accounting for the requirements previously mentioned: interactivity, validation, good user experience, and error handling. In addition to being used with frameworks, there is another aspect to deciding how to better architecture forms: compatibility. Frameworks have a certain mindset required to be adopted in order to accomplish its purpose, which includes the manner data is introduced in the system, how interactivity is implemented, how it handles exceptional behavior…all of which is hugely intertwined with good form requirements. This is why people devise their own way of forms handling, from barebones ones to comprehensive sets of functions.

import { useState } from 'react';

function EmailForm(props) {
  const { defaultEmail } = props;
  const [email, setEmail] = useState<string>();
  
  function handleEmailChange(e) {
    setEmail(e.currentTarget.value);
  }
  
  function checkEmailAvailability(e) {
    e.preventDefault();
    // left for brevity -- use the state we have for the current value
    // for email
  }
  
  return (
    <div>
      <input
        type="email"
        onChange={handleEmailChange}
      />
      <button
        type="submit"
        onClick={checkEmailAvailability}
      >
        Check Email Availability
      </button>
    </div>
  )
}
EmailForm.tsx - example component using basic form state management. Imagine if there are more inputs in this form...

One would expect for a given framework there is at least one library whose purpose has something to do with forms. And with these libraries entail varying levels of complexity needed to be consumed properly, however the focus is now shifted, instead of the values being handled by the form, to the library being used to handle the form.

Single source of truth...or is it?

It is a common theme across form state libraries to have to duplicate the native form state. This is for a good reason—with the state available within the form state library’s domain, it can perform tasks that it offers to consumers to be factors worth deciding for, such as validating values on the fly, providing response depending on how “pristine” values are in the form, etc. However, this is a big caveat as whoever decides that values are stale is now left to the form state library instead of the native form state. There could be instances where the form state library and the native form system might have unsynchronized state! In addition, over-reliance to client-side code could be prone to modified or reduced behavior such as when users choose to have plugins on their user agents. Maybe it is by design so that users avoid tampering with their data? Who knows!

Regardless, there are already a few ways that forms already expose their state to the developer without too much dependence to these form state libraries. The only requirement would be having access to the form element from the DOM.

Getting form state natively

All of the methods below are available with every modern browser and browser-like environments.

Via individual inputs (HTMLFormElement.elements)

Upon querying the form element, there is an attribute called elements which keep track of input controls linked to the particular form. When querying a single input, one could use elements.item(index) when querying for a control in a certain order, or elements.namedItem(name) when querying for the named control. The property accessor syntax elements[indexOrName] could also be used in the same ways.

// Get our form
const form = window.document.querySelector('form')

// Get the input
const input = form.elements.namedItem('username')

// Same as above
const sameInput = form.elements['username']

// Get the first input
const firstInput = form.elements.item(0)

// Same as above
const sameFirstInput = form.elements[0]
HTMLFormElement.elements usage.

Via FormData

One could also use something like new FormData(formElement) to create a FormData object using the values bound to the form element. Accessing items are the same as with elements but FormData elements are utterable such as when you need to iterate all of the values in the form.

// Get our form
const form = window.document.querySelector('form')

// Create a FormData from this form
const formData = new FormData(form)

// Get all values from the form
const valuesEntries = Array.from(formData.entries())

// Make it an object if you want
const values = Object.fromEntries(values)

// Maybe use with fetch (or axios?)
const url = new URL('/api/v1/post', 'https://www.example.com');

fetch(
  url.toString(),
  {
    method: 'POST',
    body: JSON.stringify(values),
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
  },
)
  .then(
    (response) => {
      if (!response.ok) {
        console.error('Error response sent by server.');
        // TODO - Implement your error handling here.
      }
      // TODO - Implement your success.
    },
    (error) => {
      console.error('Error during fetch.');
      // TODO - Implement your error handling here.
    },
  );
FormData usage

Via formxtra

I have created a library called formxtra that extracts values from all bound inputs in a form. I made it to be as flexible as possible, because FormData has limitations that it could not properly serialize values. And a simple object would be much easier to operate on.

Install the formxtra package from npm:

npm install --save @theoryofnekomata/formxtra

Then use the library like so:

import { getFormValues } from '@theoryofnekomata/formxtra';

// Get our form
const form = window.document.querySelector('form');

// Get the form values
//
// Options may be passed as second parameter,
// refer to package for details.
const values = getFormValues(form);

// Maybe use with fetch (or axios?)
const url = new URL('/api/v1/post', 'https://www.example.com');

fetch(
  url.toString(),
  {
    method: 'POST',
    body: JSON.stringify(values),
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
  },
)
  .then(
    (response) => {
      if (!response.ok) {
        console.error('Error response sent by server.');
        // TODO - Implement your error handling here.
      }
      // TODO - Implement your success.
    },
    (error) => {
      console.error('Error during fetch.');
      // TODO - Implement your error handling here.
    },
  );
formxtra usage.

The formxtra library has parity with forms being submitted without client-side code, that is, how values are serialized with formxtra should be the same with values serialized by forms submitted without JavaScript enabled (with the exception of files, which have special handling by design due to security considerations).

Since formxtra does not require dependencies, it can already work with frameworks: Here is a pen for vanilla, a pen for React, a pen for Vue, and a pen for Solid.

Should you stop using form state libraries now?

Let me be clear—I am not against form state libraries. If they accomplish their purpose and help you deliver well, then they should continue to exist. But it’s not like one always has to require form state libraries in their projects, provided they are not complex enough to warrant such usage. However, this is a proposal to embrace the native APIs first instead of resorting to client-side state mechanisms that mimic the former. Because what problems we could have been experiencing right now might already be solved by the ones before us. After all, isn’t that the aim of reusable code?