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, andhandle*
functions into aFormContainer
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 ofonChange
andonBlur
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.