You Don’t Need All That React State in Your Forms | by Isaiah Thomason

Did you know that it’s possible to handle forms using just pure HTML and JavaScript?

Photo by Katka Pavlickova on Unsplash

It’s very common for frontend applications to include some kind of form. Unfortunately, when it comes to React applications specifically, I often see people building out forms with an unnecessarily high number of controlled inputs. This typically adds more complexity to the form without affording any real benefits. I used to do the same thing. And I can’t blame others or even my old self for this poor habit. After all, the official React Docs (at the time of this writing) says:

In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.

But… I don’t buy what they’re selling me. I’m here to show you a much simpler way to write out your forms.

First, I want to remind all of us of what forms look like when we fill them with state.

I’ll use some of the basic kinds of inputs in my example. Feel free to interact with the code sandboxes as you read through this article.

React Form with Controlled Inputs (Using useState)

Note that I added a little bit of styling to the form element to make it slightly easier on the eyes in the browser.

Honestly… Having to write all that out really hurt. You’ll notice that there’s a lot of awkward code that looks redundant with this approach. But this isn’t anything new. Our React friends mention this problem in the docs:

It can sometimes be tedious to use controlled components, because you need to write an event handler for every way your data can change and pipe all of the input state through a React component.

And this is one of the noticeable downsides to controlled forms in React. Although the world of hooks is great, surely you’ve experienced this struggle… the tons of calls to useState… the tons of handleChange functions that look almost exactly the same… the pollution of tons of input props (some of which may cause your formatter to multiline the input elements)… it’s depressing.

Those of you who loved the old days of having one event handler and a whole object of state in a Class Component may be aware of the useReducer pattern:

React Form with Controlled Inputs (Using useReducer Pattern)

(I learned this useReducer pattern from resources made by Kent C. Dodds. Though unusual, it’s certainly useful in some situations.)

One hook call, one event handler, and one place to store our form state (as opposed to having it scattered across variables). Everything’s good, right? Eh…Mostly.

It still feels like we have some redundancy here. Don’t get me wrong, the decrease in lines showing useState and handle*Change is definitely great! But now we have the same form data properties (eg, firstName) being shown over and over and over again. And we’re still redundantly polluting all of the input props!

The approach is nice, but it’s roughly the same lines of code as the previous one. Actually, with the current formatter, it’s a few lines longer. This approach also adds some overhead to new devs by requiring them to learn the slightly-more-complex useReducer hook. (And the reducer pattern we used for the form is… a bit unorthodox for reducers.)

So at the end, we’re still left with problems of undesired redundancy and complexity. Surely there must be a better way…

Did you know that it’s possible to handle forms using just pure HTML and JS? No, I don’t mean the “pure JS” that React refers to. I mean literal, pure JS that doesn’t depend on any npm package. Things turn out just fine if you use the regular form API:

React Form Using Pure HTML and JS (ie, Uncontrolled Inputs)

Now this approach is glorious! No unnecessary state variables… No unnecessary complexity… Less code redundancy… And surely you can see that this implementation is far less verbose than the previous ones. This implementation uses almost half the lines of code compared to the previous implementations (ignoring styles). HALF (almost).

(Note: I’m aware of the defaultValue React warning for select elements; but I am ignoring it since our input is uncontrolled, and the point of this article is to highlight what’s possible with pure HTML+JS, not React-specific syntax.)

If you’re not familiar with form.elements, you can see MDN’s Docs. But basically, the form.elements property allows you to grab all of a form’s input-related elements via their ids or names. We’re using the namedItem method for this, but you can also destructure everything from form.elements directly:

React Form Using Pure HTML and JS (with handleSubmit Refactored)

Alternatively, you can just grab all the inputs as if they’re coming from an array. Please see the documentation on form.elements for more info.

You’ll notice that things get a little more verbose for TypeScript users, as you’ll have to specify the properties on form.elements, as well as the input elements to which said properties are mapped. For the JS-only users… you don’t have to worry about defining interfaces. You can just destruct the elements as normal.

For the TypeScript users, note that there is a simpler way to handle the typings for form controls; so don’t freak out about what you see above. More than likely, your form data is already an explicit type defined somewhere in your codebase… at least it should be. If that’s the case, then you can make a utility type that maps the keys of your form data to the proper input element. From personal experience, creating this utility type is rather simple. However, I don’t intend to go over the approach in this article. If there’s enough interest, perhaps I’ll add it here later — or in a separate article.

