Better Understanding Forms in React

The goal for this post is to get a better understanding of how to build forms in React. But before we see some actual implementations let’s try to better understand why this topic might seem complex in a React context.

When talking about forms, we mostly have three areas in mind. Form Field Elements (representation), Form Values (state management) and Field Validations (validation). Let’s see what we can take-away by treating representation, state management and validation as independent areas.


Form State Management

Developers new to React seem to be surprised at how much manual work has to be done as compared to say jQuery to retrieve form values. In jQuery serializeArray returns all form values as an array of objects for example. (Thanks Artem Sapegin for the good comparison, also see https://twitter.com/iamsapegin/status/1010406113555120131)

In React there are two ways to manage this state, via controlled and uncontrolled components. Let’s take a closer look at the former approach and consult the documentation.

In HTML, form elements such as <input>, <textarea>, and <select> typically maintain their own state and update it based on user input. In React, mutable state is typically kept in the state property of components, and only updated with setState().
We can combine the two by making the React state be the “single source of truth”.
https://reactjs.org/docs/forms.html#controlled-components

So any input element value that is controlled by React is described as a controlled component.

What does the actual implementation look like?

onChange = event => {
// taken straight from the official React Docs
// https://reactjs.org/docs/forms.html#handling-multiple-inputs
  const target = event.target;
const value = target.type === "checkbox"
? target.checked
: target.value;
const name = target.name;
  this.setState({
[name]: value
});
};

As seen in the above example, we can retrieve name and value from event.target and update the value for given name.

Now we also need to call onChange when the input value has changed, which can be achieved via the onChange prop on an form input element.

<input
name="active"
type="checkbox"
checked={form.active}
onChange={onChange}
/>

We should have a basic understanding of how we can manage form state in React. Check the following example that shows how React controls the complete form state.


Form Validation

Another important aspect when building forms is validation. We need a way to ensure that the data is valid and display meaningful error messages in case any input values are incorrect.

Typically we want to ensure that a required input is not empty, that a name has a certain string length, that an e-mail has the correct format or that two dependent fields have the same values.

Let’s take a look at the following validation function.

const validate = ({ firstName, lastName }) => {
return {
firstName: !firstName || firstName.trim().length === 0
? "First Name is required"
: false,
lastName:
!lastName || lastName.trim().length === 0
? "Last Name is required"
: false
};
};

We need a way to represent any errors for given data. The actual validation implementation can vary, but in this example we want to represent any errors via an object. Every form field value can either be false or contain an error message. Interestingly our validation function doesn’t know anything about the form. It’s a standalone function that accepts any data and returns an object.

In reality we could compose validation functions and run them against any data, we don’t have to be explicit about the specific names etc.

What we need to do now, is enable to run the form data against the validation and display error messages when needed. Due to the fact that React renders the component as soon as the state has changed, we can run the validation every time we render. That also means there is no need to keep any errors in state.

Every time we render, we validate the current form state against the validation.

render{) {
const {form, onChange } = this.state;
const errors = validate(form);
return (
<form>
<label>
active:
<input
name="active"
type="checkbox"
checked={form.active}
onChange={onChange}
/>
</label>
{errors.firstName &&
<span className="error">{errors.firstName}</span>
}
</form>
);
}

By separating the validation from the state management aspect, we can interchange how we want to validate our data, without being coupled to how state is managed. We can use a traditional validation library or write our own implementation.

Check the following example to see in more detail how this could be implemented.


Form Representation

Having covered the state management and the validation aspects, we also need to take care of the actual representation for our forms. React enables us to define components, compose these components etc. The only important fact to highlight here, is that our components should not know anything about how are form state is managed nor how this state is validated. As long as we separate the representation from these two aspects, we can build components that receive props and display information according to these props.

const CheckBox = ({ label, error, onChange, name, value }) => ( 
<label
{label}:
<input
name={name}
type="checkbox"
checked={value}
onChange={onChange}
/>
{error && <span style={errorStyle}>{error}</span>}
</label>
);

We can also build components that take care of handling accessibility, ensuring certain standards are considered etc. The important aspect here is that our representation is independent of the Form component and we can interchange these components without having to change the actual Form component.


More advanced Concepts

In the previous section we built a basic implementation of a form library. But in reality this implementation will only get us this far. Real world requirements might include the need to validate fields dynamically or on submit. Validate all fields or only fields that have been changed or instant validation only after an initial submit. Sometimes we might need to validate asynchronously, for example check if a given user name already exists etc.

Let’s try to see how we can solve these requirements while providing flexibility in user land.

How can we build a reusable Form component, without having to provide an extensive API for handling common scenarios?

Asynchronous Validations

What if we delegate any async validations to a parent component? By taking this approach we can define the async validation outside our Form component and only pass down the validation function and the validation result to our Form component.

Take a look at the following example. Our Form component passes any props through to our render functionality. This means we re-render every time an asynchronous validation result is available.

Dynamic Validation

Sometimes we need to validate fields dynamically, as soon as the value has changed, but leave any untouched inputs unvalidated. To be able to display error messages on changed fields, we need to keep track of which fields have actually changed.

This can be achieved by extending our form state to also know about which fields have been changed. For example we could extend the state object with a changedFields property. The same approach can be taken to check if a form has submitted or not. Again, we can the state object with a submitted property, which we can use when rendering the actual form.

Checkout the following example that displays this approach in more detail.


Finally, we can provide user land more flexibility if we enable users to define not only what to render and the initialState, but also how to update the data.

<Form
intitialState={{
values: {
active: false,
firstName: "",
lastName: ""
},
submitted: false,
changedFields: {}
}}
update={(state, name, value) => {
return {
...state,
values: { ...state.values, [name]: value },
changedFields: { ...state.changedFields, [name]: true }
};
}}
...
/>

Now our form doesn’t even have to know about our state or how to actually update it, it only takes care of managing the state.

This approach is best understood, when applied via an example.


Summary

Hopefully this write-up showed how even more complicated requirements can be tackled in React. As long as the validation, state management and representation aspects are independent from each other, we can extend and provide flexibility for the forms we are building. A good example for this approach is Formik for example.


If there are better ways to solve these problems or if you have any questions, please leave a comment here or on Twitter.