Looking for the Best React Form Library? It’s Probably on This List

Looking for the Best React Form Library? It’s Probably on This List

Forms are used to collect data for processing from users. Many websites today have one or more forms. If you work with React, you know that it provides a way to handle forms using controlled components. However, it can become tedious with a lot of repetitive code if you build a lot of forms, and you may want to also validate and keep track of the visited fields or form state. For this reason, you might seek out a form library that can help make it easier to build forms of varying complexity, with validation and state management.

In this post, I'm going to list some React form libraries you should consider. I'll include code snippets for a form to collect data and you will see the syntax for each and how each one differs in usage.

Formik

Formik is one of the popular libraries (with 26.2k stars on GitHub) for building forms in React. Formik helps you with managing the form state, handling submission, formatting and validating form values. It's also quite small in size. It is 13.1 kB when gzipped and minified, with support for TypeScript and works with React Native.

Here's how you would write a form to collect user data with Formik:

import { Formik, Form, Field, ErrorMessage } from "formik";

const DataForm = () => (
  <>
    <h1>Your Data</h1>
    <Formik
      initialValues={{ name: "", email: "", acceptedTerms: false }}
      validate={(values) => {
        const errors = {};
        if (!values.name) {
          errors.name = "Required";
        }

        if (!values.acceptedTerms) {
          errors.acceptedTerms =
            "You must accept the terms and conditions before you proceed.";
        }

        if (!values.email) {
          errors.email = "Required";
        } else if (
          !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
        ) {
          errors.email = "Invalid email address";
        }
        return errors;
      }}
      onSubmit={(values, { setSubmitting }) => {
        // post data to server
        alert(JSON.stringify(values, null, 2));
        setSubmitting(false);
      }}
    >
      {({ isSubmitting, dirty, handleReset }) => (
        <Form>
          <div>
            <label>
              Name
              <Field type="text" name="name" />
            </label>
            <ErrorMessage name="name" component="span" />
          </div>
          <div>
            <label htmlFor="email">Email</label>
            <Field type="email" name="email" />
            <ErrorMessage name="email" component="span" />
          </div>
          <div>
            <label>Accept terms</label>
            <Field type="checkbox" name="acceptedTerms" />
            <ErrorMessage name="acceptedTerms" component="span" />
          </div>
          <button
            type="button"
            onClick={handleReset}
            disabled={!dirty || isSubmitting}
          >
            Reset
          </button>
          <button type="submit" disabled={isSubmitting}>
            Submit
          </button>
        </Form>
      )}
    </Formik>
  </>
);

export default DataForm;

Formik comes with components that make it easier to manage form state and then exposes the form data via props. You wrap the form with the <Formik /> component and pass it props. In the example, I passed in prop for initialValues, which is an object with keys that match the name or id of the fields it should bind to and the values for the fields when they’re rendered.

The onSubmit prop is the function that will be called when the form is submitting and the form values are valid. If the form is invalid, then the error messages will be displayed for each field using the <ErrorMessage /> component. I prefer using <ErrorMessage /> compared to checking the error state and if the field has been visited. That is, writing <ErrorMessage name="email" component="span" /> instead of {errors.email && touched.email && <span>errors.email</span>}.

You can use field-level validation or form-level validation by specifying a validate props for <Field /> or <Formik />. You specify a synchronous or asynchronous function that returns the error message for field-level validation, or an object with keys that match the respective fields for form-level validation. You can use libraries like Yup or Joi if you don't want to write your own validation function. Formik has a special prop for Yup called validationSchema which will automatically transform Yup's validation errors into a pretty object whose keys match the respective form fields.

You can access the form state through props such as dirty and isSubmitting, as seen in the example, and also event handlers like handleSubmit. In the example, the form is reset by calling the handleReset function passed in as props.

I like how easy it can be to use the <Field /> and <ErrorMessage />, but you can also use HTML form controls or pass a custom component to <Field />. You also have access to the event handlers, form values, and validation status as props.