I already gave a few reasons earlier, but another big reason why it’s better to use uncontrolled inputs by default is that they significantly reduce the re-renders produced on your page. I know there are some of you who will say this is nitpicky, but this is truly worth considering. If you’re working with a form/page that uses controlled inputs, then the entire form/page will rerender every single time the user updates a value. Every time a letter is typed (or deleted), every time an option is chosen from a select element, every time a checkbox is clicked… Boom! Whole re-rerender. This is… inefficient.

If you want to visualize this a bit more clearly, visit the React Hook Form docs and scroll down to the “Isolate Re-renders” demo section. Now imagine that situation when you have forms and components that are even more involved.

Now yes, we can talk about “workarounds”. Maybe you’ll try to isolate certain amounts of state or certain parts of the form to special subcomponents. Maybe you can try special optimization techniques?

Or perhaps you’ve stomached the classic statement: “The cost isn’t that bad anyway.” But my question is this: Why even bother with all the complexities and abstractions when a simpler, pure JS solution exists?

A solution that’s transferrable between all frontend frameworks? I could understand arguing for using controlled inputs by default if it made life easier, reduced complexity, or reduced lines of code… but in normal situations this isn’t the case at all.

Typically, the default should be to use uncontrolled inputs, not controlled inputs. (Though yes, there is a time and place for everything, including controlled inputs.)

Although we’ve covered how to handle form submissions and how to work with inputs without the state, some of you are probably wondering about more complex situations. Someone may say, “Formatting is a common use case, and you can’t format inputs without the state.” Actually, I’ve written another article that proves you can format inputs without state… and perhaps more cleanly too.

Are you wondering about form validation? There are native API’s for that which don’t require a frontend framework. And they may make styling easier than you’d expect.

Are your form validation use cases more complex? Or perhaps you need access to more precise information, like whether or not a field has already been visited? Then React Hook Form is an excellent solution worth checking out. It basically seeks to provide these common form features while minimizing the number of state variables, re-renders, and unnecessary abstractions in your code. It plays very nicely with basic HTML/CSS/JS too!

(If you’re familiar with Formikthen I’d recommend trying out React Hook Form if you haven’t done so yet. This is mainly because Formik It takes a state-first approach, thus increasing your re-renders.

Formik also makes it more likely that you’ll drift towards unnecessary abstractions prematurely — like abstracted components — when simple HTML could suffice just fine. That’s not to say that Formik is a bad library. But it’ll likely be harder for you to reap the benefits mentioned in this article if you heavily depend on Formik by default.)

React is a great frontend framework. This can be seen clearly by how widely it’s used and appreciated. And without their innovation, other loveable frameworks like Vue may not have turned out as nicely as they have today.

But if we’re being honest, React has its downsides. And one downside that seems to be common (at least from my experience) is an over-reliance on state in the React community. We saw it with redux. And we still see it today with forms. This over-reliance on the state often makes us look past solutions that are staring us right in the face. (For instance, using the regular form features with pure HTML and JS.) And it results in code that may be less efficient or less easy to maintain.

We’ve been pre-wired to rely on the state. But there’s more that’s possible with plain old HTML/CSS/JS than we’d think.

Thankfully, the problem of over-reliance on the state doesn’t make React a bad framework because React itself doesn’t force you to over-rely on the state. We can Undo those bad instincts by learning the basics, and by choosing only to leverage state when we truly need to. This will enhance our capabilities as React developers and help us appreciate the framework much more.

Nothing has been better for my frontend skills than learning how regular HTML, CSS, and JS work first. Getting those basic fundamentals down has largely improved my code across the apps that I make. I encourage you to strengthen your foundations as well (even if you’re confident in those skills because you know React).

Here’s a pro tip: When you’re googling for solutions (as we all do), try to figure out if something is possible with native HTML/CSS/JS features before seeing how to do it in your framework of choice (whether it’s React or Vue or Svelte or whatever else). For instance, start by searching “JS how to submit a form” before you search “React how to submit a form”. It’ll make life much easier.

Yes, I acknowledge that you can technically do everything with raw HTML/CSS/JS, but some of that is painful. The big brain play is to figure out what’s easier with native features and what’s easier with your frontend framework, and then choose whatever is best.

Biased “tip” that you can ignore: Try out Svelte. The framework that really got me to start strengthening my basics wasn’t React or Vue… It was Svelte. And that’s because Svelte really tries to enhance the existing web features instead of creating completely different concepts (and Svelte has some really awesome features).

I’d highly encourage you to try learning basic HTML/CSS/JS while sticking to the framework that you’re already familiar with. But if that doesn’t work because state variables tempt you, then consider experimenting with Svelte to try recalibrating yourself. Couldn’t hurt.

Leave a Comment