Libraries are good but often don’t fit into your problem. This article presents a library-free framework to write complex nested validated forms in react.

Adding nested validated forms to a react application should be as simple and straightforward as it was with plain HTML pages. A lot of libraries have been written for that purpose. However, depending on your very single and unique problem at hand, you may end up spending days understanding how to tweak the library of your choice to adapt it to your problem.

In my experience, I wanted to create:

dynamic forms: the user can add fields (like in a to-do list) and the content of some inputs can change the content and/or the display of others

validated forms: the form should have a front-end declarative validation triggered on demand

nested forms: some forms are fields of a main form or of sub-forms

custom input components: either to use basic HTML 5 inputs or for instance material UI ones.

My react application used the redux store. This is very common but often a source of confusion. As noted by the react team:

From the very beginning, we need to stress that redux has no relation to react

Especially one of the most used libraries for nested forms in react is redux-forms.

I felt very confused by this profusion of tools and went back to the react documentation to end up rebuilding very complex react nested forms with very basic react.

This post intends to summarize my approach to address the above-mentioned points. In the end, I will also present their limitations.

React guidelines

There is an official react documentation on forms. It gives the basis of a simple form with some inputs directly in the component. The main things to catch from it are:

the data of the form should be stored somewhere in the state of the component (or in the redux store). There is one object for the whole form (single source of truth), i.e. that there is not one variable per input but inputs are keys of the same data object.

you can have either controlled or uncontrolled react components:

controlled means that the data displayed to the user is the data that you pass to the component. In this setting, you have to handle each input so that you update the data and the user still has the feeling that he is actually filling the input.

uncontrolled means that the data displayed is the data typed by the user. You can still display a default value at mount but there is no way of enforcing the display of a value when the user starts typing

If you choose to use controlled component, you don’t really need to place your inputs inside a <form> tag because you already get the data, at any time. With uncontrolled component, you can either handle data changes or use a <form> tag to handle data on submit.

As far as your form only contains few inputs all in the same component, this works well and is rather easy, a little bit wordy though. Confusion starts when you want to add some logic and nested forms.

Nested forms

Nested forms are forms in which the data are not only primitive types but also arrays or objects. In essence, I think that nested forms do not change a thing to what has been done so far. But you have to be cautious to carefully apply the framework to avoid unexpected changes in form data.

Main form

In this context, we consider that each component stands for a data unit. It can be either a simple input or a whole sub-form. It means that the component’s structure should match the form data structure. For instance, say that you the data look like that:

const data = { key1: 'key1', key2: {...}, key3: [{...}, {...}], }

This data could be either in the state of the main react component or in the redux store. I will use the first option in the following of this tutorial so as not to be dependent on an unnecessary library. The only reason why you should place it in the store is if you want to access your data somewhere else in your react application.

At the top level, key1 , key2 and key3 are data units of your form data. So you should have in the main render three components, one for each of these keys. For instance:

render() { return ( <input type='text' name='key1' value={this.state.data.key1} /> <SubFormComponentForKey2DataType value={this.state.data.key2} /> <ArrayOfSubFormComponentsForKey3DataType value={this.state.data.key3} /> ); }

In this latter case one might be tempted to use directly a map :

