How to handle forms with just React

Ok, the title of this article is a bait. I’m actually going to tell you about how forms can be handled with… Javascript. And react is just going to help us stay declarative. That’s what it’s for, after all.

If you ever had a feeling that working with forms in React was too much boilerplate or that maybe angular used to provide a superior experience or you just wish to know what might be the *best way* to organize forms in react, then read on.

First thing I wish to mention is how powerful and underestimated DOM API is. It has a somewhat bad reputation, though, because it’s completely imperative. That’s why libraries like react, vue, angular, etc exist in the first place: to abstract away the imperative nature of DOM api. I think this creates a wrong impression with the beginners who jump to the conclusion that you should stay away from it. You should indeed avoid imperative code, but you should also embrace all the power that the browser and the DOM give you.

Ok, enough with introduction, let’s jump in.

How to create forms with react and send data to the server?

Let’s start simple.

The most minimalistic approach

A demo:

And here’s the code:

Ok, where are the value attributes or the onChange callbacks? Well, you’re not required to use them. The onSubmit callback gets called when you submit the html form by either clicking on the submit button or just by pressing “enter” while focused in one of the input fields. When you add name attributes to your inputs, you add structure to your form. This structure can be serialized by the native FormData interface (basic support in all browsers and IE10+). All you do is pass in a form element (which we access via event.target) to the FormData constructor and you get a serialized interpretation of the inputs which can be sent to the server.

Also notice that we don’t add an onClick listener to the button. If we did, we would not be able to respond to submit events triggered from the keyboard (by pressing enter). That’s bad UX. By using the onSubmit callback we cover both cases.

By using this method, no matter how large your form grows, you don’t need to write any additional boilerplate code. Just follow a good practice of always adding a name attribute to your input tags (of course these names should correspond to what the server expects).

Also notice how we have kept our form component fully declarative even without using such react features as “controlled components”. No refs or raw DOM manipulations are needed. Here’s the link to the fiddle with the form.

My data requires to be transformed from user input, so I need state and complex controlled components!

Well, no, not necessarily.

One of the first things you learn when starting with react is that data should have a single source of truth and that it should flow one way, top to bottom. That’s true. But where is the “source” of the form data? It depends on the kind of application you have.

It’s very likely that the data for the form comes from the user input and from nowhere else, and it is not shared anywhere else.

One of valuable lessons you learn from react docs is that when you have to share state, you should lift the state up. But be careful: don’t lift the state when you don’t need to.

Yes, there are cases where controlled inputs are a valid choice. I plan to cover those cases in my next post.

But for now I would like to explore how far you can go with the simple approach described above without reaching out for controlled inputs.

Input data transformations

Imagine the data that the server needs is in different form than the data that the user enters. Let’s say we have these requirements:

  • the user enters the date in MM/DD/YYYY format, but the server expects it in YYYY-MM-DD
  • the username should be sent in uppercase
handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.target);
    // NOTE: you access FormData fields with `data.get(fieldName)`    
const [month, day, year] = data.get('birthdate').split('/');
const serverDate = `${year}-${month}-${day}`;
    data.set('birthdate', serverDate);
data.set('username', data.get('username').toUpperCase());
    fetch('/api/form-submit-url', {
method: 'POST',
body: data,
});
}

That was very easy. The best thing about it is that you didn’t have to search for a framework- or plugin-specific way to do this. Remember $parsers and $formatters pipeline? It wasn’t a bad feature, it was a great feature. I enjoyed using it. But I think you’ll agree that it’s an overkill for this particular case.

There’s one downside, though. Did you notice how we have tied ourselves to particular pieces of input? Inside the handleSubmit handler we now have to know which inputs need to be transformed and which don’t. We’ve lost some declarativity. Let’s solve this problem.

from now on when the user-entered value needs to be transformed I’ll call that “parsing”.

Depending on your app you might need different parser functions for different kinds of inputs. But across your app you’re probably going to reuse many of them. Reusing code is something we do all the time. Why not create a set of utility functions that are responsible for parsing your form inputs? Here’s an example:

const inputParsers = {
date(input) {
const [month, day, year] = input.split('/');
return `${year}-${month}-${day}`;
},
  uppercase(input) {
return input.toUpperCase();
},
  number(input) {
return parseFloat(input);
},
};

Just a javascript object with methods. But how do we use it? Well…

Time to find out more about DOM api

Any <form /> element has an elements property. Read more about it here. It’s an html collection object. The best thing about it is it provides access to all input nodes of the form by key, where the key is the input’s name attribute. Yes, no matter how deep inside the form you have an <input name='birthdate' /> element, you can access it with form.elements.birthdate. Isn’t that great?

Ok, we can access all form’s input nodes by their name. But how do we know which ones to parse? How can we mark those inputs that do need some additional parsing?

The data-attributes of course! That’s what they’re here for. So to indicate that an input’s value needs to be transformed to uppercase before we send it to the server I suggest adding a data-parse attribute to it:

<input
name="username"
type="text
data-parse="uppercase"
/>

Very descriptive. And here’s the whole example showing how we can transform all necessary data. Pay attention to the “handleSubmit” handler:

(I removed the label tags for readability)

That’s pretty powerful, in my opinion. Once again, our forms can grow as large as they need without making our handler function bigger.

