Modeling form state in React


In this blog post we’ll describe a method for modeling forms in React. What follows is a general method for shaping form state data. It can be used with any state management tool such as Redux, MobX, another flux implementation, or even just this.setState. We’ll be using this.setState in this post.

First, let’s start with a basic form. For the sake of clarity, we’re ignoring styles.

Input state

In the form above, we’re not capturing the state of user input. Let’s add a state object.

First, we added an initial state object to the constuctor. I prefer to keep all of the inputs’ value state nested under an object called input. We then created a handleInputChange method to make setting the state under these nested objects easier. Finally, we turned the two inputs into controlled inputs by setting their value and onChange attributes.

Validation

Ok, so the next requirement is to add validation. Both an email and a name are required. Email also needs to pass our validation function isEmailValid, which returns a boolean.

So, now we’ve added a validate function which goes through the input values and determines any error messages that need to be displayed. This function returns an object with an errors property that will contain an object with a key for each invalid input. It also returns an isValid property of the validation state of the overall form. If isValid is false, the user should be barred from submitting it.

In the render method, we call validate and display each error inline with the relevant input. We also disable the submit button if the form is not valid. It’s important to note here that we are not storing the errors in this.state. We instead calculate them each time our component is rendered. The input data is the single source of truth and the errors are derived from it. If you also kept errors in this.state, you could introduce bugs where they get out of sync. One caveat is that this will only work if you can compute all validation errors synchronously.

Blur State

We still have one more problem: the errors are always shown even when the form is first loaded. This is annoying to the user! You don’t want to show them an error message before they even had a chance to fill out the form. So we will need to add some additional state.

Now we have an initial state that tracks whether an input has been blurred. We use the onBlur attribute of the input to set this to true when the user has navigated away from the input field. We are still calculating every error on every render, but now we only display them to the user once the input has been blurred. This creates a nicer user experience.

An alternative method is put errors in state instead of tracking input blur. In those cases, errors are only added to state when they need to be shown to the user. This gets complicated quickly as you need to calculate on each change if the error should be shown or not. It’s also more difficult to figure out if the form can be submitted because there may be no visible errors, but the form is still invalid. You avoid these complexities by computing errors on each render and storing blur state instead.

Additional features

There are few other features we typically add to forms. If you have a long form with many inputs, you may want a way for the user to know which fields are still invalid without having to blur each one. One option is to add logic when the form is submitted. If the form is not valid, set every key to true in the blurred object in state. This will cause all of the errors to be rendered.

You probably also want to add an isSaving property to state. Set this to true before the submit logic is executed and back to false when it is done. We typically disable each input and show a loading spinner while this is true.

You can also use the blur state to add a successful validation indicator. If the input’s blurred state is true and there is no error, you can style the input differently (add green outline, for example) to let the user know the input is completed correctly.

Organization

Doing all of this in a single React component will quickly get unwieldy, especially in larger forms. There are a few strategies to combat this.

  • Split into presentational and container components.
    Move all of the state, validation, and handle* functions into a FormContainer component. Pass all of these as props to a stateless component that just handles the rendering.
  • Use Redux
    You can move the state out of your React components and into redux. Instead of onChange and onBlur callbacks in your component, you use action creators and reducers to create the same state changes in your redux store.

Conclusion

As front-end engineers, we’re frequently tasked with creating forms. The pattern described here has proven successful in both simple and complex forms at 1stdibs and we hope it provides a simple framework for the next form you need to build.