render() { return ( <input type='text' name='key1' value={this.state.data.key1} /> <SubFormComponentForKey2DataType value={this.state.key2} /> {this.state.data.key3.map((subform) => <SubFormComponentForKey3DataType key={subform.key} value={subform} /> } ) }

This practice is discouraged by the fact that one would have to handle changes of each SubFormComponentForKey3DataType component according to their index. By doing so we break the similarity with the other fields ( key1 and key2 for instance). In case of heavy forms, it is necessary to really think small, divide and conquer. In other words, don’t ask too much to your component, keep it simple and similar, split your logic.

In the main component, one defines the method to handle changes like that:

handleFieldChange = (field) => (event, value, selectedKey) => { // make a copy of the object first to avoid changes by reference let data = { ...this.state.data }; // use here event or value of selectedKey depending on your component's event data[field] = value; this .setState({ data }); }

“Keep it simple” means that as for a very simple form, the method only has to be able to handle key1 , key2 or key3 on their whole. You don’t want to make it aware of a change in key2.subkey1 or in a change of a sub-field of a data of key3 in any way. Then this method should be passed to your components in the render:

render() { return ( <input type='text' name='key1' value={this.state.data.key1} onChange={this.handleFieldChange('key1')} /> <SubFormComponentForKey2DataType value={this.state.data.key2} onChange={this.handleFieldChange('key2')} /> <ArrayOfSubFormComponentsForKey3DataType value={this.state.data.key3} onChange={this.handleFieldChange('key3')} /> ) }

Subform as an object

Now let’s have a look at what should be inside <SubFormComponentForKey2DataType /> . Remember that there is only one state for the whole form. This means that the sub-forms receive their part in props and call their parent’s method on change. Thus the handleFieldChange is:

handleFieldChamge = (field) => (event, value, selectedKey) => { let data = { ...this.props.value }; data[field] = value; // you could pass the event here but also null if it is not necessary nor useful this .props.onChange(null, data); }

Here, the only difference between the previous method and this one is that one calls this.props.onChange instead of setting the state directly. Once again, my advice is to keep it as simple as possible.

Array of nested forms

Let us finally consider the case of a map as in <ArrayOfSubFormComponentsForKey3DataType /> . This component should be able to handle a change only at the first level, i.e. on an array of objects. Precisely, it should have a method:

handleFieldChange = (index) => (event, value, selectedKey) => { let data = [...this.props.value]; data[index] = value; this .props.onChange(null, data); }

This is very (very) similar to the previous method. Indeed the only difference is that it destructures an array instead of an object.

Finally, the render should just contain:

render() { return ( { this .props.value.map((subform, index) => <SubFormComponentforKey3DataType key={subform.key} value={subform} onChange={ this .handleFieldChange(index)} /> } ) }

By doing so, changes are propagated from component to component and you never have to think about what is going on outside of your component nor where you are in your form data.

This setting is as simple as dumb: you only treat in a component the changes in its first level values. The component can handle a change and then calls a parent’s method meaning “my job is done, do what you want with it”.

Data validation

Forms validator

HTML5 provides easy data validators directly in the basic <input> components. For instance, specifying type="number" or required will directly prevent the user from typing anything else than numbers or from submitting an empty field. Read the documentation for a comprehensive list of what is possible with HTML5 built-in form validation. However, I was using the material-ui library so that these validators were not available.

Furthermore, in some cases, you want not only an input validation but also a data validation, for example field1 + field2 < threshold .

Because of these reasons, I decided to rely on the validate.js package. It provides a comprehensive list of validators and an easy declarative definition of the desired constraints on an object. It is possible to define several constraints for a single field with different error messages. Because we don’t want to think about data validation outside of a form we define the formValidator together with its constraints in the same .js file:

import { Component } from 'react' import validate from 'validate.js'; const constraints = { key1: { presence: { // prebuilt validate.js validators. allowEmpty: false, message: 'some custom or intl error message', }, numericality: true, }, }; export const subformValidator = (data) => validate(data, constraints); export const Subform extends Component { ... }

If, on the other hand, the form has nested forms such as in the first example, the form1Validator should make a call to the subformValidator :

import { subformValidator } from 'components/forms/subform' export const form1Validator = (data) => { let validation = validate(data, constraints); if (data.subform) { validation = { ...validation, subform: subformValidator(subform), }; } return validation; }

All together it means that the constraints defined on the *form file only concerns the true inputs of the component while the validation of the more complex objects is delegated to their own validator.

Display errors

validate.js function returns an object with the same keys as the argument. It contains either undefined if there were no error or an object with the error messages if appropriate.

It is then possible to pass the validation object to each one of the input components so that they can display the error message if appropriate. If there is no error, undefined is received and nothing is displayed.

Validation on submit

One may also want to perform form validation on submit only. It means that before sending the full object the (nested) validation is done using the previously defined validators.

The previous consideration leads to two points of attention:

the form validation computed at the main component should be propagated to the children components.

the validation object is more complex than just being defined or undefined since the keys might be defined but containing only undefined values.

The first point means that one has to pass in props each validation together with the original value:

render() { return ( <input type='text' name='key1' value={this.state.data.key1} validation={this.state.validation.key1} onChange={this.handleFieldChange('key1')} /> <SubFormComponentForKey2DataType value={this.state.data.key2} validation={this.state.validation.key2} onChange={this.handleFieldChange('key2')} /> <ArrayOfSubFormComponentsForKey3DataType value={this.state.data.key3} validation={this.state.validation.key3} onChange={this.handleFieldChange('key3')} /> ) }

The second point has been tackled with a util function. It performs a deep search for undefined going iteratively through the object to make sure that if keys are defined, they only lead to undefined values.

export const deepUndefinedSearch = (obj) => ( obj instanceof Object && obj.constructor === Object ? Object.keys(obj) .map( (key) => obj[key] instanceof Array ? obj[key].map((o) => deepUndefinedSearch(o)).filter((o) => !o).length === 0 : obj[key] === undefined ) .filter((o) => !o) .length === 0 : obj === undefined );

Then the output of the main form validator can be processed by that function to return either true if forms are all validated or false otherwise:

const formIsValidated = (validation) => deepUndefinedSearch(validation)

Limits of the current implementation

Validation

As long as the objects are self validating the current implementation, while somehow wordy, works. However, we faced some cases where the objects to be filled were defined on purpose depending on some business logic implemented somewhere else in the component. This logic was not accessible from the object itself.

In this context, it means that each instance of the same component can lead to different validators. The more probable is that the render embeds the business logic that one does not want to copy. In a sense, one has to validate the data that the user can fill. Then, each component should validate its data itself and send back the result to the parent component.

The ref attribute can be used to determine what is displayed in the render. So a first possibility would be to filter the constraints object with the displayed inputs. Then the results should be lifted-up through the nested forms until the main react component.

However if one does not want to perform validation on input changes but only on submit, one would call far too much the validators. At least the errors should not be displayed until the user clicks submit .

A possibility would be to pass a boolean to each child component: displayErrorMessage . This variable could be initialized at false and set to true on submit. Still is validation computed on change.

Factorisation

All the components in the nested forms always receive the same props with the same names apart from a difference in the key . This is a direct call for factorization based on a single key and using either react Higher Order Component or children props to simplify the code.

Conclusion

As for now, I have not felt any needs for moving from that basic implementation to a public library.

I hope this can help some other save time and gain experience in react! Please share your thoughts and tips here, on twitter or contact us. Also, don’t hesitate to have a look at our other blog posts about Node.Js and react.

I also give a special thanks to Nicklas Ansman Giertz for the wonderful validate.js library.

Thanks to Tristan Roussel and Pierre-Henri Cumenge.