DEV Community

Cover image for Looking for the Best React Form Library? It’s Probably on This List
Peter Mbanugo
Peter Mbanugo

Posted on • Edited on • Originally published at pmbanugo.me

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>}
/>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (19)

Collapse
 
dastasoft profile image
dastasoft

I usually use React Hook Forms, they are updating the library often and with the context it is very easy to create components that take advantage of the form utilities they provide.

If the form is big enough the combination of React Hook Forms + Yup is awesome, I used to really hate working with forms but now I can admit it's very affordable.

Collapse
 
sunpietro profile image
Piotr Nalepa

The only downside of it is its asynchronicity. Whenever you need to get the most actual data you need to use useWatch or watch from useForm hook. They decrease performance heavily.

Collapse
 
hidaytrahman profile image
Hidayt Rahman

Also, It doesn't work properly in case of setting the default value.

Thread Thread
 
sunpietro profile image
Piotr Nalepa

I haven't been in such situation before.

Thread Thread
 
hidaytrahman profile image
Hidayt Rahman • Edited

But when you use this with material UI. you will get to know how complex is this to handle the default value.

Collapse
 
gkhan205 profile image
Ghazi Khan

You can also have a look into this.
npmjs.com/package/formify-react

Collapse
 
michelledev3 profile image
Info Comment hidden by post author - thread only accessible via permalink
michelledev • Edited

This is just a share of a product I use that has a deep integrations with React, Vue and even Angular. Have you heard of Wijmo? grapecity.com/wijmo/react-ui-compo... It has 100+ dynamic JS UI Components.
Low level learning curve available on npm

Collapse
 
pmbanugo profile image
Peter Mbanugo

You shouldn't be doing this. Your comment is not related to my post and trying to promote your product in a manner I find to be unethical.

Collapse
 
michelledev3 profile image
michelledev

Didn't mean to offend, we have a react library similar to Kendo. It relates to your article as we are not listed and thought I would send you the details, in case you were interesting in learning about other alternatives. I will remove my comment but it was not be unethical, it was being informative. This is a place where we come together to learn and share. I learned from your article and then I shared what I know. Sorry Peter.

Thread Thread
 
pmbanugo profile image
Peter Mbanugo

Seems you updated your comment....

How does Wijmo deal with Forms in React? I couldn't find anything relating to that in your library.

Collapse
 
mpwiatr profile image
mpwiatr

Hey @pmbanugo ! What about uniforms.tools/? IMHO it's a good one. What made you decide it wasn't in your summary? I think it has even better solutions in some specific areas than mentioned ones.

Collapse
 
mohithgupta profile image
K.Mohith Gupta

Can you suggest a form library for a multi step form in which I can include interactivity ( such as handleClick or onHover ) for the step numbers (the numbers at the top that show the step you are in) . I want to include handle click such that user goes directly to that step if he clicks on a certain number (step number).

Collapse
 
tracker1 profile image
Michael J. Ryan

It's mostly the combination of forms with a tabstrip, customizing the tab strip... as to what that takes, depends on your UI components you're already using as to what approach or styling is needed.

In the end, what you're doing is a combination of things, not an out of the box component. This is where you get into custom development beyond CRUD.

Collapse
 
pmbanugo profile image
Peter Mbanugo

Hi Gupta,

Any of the libraries I mentioned can do that. Here's an example using KendoReact .

Collapse
 
mohithgupta profile image
K.Mohith Gupta

Thanks

Collapse
 
heresinceres profile image
徐凯

You can also have a look into this.
github.com/react-component/field-form

Collapse
 
movingelectrons profile image
Jerome Stonebridge

I prefer to just use forms provided by bootstrap:

react-bootstrap.github.io/componen...

Collapse
 
gustavoluchi profile image
Gustavo Luchi

Nice article. You said "while React Hook Form uses hooks and uncontrolled inputs". I'm not sure that you meant that, but React Hook Form works pretty well with controlled inputs also (done it myself).

Collapse
 
pmbanugo profile image
Peter Mbanugo

I haven't looked at it recently but I think that was the case as at when I wrote the article

Some comments have been hidden by the post's author - find out more