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.
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
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
false, the user should be barred from submitting it.
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.
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.
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
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.
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
FormContainercomponent. 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
onBlurcallbacks in your component, you use action creators and reducers to create the same state changes in your redux store.
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.