Performance of large forms on React

Maxilect
6 min readAug 21, 2020

--

During one of our projects, we encountered forms of several dozen blocks that depend on each other. As usual, we cannot talk about the task in detail because of the NDA. We will try to describe our experience of optimization using an abstract (even slightly unrealistic) example. I’ll tell you what we have experienced with React Final Form.

Imagine that the form allows you to obtain a foreign passport and simultaneously process a needed visa through your preferred country’s visa center. This example seems bureaucratic enough to demonstrate our complexities.

On our project, we faced a form of many blocks with particular properties:

  • Among the fields, there are inputs, multiple choices, fields with autocomplete.
  • The blocks are linked together. Suppose you need to specify the data on the internal driving license in one block, and just below, there will be a block with the data of the visa applicant. In this case, the agreement with the visa center is also issued using your driving license.
  • In each block, you need to implement your own validation — adequacy of the passport number, the correctness of the email input, the person’s age, and much more.
  • The data entered in some blocks may affect the visibility and automatic content of other blocks. If a passport is issued for a 10-year-old student, you need to display a block with parents’ data. Dependencies are not trivial: one field can depend on five or more other fields.
  • Filling out the form is divided into two steps. In the first step, we show only a small part of the fields. But we must save and use the entered information in the second step.

The final form occupies about 6 thousand pixels vertically — this is about 3–4 screens, in total, more than 80 different fields. The closest thing in terms of question number is probably a questionnaire from some large corporation’s security service or a boring opinion poll about the preferences of video content.

However, large forms are not so common. If we try to implement such a form by analogy with how we are used to working with small forms — then the result will be impossible to use.

The main problem is that when you enter each letter in the appropriate fields, the entire form will be redrawn, which causes performance problems, especially on mobile devices. Also, it is difficult to cope with the form for users and for the developers who have to maintain it. If you do not take appropriate steps, the relationship of fields in the code is difficult to track. Changes in one place entail consequences that are sometimes difficult to predict.

How we deployed Final-form

The project used React and TypeScript (as we completed our tasks, we completely switched to TypeScript). Therefore, to implement the forms, we took the React Final Form library from the creators of Redux form.

First, we split the form into separate blocks and used the approaches described in the Final Form documentation. Alas, the input in one of the fields caused a change in the entire form. Since the library is relatively new, the documentation there is not perfect. It does not describe how to improve the performance of large forms. As I understand it, very few people were facing this. For small forms, a few extra redraws of the component have no effect on performance.

Now I’ll tell how we’ve dealt with the most common issues.

Dependencies

The first confusion we encountered was how to implement dependencies between fields. If you work strictly according to the documentation, the “overgrown” form starts to slow down. It happens due to a large number of interconnected fields. The documentation suggests putting a subscription to an external field next to the field. This is how it was on our project — adapted versions of react-final-form-listeners, which were responsible for connecting the fields, lay in the same place as the components. That is a very chaotic approach. Dependencies were difficult to track down. The components have become gigantic. Everything worked slowly. To change something in the form, you had to spend a lot of time using the search in all project files (there are about 600 files in the project, of which more than 100 are components).

We have made several attempts to improve the situation.

We had to implement our own selector, which selects only the data needed by a particular block.

<Form onSubmit = {this.handleSubmit} initialValues ​​= {initialValues}>
{({values, error, … other}) => (
<>
<Block1 data = {selectDataForBlock1 (values)} />
<Block2 data = { selectDataForBlock2 (values)} />

<BlockN data = {selectDataForBlockN (values)} />
</>
)}
</Form>

I had to reinvent the memoize pick([field1, field2, … fieldn ]). In conjunction with PureComponent (React.memo, reselect), all this makes blocks redraw only when the connected data changes. We introduced the Reselect library into the project, which was not previously used. With its help, we perform almost all data requests.

As a result, we switched to one listener, which describes all the dependencies for the form. We took the very idea of ​​this approach from the final-form-calculate project.

<Form
onSubmit = {this.handleSubmit}
initialValues ​​= {initialValues}
decorators = {[withContextListenerDecorator]}
>

export const listenerDecorator = (context: IContext) =>
createDecorator (
… block1FieldListeners (context),
… block2FieldListeners (context) ,

);

export const block1FieldListeners = (context: any): IListener [] => [
{
field: ‘block1Field’,
updates: (value: string, name: string) => {
// When the block1Field field changes, this function is triggered and we are dependent fields …
return {
block2Field1: block2Field1NewValue,
block2Field2: block2Field2NewValue,
};
},
},
];

As a result, we got the required relationship between the fields. Plus, the data is stored in one place and is used more transparently. Moreover, we know in what order the subscriptions are triggered since this is also important.

Validation

By analogy with dependencies, we have dealt with validation.

In almost every field, we needed to check whether the person entered the correct age (for example, whether the set of documents corresponds to the specified age). From dozens of different validators scattered across all forms, we switched to one global one, breaking it down into separate blocks:

  • validator for passport data,
  • validator for travel,
  • validator of previous visas,
  • etc.

It almost did not affect performance, but it accelerated further development. Now, when making changes, you do not need to go through the entire file to understand what is happening in individual validators.

Reuse of the code

We started with one large form, on which we tested our ideas, but over time the project grew — another form appeared. Naturally, in the second form, we used all the same ideas and even reused the code. We have already moved all the logic into separate modules, so why not connect them to the new form? This way, we have significantly reduced the amount of code and development speed.

Similarly, the new form now has types, constants, and components shared with the old form — for example, they have authorization.

Conclusion

Why didn’t we use another library for forms, since this one had difficulties? Large forms will create problems anyway. In the past, I have worked with Formik myself. Taking into account the fact that we did find solutions there, the Final Form turned out to be more convenient.

Overall, this is an excellent tool for creating forms. Together with some rules for the development of the code, it helped us to significantly optimize development. The added bonus of all this work is the ability to bring new team members up to date faster.

After highlighting the logic, it became much clearer what a particular field depends on — it is not necessary to read three sheets of requirements to understand something. Under these conditions, debugging takes at just two hours, although previously it could take a couple of days before all improvements.

Author of the article: Oleg Troshagin, Maxilect.

PS. Subscribe to our social networks: Twitter, Telegram, FB to learn about our publications and Maxilect news.

--

--

Maxilect

We are building IT-solutions for the Adtech and Fintech industries. Our clients are SMBs across the Globe (including USA, EU, Australia).