KendoReact Form

KendoReact Form is a small and fast library with full accessibility support, all just in 6.2 kB gzipped and minified. It is the smallest in size, when compared to the others on this list. It has a simple syntax and provides components and props to access form state, with full support for TypeScript. It supports field-level and form-level validation. Let's look at a similar user data form built with KendoReact.

import { useCallback } from "react";
import { Form, Field, FormElement } from "@progress/kendo-react-form";

const emailRegex = new RegExp(/\S+@\S+\.\S+/);
const emailValidator = (value) =>
  emailRegex.test(value) ? "" : "Please enter a valid email.";

const CustomCheckbox = (fieldRenderProps) => {
  const {
    validationMessage,
    visited,
    value,
    onChange,
    onFocus,
    onBlur,
    ...props
  } = fieldRenderProps;

  const onValueChange = useCallback(() => {
    onChange({ value: !value });
  }, [onChange, value]);

  return (
    <div onFocus={onFocus} onBlur={onBlur}>
      <label htmlFor={props.name}>{props.label}</label>
      <input
        type="checkbox"
        onChange={onValueChange}
        checked={value}
        id={props.id}
      />
      {visited && validationMessage && <span>{validationMessage}</span>}
    </div>
  );
};

const checkboxValidator = (value) =>
  value ? "" : "You must accept the terms and conditions before you proceed.";

const DataForm = () => {
  const handleSubmit = (dataItem) => alert(JSON.stringify(dataItem, null, 2));
  return (
    <Form
      initialValues={{ name: "", email: "" }}
      onSubmit={handleSubmit}
      validator={({ name, email, acceptedTerms }) => ({
        name: name ? "" : "Your name is required",
        email: emailValidator(email),
        acceptedTerms: checkboxValidator(acceptedTerms),
      })}
      render={(formRenderProps) => (
        <FormElement>
          <fieldset>
            <legend>Your Data</legend>
            <div>
              <label>Full Name </label>
              <Field name="name" component="input" />
              {formRenderProps.touched && formRenderProps.errors.name && (
                <span>{formRenderProps.errors.name}</span>
              )}
            </div>

            <div>
              <label>Email </label>
              <Field name="email" type="email" component="input" />
              {formRenderProps.touched && formRenderProps.errors.email && (
                <span>{formRenderProps.errors.email}</span>
              )}
            </div>
            <Field
              name="acceptedTerms"
              label={"Accept Terms"}
              component={CustomCheckbox}
            />
          </fieldset>
          <div>
            <button
              type="submit"
              disabled={!formRenderProps.modified}
              onClick={formRenderProps.onFormReset}
            >
              Reset
            </button>
            <button type={"submit"} disabled={!formRenderProps.allowSubmit}>
              Submit
            </button>
          </div>
        </FormElement>
      )}
    />
  );
};

export default DataForm;

The syntax is simple to get started with. You pass some props to the <Form /> component. In the example, I set the initialValues, onSubmit prop to handle the form submission, and validator for form-level validation. If you choose to use field-level validation, you can pass validator props to <Field />. The <Field /> component uses the name prop to store the value for the input and can render a custom component or an HTML element such as input. Unlike Formik where you can specify the type prop and omit component and it'll render <input />, KendoReact requires you to pass a value for component.

You get access to the form state and event handlers as props. In the example, I used onFormReset to reset the form when the Reset button is clicked, and allowSubmit to know when to disable the Submit button. You also get access to the errors object, which contains the message for each field that failed validation.

Usage of KendoReact Form requires a paid license. Getting a KendoReact license gives you access to a suite of components to build rich, accessible web applications, with fast technical support. You get different components for displaying data (e.g. Grid), form input components like checkbox, dropdown, and different data input components.

The Label package gives you components that can be visually associated with form elements, and assistive technologies will read the label's content when the form element is focused. The form input element will receive focus when the label associated with it is clicked. For example, the Error component, which is part of the Labels package, allows you to display an error message. Here's an example of how you can use the KendoReact Input, Label, and Form packages together.

