React-admin V4: Switching Form Library

François Zaninotto
François ZaninottoApril 12, 2022
#react#react-admin#tutorial

React-admin v3 used react-final-form for form handling ; react-admin v4 uses react-hook-form instead. Read on to learn why we made the switch and what it changes for you.

Forms Are Hard

Form handling is a complex beast. Default values, sync, and async validation, validation on submit or on blur, global and per-field errors, field arrays, dependent fields, parsing and formatting, auto-save, handling of boolean and number inputs, integration with UI libraries, performance... React comes a bit short when it comes to form handling.

React-admin doesn't manage this complexity by itself. Instead, we rely on a Form library.

It used to be redux-form in react-admin v2, but its author abandoned it and invited users to use react-final-form instead. That's what we did in react-admin v3. But now react-final-form is not getting much attention from its maintainers, and the bug count keeps growing (360 open bugs at the time of writing). Some of these bugs affect react-admin users, with no solution in sight.

react-final-form contributions are decreasing

We know that managing an open-source library is tedious and not always gratifying, so we're not going to complain about react-final-form being abandoned. In fact, looking at alternative solutions, we saw that the leader of React form management libraries, formik, is also kind of abandoned (version 2 is no longer maintained, and version 3 has been in alpha since October 2020).

Maybe because form management is hard.

So we were super pleased to discover react-hook-form, a library that has a complete feature set, and a robust codebase. At the time of writing, react-hook-form has only one open issue, which is remarkable. And its maintainers are actively working on it, releasing one new version every week.

react-hook-form contributions are strong and constant

So we made the decision to use react-hook-form in react-admin v4. And to sponsor its lead developer, bluebill1049.

The React-Admin Form API Is (Mostly) Unchanged

We replaced the library used by react-admin forms without changing the react-admin form API - for the most part. The same code works in react-admin v3 and v4:

const PostCreate = () => (
    <Create>
        <SimpleForm>
            <TextInput source="title" validate={required()} />
            <TextInput source="teaser" fullWidth multiline />
            <RichTextInput source="body" fullWidth validate={required()} />
            <ReferenceInput source="author" reference="users">
                <AutocompleteInput optionText="name" />
            </ReferenceInput>
        </SimpleForm>
    </Create>
);

Maintaining backward compatibility proved challenging, especially for advanced features. We decided to embrace the react-hook-form API so that users don't have to learn two ways of working with forms (react-admin's and react-hook-form's). This means there are some breaking changes, documented in the react-admin v4 Upgrade Guide. The most striking is that initialValues is now called defaultValues, but the upgrade should be as simple as a search and replace.

And beyond the internal handling of forms, react-hook-form opens many new possibilities.

Validation By Schema

react-hook-form allows to use external validation libraries (such as Yup, Zod, Joi, Superstruct, Vest and many others) for form validation. React-admin supports this feature through the <Form resolver> prop.

For instance, to validate a Customer form with Yup:

import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { SimpleForm, TextInput, NumberInput } from 'react-admin';

const schema = yup
    .object()
    .shape({
        name: yup.string().required(),
        age: yup.number().required(),
    })
    .required();

const CustomerCreate = () => (
    <Create>
        <SimpleForm resolver={yupResolver(schema)}>
            <TextInput source="name" />
            <NumberInput source="age" />
        </SimpleForm>
    </Create>
);

Uncontrolled Inputs For Better Performance

By default, react-admin inputs are controlled, meaning their value changes as the user edits the input value. Something like:

import { useState } from 'react';

const NameForm = ({ onSubmit }) => {
    const [name, setName] = useState('');

    const handleChange = event => setName(event.target.value);

    const handleSubmit = event => {
        onSubmit({ name });
        event.preventDefault();
    };

    return (
        <form onSubmit={handleSubmit}>
            <label>
                Name:
                <input type="text" value={name} onChange={handleChange} />
            </label>
            <input type="submit" value="Submit" />
        </form>
    );
};

But controlled components have a cost: in a naive implementation, the entire form renders at every keystroke. This becomes a problem when the form contains many controls. react-hook-form solves this by supporting uncontrolled components, where form data is handled by the DOM itself. Something like:

import { useRef } from 'react';

const NameForm = ({ onSubmit }) => {
    const input = useRef();

    const handleSubmit = event => {
        onSubmit({ name: input.current.value });
        event.preventDefault();
    };

    return (
        <form onSubmit={handleSubmit}>
            <label>
                Name:
                <input type="text" ref={input} />
            </label>
            <input type="submit" value="Submit" />
        </form>
    );
};

You can leverage the register function from the FormContext to register uncontrolled components and improve form performance:

import { useFormContext } from 'react-hook-form';

const NameInput = () => {
    const {
        register,
        formState: { errors },
    } = useFormContext();
    return (
        <input
            id="name"
            aria-invalid={errors.firstName ? 'true' : 'false'}
            {...register('name', { required: true })}
        />
    );
};

Built-In DevTools

If you need to inspect the form state, no need for console.log()! react-hook-form comes with developer tools:

react-hook-form devtools

Simply add the DevTool component to your form:

import { DevTool } from '@hookform/devtools';

const PostCreate = () => (
    <Create>
        <SimpleForm>
            <TextInput source="title" validate={required()} />
            <TextInput source="teaser" fullWidth multiline />
            {/* Add the devtool inside the form */}
            <DevTool />
        </SimpleForm>
    </Create>
);

Conclusion

This article only scratches the surface of all the features supported by react-hook-form, so I can only recommend you to dive into their great documentation.

react-hook-form

By switching to react-hook-form in react-admin v4, we made react-admin forms more robust, more flexible, and faster. Especially for complex forms with dependent inputs and value formatting, our experience is that react-admin feels more intuitive and that we never get blocked. Overall, react-hook-form increases the react-admin developer experience a lot. I'm sure you'll love it!

Did you like this article? Share it!