React Form Handling with useReactiveForm

Michael Kutateladze
5 min readFeb 18, 2020

When I was working with Angular, form handling was not a problem. But we have switched to React recently and suddenly it happened to be a painful process.

There are several options. And they all have the same disadvantage — an enormous amount of re-renders. Also, it is not convenient at all to handle every <input/> with an onChange function.

So I came up with an idea to create such a form handler, which would be declarative, would not depend on the complexity of the form and that would not re-render component as often. Let me introduce useReactiveForm.

The idea behind my approach is to store form data in a useRef hook. Unlike setting state variables, updating reference does not re-render the component.

TL;TR

  1. React hook for gathering and validating form data in a declarative way without unnecessary re-renders.
  2. Can be used with any type of <input/> , <textarea/> and <select/>. As long as custom form controls such as DatePickers or TimePickers have name attribute this hook will work.
  3. You are not bound to use a specific UI components library.
  4. It works with dynamic fields. Use update() method for that.
update({
...values,
books: [
...values.books,
{
title: '',
author: '',
year: '',
readers: []
}
]
})

Here is an example of the hook.

Let’s start coding!

Step 1: Describe initial values

const fields = {
user: '',
books: [
{
title: '',
author: '',
}
]
}

The most common case in handling forms is to gather everything to an object and send it to the backend. So let’s start with this object. Here we describe what fields should be there.

Step 2: [Optional] Describe validation schema in Yup syntax

import { array, object, string } from 'yup';

// ...

const schema = object().shape({
user: string().max(20, 'Character limit exceeded')
.required('This field is required'),
books: array().of(object().shape({
title: string().required('This field is required'),
author: string().required('This field is required'),
})),
});

Yup is an awesome library that provides you with convenient validation schema. In initial values, we have an object, so our schema should start with object(). This object should consist of some fields, in other words, it has a shape(). The shape of this object is two fields: user and books. The user is a string, so there is a method string(). And books is an array of objects, which fields are strings. I guess, here you already got the point, that this library is also declarative and it is so convenient to work with.

Step 3: Create the config

const config = {
fields: fields,
schema: schema
};

This is the config, which we will later provide to our useReactiveForm hook. Basically only required field is fields. Since I have shown an optional step of adding validation schema, I also add it to config. Here is the list of every field (with types. T stands for the type of your fields) in config object:

fields: T - Form fields / structure  
deps?: any[] - Array of dependencies that trigger re-render.
schema
?: any - Validation schema
separator?: string - Separator for name property of inputs. _ is set by default
validateOnChange?: boolean - Validate on input change
actionOnChange?: (values: T) => void - Fire function on input change
updateTriggers? string[] - array of name attributes whose change triggers re-render

Step 4: Use the hook

const { values, ref, validate, errors} = useReactiveForm(config);

Here we just pass to the hook our config and get something back from the hook. Here are all methods that you can extract from the hook.

values - get current form state
ref - reference to the <form> tag
validate() - function which validates the form
errors - get errors after validation
clear() - function which clears form values form and errors
update() - function which re-renders form. It is needed in case when you dynamically add fields.

Step 5: Connect the hook to the form

The time has come! The moment is at hand! You are… prepared to connect the hook to the form.

const onSubmit = (e) => {
e.preventDefault();
if (validate()) {
console.log(values);
} else {
console.log(errors);
}
};

return (
<form ref={ref} onSubmit={onSubmit}>

<div>
<input type='text' name='user' defaultValue{values.name}/>
{ errors.user.error && <p> {errors.user.error} </p>}
</div>

{
values.books.map((b, i) => (
<div key={`book${i}`}>
<input type='text' name={`books_${i}_title`}/>
<input type='text' name={`books_${i}_author`}/>
</div>
))
}

<button type='submit' onClick={onSubmit}> Submit </button>

</form>
)

The ref from the useReactiveForm is going straight to the ref attribute on the <form ref={ref}> tag.

The default values you can find in values by the proper key. The array which we want to map is also contained in values .

Notice, that you have to describe name attribute as a path to the key in your form object, for example: books_${i}_title. But instead of common separators (., []) use _ or your separator described in config.

It is needed for one of the methods inside the hook to look up for the key recursively and change it.

To get an error message use errors. It contains an object with the same structure as your values object. But instead of just values it contains an object { value: string, error: string }. Therefore, the error message for user field is located in errors.user.error.

Any action triggered on the <input/> will provide it with one of the following classes: touched, dirty or invalid . You can use it to visually show the result of validation.

In onSubmit function we call validate() method from the hook. It returns true or false.

Now when we submit our form we have the same structure as we put into the hook, but filled with the values we typed into input fields and other form controls. It means that we do not have to gather the data from the pieces of state of the component into one big object — it’s already done!

Notice that when you type the values into the inputs the component does not re-render.

With this approach, you can easily scale the form to infinity and beyond. No matter how deep you have nested the key, the recursive function will find and update it.

Also, the convenient part is that you are not required to use any custom form controls provided by some library. Everything you need is a form control tag with name attribute. You can create your own or indeed use some library. Just make sure to have name attribute.

Full code

import { array, object, string } from 'yup';
import { useReactiveForm } from 'use-reactive-form';
const Example = () => {const fields = {
user: '',
books: [
{
title: '',
author: '',
}
]
}


const schema = object().shape({
user: string().max(20, 'Character limit exceeded')
.required('This field is required'),
books: array().of(object().shape({
title: string().required('This field is required'),
author: string().required('This field is required'),
})),
});
const config = {
fields: fields,
schema: schema
};
const { values, ref, validate, errors} = useReactiveForm(config);const onSubmit = (e) => {
e.preventDefault();
if (validate()) {
console.log(values);
} else {
console.log(errors);
}
};

return (
<form ref={ref} onSubmit={onSubmit}>

<div>
<input type='text' name='user' defaultValue={values.name}/>
{ errors.user.error && <p> {errors.user.error} </p>}
</div>

{
values.books.map((b, i) => (
<div key={`book${i}`}>
<input type='text' name={`books_${i}_title`}/>
<input type='text' name={`books_${i}_author`}/>
</div>
))
}

<button type='submit' onClick={onSubmit}> Submit </button>

</form>
)
}

I hope this library will help you handle forms in React easier.

Happy coding 🤓

--

--