import { useState } from "react";
import { Label, Error } from "@progress/kendo-react-labels";
import { Input } from "@progress/kendo-react-inputs";
import { FormElement } from "@progress/kendo-react-form";

const App = () => {
  const [value, setValue] = useState();
  const editorId = "firstName";
  return (
    <FormElement style={{ maxWidth: 400 }}>
      <Label editorId={editorId}>First Name:&nbsp;</Label>
      <Input
        id={editorId}
        value={value}
        ariaDescribedBy={"firstNameError"}
        onChange={(e) => setValue(e.value)}
      />
      {!value && <Error id={"firstNameError"}>This field is required.</Error>}
    </FormElement>
  );
};

The KendoReact Form documentation is well detailed and includes form design guidelines on creating and styling forms with accessibility support.

React Hook Form

React Hook Form is a flexible library that embraces the hooks API and uncontrolled components. It is open source and has 17.3k GitHub stars, and it’s 9.1kB when gzipped and minified.

The API is slightly different from the others I mentioned. It has TypeScript and React Native support, but unlike the others I mentioned, there's no component to wrap your form. You will use the useForm hook it provides to access form state. Let's look at an example.

import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit, errors, reset, formState } = useForm();
  const { isDirty, isSubmitting } = formState;

  const onSubmit = (data) => alert(JSON.stringify(data, null, 2));

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h1> Your Data</h1>
      <div>
        <label>Name</label>
        <input
          type="text"
          placeholder="Full Name"
          name="name"
          ref={register({ required: "Name Required " })}
        />
        <span>{errors.name?.message}</span>
      </div>
      <div>
        <label>Email</label>
        <input
          type="text"
          placeholder="Email"
          name="email"
          ref={register({
            required: "Email Required",
            pattern: { value: /^\S+@\S+$/i, message: "Invalid email address" },
          })}
        />
        <span>{errors.email?.message}</span>
      </div>
      <div>
        <label>Accept Terms</label>
        <input
          type="checkbox"
          placeholder="Accept Terms"
          name="acceptedTerms"
          ref={register({ required: true })}
        />
        {errors.acceptedTerms && <span>You must accepet the terms</span>}
      </div>

      <button type="button" onClick={reset} disabled={!isDirty || isSubmitting}>
        Reset
      </button>
      <input type="submit" disabled={isSubmitting} />
    </form>
  );
}

To use this library, you call the useForm() hook which will return objects and functions to manage form state. The handleSubmit function will be called when the form is submitting. It accepts two functions as arguments: the first one will be called with the form data if the form validation is successful, and the second one will be called when the validation fails.

The register function allows you to register an input/select element Ref and supply validation rules as well. You can specify the error message for a validation rule when it is defined or skip it. You can see the different approach in the rule applied to the email and acceptedTerms input. If you specify an error message, you can access it in the errors object, through the message property of the validated field. If you would like to use a component to render the error message like you saw with Formik, you can install the @hookform/error-message package. With it, you can use it to display the error message for name and email as follows:

import { ErrorMessage } from "@hookform/error-message";
// other necessary code ...
<ErrorMessage errors={errors} name="name" />

<ErrorMessage
  errors={errors}
  name="email"
  render={({ message }) => <p>{message}</p>}
/>

React Final Form

React Final Form is a subscription-based form state management library based on Final Form. It uses the Observer pattern so that only the components that need updating are re-rendered as the form's state changes. By default, it subscribes to all changes, but if you want to optimize for blazing-fast perfection, you may specify only the form state that you care about.

Let's look at the syntax for using Final Form.

import { Form, Field } from "react-final-form";