And the best thing is that not only we did not tie ourselves to any react-form-handling library, we hardly tied ourselves to react. If one day you decide to abandon react for any reason, you don’t have to noticeably change the way you deal with forms. The DOM is not going anywhere.

Input validation

If you’re observant, you may have noticed another problem with the above form. We don’t check inputs for validity before parsing them, which may lead to errors. If you think that DOM api can’t help us here or that it’s not supported well, I’m happy to say that you’re wrong. Html form validation is another powerful thing I like using very much.

The most simple example:

<form>
<input name="username" type="text" required />
</form>

That’s it. We only added a required attribute to an input. The browser will consider this input field invalid if it is empty and valid if it has at least one character. When at least on of the the input fields is invalid, the browser will not let the user submit the form, instead it will show a tooltip near the first invalid input.

But the thing is, we cannot rely on this behavior: the browser will not prevent the form from being submitted in Safari and mobile safari. But we don’t need to! The browser tooltips are not the best way to go anyway: they are not flexible enough and are not easily styleable.

What we do is this:

<form noValidate>
<input name="username" type="text" required />
</form>

We add a novalidate attribute (noValidate in jsx turns into novalidate in html). The name of the attribute is somewhat misleading. When we add it, we do not actually turn off form validation. We only prevent the browser from interfering when an invalid form is submitted so that we can “interfere” ourselves.

So, how does form validation work now? Like this: a <form /> element has a checkValidity() method which returns false when the form is considered invalid and true when it is valid. A form is considered invalid when at least one of its input elements is invalid. Here’s a small demo-gif:

And here’s how we can use this in our handleSubmit method:

handleSubmit(event) {
if (!event.target.checkValidity()) {
// form is invalid! so we do nothing
return;
}
// form is valid! We can parse and submit data
}

Great! Now we have a way to prevent parsing invalid inputs when the form is not valid.

Can we detect individual invalid inputs? Sure! How? Exactly the same: inputs also have a .checkValidity() method. See for yourself:

Adding a required attribute is not the only way to tell the browser that an input needs to be checked. What else can we do?

  • Use the pattern attribute. The most powerful validation attribute. Its value should be a regex which will be matched against the whole input value.
    Let’s say we want to allow only numbers in some input field. Here’s how we can do it: <input type="text" pattern="\d+" />. That’s it. We can now call .checkValidity() method of the input and check whether it is valid or not.
Note that when the input is empty it’s considered “valid” even though it doesn’t match the regex. That’s because we omitted the “required” attribute.
  • Give an input a type email. Just like the name suggests, the input will be checked for a valid email pattern, so we don’t have to come up with a quirky regex solution.

Ok, let’s use this knowledge. Here’s what our form code may look like now:

<form onSubmit={this.handleSubmit} noValidate>
<label htmlFor="username">Username:</label>
<input
id="username"
name="username"
type="text"
data-parse="uppercase"
/>
  <label htmlFor="email">Email:</label>
<input id="email" name="email" type="email" />
  <label htmlFor="birthdate">Birthdate:</label>
<input
id="birthdate"
name="birthdate"
type="text"
data-parse="date"
placeholder="MM/DD//YYYY"
pattern="\d{2}\/\d{2}/\d{4}"
required
/>
  <button>Send data!</button>
</form>

Here is a complete fiddle with validation and here’s a glimpse of how it can work:

Visualizing invalid inputs

I know what you might be thinking. If the input’s validity can be checked with a .checkValidity() method, then we can toggle validity classes with javascript!

Wrong! Well, that’s not really “wrong”, but I have a much better solution to offer. Let’s find out one more powerful thing about form API.

The :invalid css selector

Those inputs which are invalid can be targeted with pure css! Just like this:

input:invalid {
border-color: red;
}

Again, I know what you might be thinking. “Such css magic probably has terrible browser support”. I’m so happy to correct you: it has wonderful browser support!

There are some drawbacks, though. While this css pseudo-selector is powerful, it’s also pretty dumb. Let’s say we want to style invalid inputs only after the user has tried to submit the form. The form has no notion of being “dirty” or having a “tried-to-submit” state. So the invalid inputs will be marked with a red border even before the user has tried typing anything at all.

How can we deal with this? Very easily! We can just add a “displayErrors” class to the form itself after the user tries to submit it and style the inputs as invalid only when they are inside a form with a .displayErrors class:

handleSubmit(event) {
if (!event.target.checkValidity()) {
this.setState({ displayErrors: true });
return;
}
this.setState({ displayErrors: false });

}
render() {
const { displayErrors } = this.state;
return (
<form
onSubmit={this.handleSubmit}
noValidate
className={displayErrors ? 'displayErrors' : ''}
>
{/* ... */}
</form>
);
}

… and in our css:

.displayErrors input:invalid {
border-color: red;
}

Works like a charm:

Here’s the fiddle for you to play with.


I hope I have convinced you that native DOM API is a powerful thing that doesn’t get enough attention and that you should use it. It’s flexible enough to give you whatever behavior you desire.

What about “controlled” inputs, though? There are lots of cases when you need them. In this post I wanted to get you familiar with the forms API and explore its power. Not using controlled inputs serves this purpose very well.

You might think that when you have “controlled” inputs you do not need this DOM API. But I assure you that these things are complementary. In my next post I want to explore how using native forms api together with controlled input can make you even more powerful. But I also want to emphasize that you shouldn’t seek a more powerful tool when you can easily do without it.