Simplifying React form state management

Managing form state in modern JavaScript applications is usually verbose and error prone, but it doesn’t have to be.

Connor Stauffer
Engineering @ BuildZoom
5 min readOct 4, 2018

--

We’re going to demonstrate how a simple HTML form written in React can require a daunting amount of code and, worse yet, how that code can easily end up duplicated across a code base.

Then, once we’re sufficiently frustrated, we’ll do it the same way over and over again for every form imaginable until we all rage quit or go into management.

Not really. Once we’re sufficiently frustrated, we’ll identify the parts of our component that can be abstracted away. Fortunately, we’ll see that the most verbose and complex parts are the easiest ones to extract. What we’ll end up with is a useful abstraction for managing UI-agnostic form state, and a simple form component responsible for little more than its markup.

Let’s get started!

Let’s say we’re building a form to handle user sign ups. Here’s a component to handle the basic input state.

Our form has text inputs for email and password. We’re using component state to keep track of the values. We have onChange handlers to update state whenever the input changes.

Let’s add some submission logic.

When the form is submitted via a click to the submit button or an enter key press from one of the fields, we’re calling thecreateUser method passed as a prop. (The details of that method aren’t relevant to this post. Assume it’s some async server call.)

This is about as far as the React docs will take us when it comes to building forms. But we’re still far from production-level behavior. We can’t just be firing off form submissions silently! User’s expect feedback when they interact with the page.

So let’s add submission and confirmation state.

In addition to keeping track of the email and password fields, we’re persisting whether the form is in a submission or confirmation state. Immediately before this.props.createUser is called, we set submitting to true, which results in our button text changing from “Submit” to “…”. Once the createUser logic is complete, we set submitting to false and confirming to true, which changes our button text to “Success!”.

Big step up in UX there. But the next addition is where our task gets painful. Currently, we’re not imposing any restrictions on what form data can be submitted.

Time for validations.

What happened? It’s chaos! Let’s step through the major changes.

First off, our state object has changed slightly. We’ve moved the email and password values into a fields key and are tracking a list of errors and a dirty Boolean in addition to value. dirty signifies whether the field has been validated since it’s last change in value.

validatePassword adds an error to our password field state if the current input is less than six character long. validateEmail does the same for our email field state if the input isn’t a valid email or if this.props.getExistingUser resolves to a user object.

Each of these methods is called when its respective input field fires a blur event. Both are called when the form is submitted, before the createUser request is made. We avoid duplicate validations by skipping them if the field hasn’t changed since the last validation.

All the behavior in this component is reasonable. For a one-off form, I wouldn’t change much. But what about the next form we add to our code base?

Let’s imagine we need to build another form.

This new form has it’s own submission logic, fields, validations, and UI. Where do we start? We could start in the same place we did for our sign up form.

We’d create a new component, give it state to keep track of the field values, errors, submission state and confirmation. We’d add methods to validate each field and update the UI state on submission. We’d end up with something very similar to our registration form.

Each of our forms would be fine React components, but together, in the same code base, the logic duplicated between them would beg to be refactored.

Almost all forms behave the same way.

They’re made up of a set of fields, each having a default value, initialization logic, errors, and dirtiness. A form component cares about the state of each individual field and the state of the form as a whole. It shouldn’t validate unless a field has changed since the last validation. It shouldn’t submit unless all fields are known to be valid. It should handle any combination of sync and async field validations.

Notice that none of these requirements have anything to do with the type of fields in or the appearance of the form.

They are shared requirements of generic form state. Form state, not UI, will be the source of our abstraction. Let’s jump ahead to what we’ll be left with after our refactoring is complete.

Here’s our registration form with it’s state managed by a higher order component called withForm.

Nothing but markup and configuration. Our component is wrapped in a HOC that takes a submit function, an optional confirmationDuration , and a map of fields each with a getErrors method. We’re defining only the things that vary significantly between different forms.

All the verbose state management still has to go somewhere. We haven’t gotten rid of it, but we’ve done the next best thing. We’ve isolated it in our withForm HOC.

It looks quite a bit like our original component. The state shape is identical. We’ve just generalized our validation methods to handle an arbitrary number of fields.

On each render, we build up a single form state object and pass it to the child component. The child component has all the information and control it had when managing its own state, but less clutter and potential for error.

We’ve come a long way.

We’ve gone from a verbose, naive implementation that doesn’t scale, to a reusable solution that will save us time, effort, and errors down the road.

This isn’t a new approach to building forms in React. I highly recommend looking into the open source library Formik. Its withFormik HOC is a more complete version of what we built in this post.

If, however, you’re trying to limit dependencies and think a minimal approach like withForm could work for your project, you can find a working example to play with in this sandbox.

Also, please remember to review and update any code included in this post or the sandbox to ensure it’s ready for your production needs.

Happy form building!

--

--