const DataForm = () => (
  <>
    <h1>Your Data</h1>
    <Form
      onSubmit={(values) => alert(JSON.stringify(values, 0, 2))}
      initialValues={{ acceptedTerms: true }}
      validate={(values) => {
        const errors = {};
        if (!values.name) {
          errors.name = "Required";
        }

        if (!values.acceptedTerms) {
          errors.acceptedTerms =
            "You must accept the terms and conditions before you proceed.";
        }

        if (!values.email) {
          errors.email = "Required";
        } else if (
          !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
        ) {
          errors.email = "Invalid email address";
        }
        return errors;
      }}
      render={({
        handleSubmit,
        form,
        submitting,
        pristine,
        values,
        errors,
        touched,
      }) => (
        <form onSubmit={handleSubmit}>
          <Field name="name">
            {({ input, meta }) => (
              <div>
                <label>Username</label>
                <input {...input} type="text" placeholder="Username" />
                {meta.error && meta.touched && <span>{meta.error}</span>}
              </div>
            )}
          </Field>
          <div>
            <label>Twitter Handle</label>
            <Field name="twitter" component="input" type="text" />
          </div>
          <Field name="email">
            {({ input, meta }) => (
              <div>
                <label>Email</label>
                <input {...input} type="email" />
                {meta.error && meta.touched && <span>{meta.error}</span>}
              </div>
            )}
          </Field>
          <div>
            <label>Accept Terms</label>
            <Field name="acceptedTerms" component="input" type="checkbox" />
            {touched.acceptedTerms && errors.acceptedTerms && (
              <span>{errors.acceptedTerms}</span>
            )}
          </div>
          <div>
            <button
              type="button"
              onClick={form.reset}
              disabled={submitting || pristine}
            >
              Reset
            </button>
            <button type="submit" disabled={submitting}>
              Submit
            </button>
          </div>
        </form>
      )}
    />
  </>
);

export default DataForm;

The two components from React Final Form used in the example are <Form /> and <Field />. The <Form /> component is a wrapper over the HTML form and it manages the form state and events. You can set initial values to use for initialising the form state, the submit handler, and validate prop for form-level validation. You can also do field-level validation by passing a validate props to the <Field /> component.

You get access to render props like values which is the form data, handleSubmit, touched, and errors. The <Field /> component registers a field with the form, subscribes to the field state, and injects both field state and callback functions (onBlur, onChange, and onFocus) via render prop. I used a child render function to render a label with an associated input and error message for the name and email field.

Unlike Formik and React Hook Form, it doesn't have an <ErrorMessage /> component. However, you can easily build one that can be reused in your project using the useField hook.

import { useField } from "react-final-form";

const ErrorMessage = ({ name }) => {
  const {
    meta: { error, touched },
  } = useField(name, { subscription: { error: true, touched: true } });
  return error && touched ? <span>{error}</span> : null;
};

React Final Form is primarily maintained by Erik Rasmussen, who also built Redux Form. React Final Form is an evolution of the lessons he learnt while using and maintaining Redux Form, and also the feedback from the community. It is open source and has 6.3k GitHub stars, weighing 3.2 kB when gzipped and modified, plus 5.4 kB gzipped for Final Form.

Conclusion

Any of the React form libraries listed is fast and helps you build and manage complex forms that are performant. Formik, KendoReact Form, and React Final Form provide components for you to work with, while React Hook Form uses hooks and uncontrolled inputs. I am not in favour of the style/syntax used in React Hook Form but if you like that approach, then use it.

I would rather go for React Final Form or KendoReact Form. I like the syntax better and I can also build a <ErrorMessage /> component if needed. For me, it requires less code while working with React Final Form compared to the others.

For KendoReact Form, the only downside I think there is that it's not free, unlike the others. However, getting a license for the whole KendoReact library gives you access to a lot of cool components to build a rich, performant, and accessible application. There's a nice theme/styling that you can do with it and all your components have the same look and feel. You get different input components and they all work nicely together with the Form package for accessible React forms. All that is well documented in their Form Guidelines, which is also a useful read even if you're not using KendoReact Forms.

Like I said, any of these libraries is a good choice if it fits your needs.

Did you find this article valuable?

Support Peter Mbanugo by becoming a sponsor. Any amount is appreciated!