Comparing React Form Libraries: SurveyJS, Formik, React Hook Form, React Final Form And Unform
This article has been kindly supported by our dear friends at SurveyJS. A product suite of open-source JavaScript client-side components designed to simplify the creation of a full-cycle form management system fully integrated in your IT infrastructure.
Working with user input has always been one of the most vital parts of developing any website. Handling things like form validation, submission, and displaying errors can become complex, so using existing form solutions may be the way to go, and there are several solutions for dealing with forms in React.
In this article, we will look at SurveyJS, Formik, React Hook Form, React Final Form and Unform. We will compare how they are used, how we can integrate them into custom UI components, and how to set up dependent fields with them. Along the way, we will also learn how to validate forms using Yup, a JavaScript object schema validator.
This article is useful for those who want to know the best form libraries to use for future applications.
Note: This article requires a basic understanding of React and Yup.
Should You Use A Form Library?
Forms are an integral part of how users interact with the web. They are used to collect data for processing from users, and many websites today have one or more forms. While forms can be handled in React by making them controlled components, it can become tedious with a lot of repetitive code if you build a lot of forms. You have an option of reaching out to one of the many form libraries that exist in the React ecosystem. These libraries make it easier to build forms of varying complexity, as they provide validation and state management out of the box, among other useful form-handling features.
Factors To Be Considered
It is one thing to know the different form libraries available, and another to know the appropriate one to use for your next project. In this article, we will examine how these form libraries work and compare them based on the following factors:
- Implementation
We will look at their APIs, and consider how easy it is to integrate them into an app, handle form validation, submission, and the overall developer experience. - Usage with Custom Components
How easy is it to integrate these libraries with inputs from UI libraries like Material UI? - Dependent Fields
You may want to render a form field B that depends on the value of a field A. How can we handle that use case with these libraries? - Learning Curve
How quickly can you start using these forms? How much learning resources and examples are available online? - Bundle Size
We always want our applications to be performant. What tradeoffs are there in terms of the bundle size of these forms?
We will also consider how to make multi step forms using these libraries. I didn’t add that to the list above due to how I structured the article. We will look at that at the end of the article.
React Form Library by SurveyJS
SurveyJS Form Library is an open-source client-side component that renders dynamic JSON-driven forms in React applications. It uses JSON objects to communicate with the server. These objects, also known as JSON schemas, define various aspects of a form, including its style, contents, layout, and behavior in response to user interactions, such as data submission, input validation, error messages, and so on.
The library has native support for React. It is free to use and is distributed under the MIT license. The SurveyJS product family also includes a self-hosted JSON form builder that features drag-and-drop UI, a CSS Theme Editor, and a GUI for conditional logic and form branching.
Features
- It’s suitable for multi-page forms, quizzes, scored surveys, calculator forms, and survey pop-ups.
- Compatible with any server & database.
- Integration demos for PHP, ASP.NET Core, and NodeJS.
- All data is stored on your own servers; therefore, there are no limits on the number of forms, submissions, and file uploads.
- 20+ accessible input types, panels for question grouping, dynamic questions with a duplicate group option.
- Input validation, partial submits & auto-save, lazy loading, load choices from web services.
- Custom input fields
- Carry forward responses, text piping, autocomplete
- Integration with 3rd-party libraries and payment systems
- Support for webhooks
- Expression language (Built-in & Custom Functions), data aggregation within a form
- Auto-localization and multi-locale surveys, support for RTL languages
- Weekly updates
- 120+ starter demos & tutorials
How To Install
npm install survey-react-ui --save
How To Use
import 'survey-core/defaultV2.min.css';
import { Model } from 'survey-core';
import { Survey } from 'survey-react-ui';
const surveyJson = {
elements: [{
name: "FirstName",
title: "Enter your first name:",
type: "text"
}, {
name: "LastName",
title: "Enter your last name:",
type: "text"
}]
};
function App() {
const survey = new Model(surveyJson);
return <Survey model={survey} />;
}
export default App;
SurveyJS Form Library for React consists of two npm packages: survey-core
(platform-independent code) and survey-react-ui
(rendering code). Run the npm install survey-react-ui --save
command to install survey-react-ui
. The survey-core package will be installed automatically as a dependency. Another advantage of SurveyJS is its seamless integration with custom UI libraries and their form components. This dedicated guide demonstrates how to integrate the React Color component to a basic SurveyJS form.
To add SurveyJS themes to your application, open the React component that will render a form and import the Form Library style sheet import 'survey-core/defaultV2.min.css';
. This style sheet applies the Default theme. You can also apply a different predefined theme or create a custom one.
Next, you need to create a model that describes the layout and contents of a form. Models are specified by model schemas (JSON objects). SurveyJS website offers a full-featured JSON form builder demo that you can use to generate JSON schemas for your forms. In this example, model schema declares two textual questions, each with a title and a name. Titles are visible to respondents, while names are used to identify the questions in code. To instantiate a model, pass the model schema to the Model constructor as shown in the code above.
To render a form, import the Survey
component, add it to the template, and pass the model instance you created in the previous step to the component’s model
attribute.
As a result, you should see the following form:
Formik
Formik is a flexible library. You can choose to use Formik with native HTML elements or with Formik’s custom components. You also have the option of setting up your form validation rules or a third-party solution like Yup. It allows you to decide when and how much you want to use it. We can control how much functionality of the Formik library we use.
Formik takes care of the repetitive and annoying stuff — keeping track of values, errors, visited fields, orchestrating validation, and handling submission — so you don’t have to. This means you spend less time setting up form state and onChange
and onBlur
handlers.
Installation
npm i formik
yarn add formik
Implementation
Formik keeps track of your form’s state and then exposes it plus a few reusable methods and event handlers (handleChange
, handleBlur
, and handleSubmit
) to your form via props
. You can find out more about the methods available in Formik here.
While Formik can be used alongside HTML’s native input fields, Formik comes with a Field
component that you can use to determine the input field you want, and an ErrorMessage
component that handles displaying the error for each input field. Let’s see how these work in practice.
import { Form, Field, ErrorMessage, withFormik } from "formik";
const App = ({ values }) => (
<div className={styles.container}>
<Head>
<title>Formik Form</title>
</Head>
<Form>
<div className={styles.formRow}>
<label htmlFor="email">Email</label>
<Field type="email" name="email" id="email" />
<ErrorMessage name="email" component="span" className={styles.error} />
</div>
<div className={styles.formRow}>
<label htmlFor="email">Select a color to continue</label>
<Field component="select" name="select">
<option value="" label="Select a color" />
<option value="red" label="red" />
<option value="blue" label="blue" />
<option value="green" label="green" />
</Field>
<ErrorMessage name="select" component="span" className={styles.error} />
</div>
<div className={styles.formRow}>
<label htmlFor="checkbox">
<Field type="checkbox" name="checkbox" checked={values.checkbox} />
Accept Terms & Conditions
</label>
<ErrorMessage
name="checkbox"
component="span"
className={styles.error}
/>
</div>
<div role="group" aria-labelledby="my-radio-group">
<label>
<Field type="radio" name="radio" value="Option 1" />
One
</label>
<label>
<Field type="radio" name="radio" value="Option 2" />
Two
</label>
<ErrorMessage name="radio" component="span" className={styles.error} />
</div>
<button type="submit" className={"disabled-btn"}>
Sign In
</button>
</Form>
</div>
);
In the code above, we are working with four input fields, an email, a select, a checkbox, and a radio field. Form
is a small wrapper around an HTML <form>
element that automatically hooks into Formik’s handleSubmit
and handleReset
. We will look into what withFormik
does next.
import { Form, Field, ErrorMessage, withFormik } from "formik";
import * as Yup from "yup";
const App = ({ values }) => (
<div className={styles.container}>
<Head>
<title>Formik Form</title>
</Head>
<Form>
//form stuffs here
<button type="submit" className={"disabled-btn"}>
Sign In
</button>
</Form>
</div>
);
const FormikApp = withFormik({
mapPropsToValues: ({ email, select, checkbox, radio }) => {
return {
email: email || "",
select: select || "",
checkbox: checkbox || false,
radio: radio || "",
};
},
validationSchema: Yup.object().shape({
select: Yup.string().required("Color is required!"),
email: Yup.string().email().required("Email is required"),
checkbox: Yup.bool().oneOf([true], "Checkbox is required"),
radio: Yup.string().required("Radio is required!"),
}),
handleSubmit: (values) => {
alert(JSON.stringify(values));
},
})(App);
export default FormikApp;
withFormik
is a HOC that injects Formik context within the wrapped component. We can pass an options
object into withFormik
where we define the behaviour of the Formik context.
Formik works well with Yup in handling the form validation, so we don’t have to set up custom validation rules. We define a schema for validation pass it to validationSchema
. With mapPropsToValues
, Formik transfers the updated state of the input fields and makes the values available to the App
component through props as props.values
. The handleSubmit
function handles the form submission.
Usage With Custom Components
Another benefit of Formik is how straightforward it is to integrate custom UI libraries and their form components. Here, we set up a basic form using Material UI’s TextField
component.
import TextField from "@material-ui/core/TextField";
import * as Yup from "yup";
import { Formik, Form, Field, ErrorMessage } from "formik";
const signInSchema = Yup.object().shape({
email: Yup.string().email().required("Email is required"),
});
export default function SignIn() {
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<div className={classes.paper}>
<Formik
initialValues={initialValues}
validationSchema={SignInSchema}
onSubmit={(values) => {
alert(JSON.stringify(values));
}}
>
{({
errors,
values,
handleChange,
handleBlur,
handleSubmit,
touched,
}) => (
<Form className={classes.form} onSubmit={handleSubmit}>
<Field
as={TextField}
variant="outlined"
margin="normal"
fullWidth
id="email"
label="Email Address"
name="email"
helperText={<ErrorMessage name="email" />}
/>
<button type="submit">submit</button>
</Form>
)}
</Formik>
</div>
</Container>
);
}
Field
hooks up inputs to Formik automatically. Formik injects onChange
, onBlur
, name
, and value
props to the TextField
component. It does the same for any type of custom component you decide to use. We pass TextField
to Field
through it’s as
prop.
We can also use Material UI’s TextField
component directly. The docs also provide an example that covers that scenario.
Dependent Fields
To set up dependent fields in Formik, we access the input’s value we want to track through the values
object in the render props. Here, we are tracking the remember
field, which is the checkbox, and rendering a message based on the state of the field.
<Field
name="remember"
type="checkbox"
as={Checkbox}
Label={{ label: "You must accept our terms!!!" }}
helperText={<ErrorMessage name="remember" />}
/>
{values.remember && (
<p>
Thank you for accepting our terms. You can now submit the
form
</p>
)}
Learning Curve
Formik’s docs are easy to understand and straight to the point. It covers several use cases, including how to use Formik with third-party UI libraries like Material UI. There are also several resources from the Formik community to aid your learning.
Bundle Size
Formik is 44.4kb minified and 13.1kb gzipped.
React Hook Form
React Hook Form, or RHF is a lightweight, zero-dependency, and flexible form library built for React.
Installation
npm i react-hook-form
yarn add react-hook-form
Implementation
RHF provides a useForm
hook which we can use to work with forms.
We start by setting up the HTML input fields we need for this form. Unlike Formik, RHF does not have a custom Field
component, so we will use HTML’s native input fields.
import { useForm } from "react-hook-form";
const validationSchema = Yup.object().shape({
select: Yup.string().required("Color is required!"),
email: Yup.string().email().required("Email is required"),
checkbox: Yup.bool().oneOf([true], "Checkbox is required"),
radio: Yup.string().required("Radio is required!"),
});
const onSubmit = (values) => {
alert(JSON.stringify(values));
};
const App = () => {
const { errors, register, handleSubmit } = useForm({
resolver: yupResolver(validationSchema),
});
return (
<div className="container">
<form onSubmit={handleSubmit(onSubmit)}>
//email field
<div className="form-row">
<label htmlFor="email">Email</label>
<input type="email" name="email" id="email" ref={register} />
{errors.email && <p className="error"> {errors.email.message} </p>}
</div>
//select field
<div className="form-row">
<label htmlFor="email">Select a color to continue</label>
<select name="select" ref={register}>
<option value="" label="Select a color" />
<option value="red" label="red" />
<option value="blue" label="blue" />
<option value="green" label="green" />
</select>
{errors.select && <p className="error"> {errors.select.message} </p>}
</div>
//checkbox field
<div className="form-row">
<label htmlFor="checkbox">
<input type="checkbox" name="checkbox" ref={register} />
Accept Terms & Conditions
</label>
{errors.checkbox && (
<p className="error"> {errors.checkbox.message} </p>
)}
</div>
//radio field
<div>
<label>
<input type="radio" name="radio" value="Option 1" ref={register} />
One
</label>
<label>
<input type="radio" name="radio" value="Option 2" ref={register} />
Two
</label>
{errors.radio && <p className="error"> {errors.radio.message} </p>}
</div>
<button type="submit">Sign In</button>
</form>
</div>
);
};
RHF supports Yup and other validation schemas. To use Yup with RHF, we need to install the @hookform/resolvers
package.
npm i @hookform/resolvers
Next, we have to configure the RHF setup and instruct it to use Yup as the form validator. We do so through the resolver
property useForm
hook’s configuration. object. We pass in yupResolver
, and now RHF knows to use Yup to validate the form.
The useForm
hook gives us access to several form methods and properties like an errors
object, and the handleSubmit
and register
methods. There are other methods we can extract from useForm
. You can find the complete list of methods.
The register
function connects input fields to RHF through the input field’s ref
prop. We pass the register
function as a ref
into each element we want RHF to watch. This approach makes the forms more performant and avoids unnecessary re-renders.
The handleSubmit
method handles the form submission. It will only run if there are no errors in the form.
The errors
object contains the errors present in each field.
Usage With Custom Components
RHF has made it easy to integrate with external UI component libraries. When using custom components, check if the component you wish to use exposes a ref
. If it does, you can use it like you would native HTML form elements. However, if it doesn’t you will need to use RHF’s Controller
component.
Material-UI and Reactstrap’s TextField
expose their inputRef
, so you can pass register
to it.
import TextField from "@material-ui/core/TextField";
export default function SignIn() {
return (
<Container component="main" maxWidth="xs">
<div className={classes.paper}>
<form className={classes.form}>
<TextField
inputRef={register}
id="email"
label="Email Address"
name="email"
error={!!errors.email}
helperText={errors?.email?.message}
/>
<Button type="submit">Sign In</Button>
</form>
</div>
</Container>
);
}
In a situation where the custom component’s inputRef
is not exposed, we have to use RHF’s Controller
component.
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Checkbox from "@material-ui/core/Checkbox";
export default function SignIn() {
const { control } = useForm()
return (
<Container component="main" maxWidth="xs">
<div className={classes.paper}>
<form>
<FormControlLabel
control={
<Controller
control={control}
name="remember"
color="primary"
render={(props) => (
<Checkbox
checked={props.value}
onChange={(e) => props.onChange(e.target.checked)}
/>
)}
/>
}
label="Remember me"
/>
<Button type="submit"> Sign In </Button>
</form>
</div>
</Container>
);
}
We import the Controller
component from RHF and access the control
object from the useForm
hook.
Controller
acts as a wrapper that allows us to use custom components in RHF. Any prop passed into Controller
will be propagated down to the Checkbox
.
The render
prop function returns a React element and provides the ability to attach events and value into the component. This simplifies integrating RHF with custom components. render
provides onChange
, onBlur
, name
, ref
, and value
to the custom component.
Dependent Fields
In some situations, you may want to render a secondary form field based on the value a user puts in field a primary form field. RHF provides a watch
API that enables us to track the value of am input field.
export default function SignIn() {
const { watch } = useForm()
const terms = watch("remember");
return (
<Container component="main" maxWidth="xs">
<div className={classes.paper}>
<form>
//other fields above
{terms && <p>Thank you for accepting our terms. You can now submit the form</p>}
<Button type="submit"> Sign In </Button>
</form>
</div>
</Container>
);
}
Learning Curve
Asides from its extensive and straightforward documentation that covers several use cases, RHF is a very popular React Library. This means there are several learning resources to get you up and running.
Bundle Size
RHF is 26.4kb minified and 9.1kb gzipped.
Final Form
Final Form is a framework-agnostic form library. However, its creator, Erik Rasmussen, created a React wrapper for Final Form, React Final Form.
Installation
npm i final-form react-final-form
yarn add final-form react-final-form
Implementation
Unlike Formik and React Hook Form, React Final Form (RFF) does not support validation with Object Schemas like Yup out of the box. This means you have to set up validation yourself.
import { Form, Field } from "react-final-form";
export default function App() {
return (
<div className={styles.container}>
<Head>
<title>React Final Form</title>
</Head>
<Form
onSubmit={onSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
//email field
<Field name="email">
{({ input, meta }) => (
<div className={styles.formRow}>
<label>Email</label>
<input {...input} type="email" placeholder="Email" />
</div>
)}
</Field>
//select field
<Field name="select" component="select">
{({ input, meta }) => (
<div className={styles.formRow}>
<label htmlFor="select">Select a color to continue</label>
<select {...input}>
<option value="" label="Select a color" />
<option value="red" label="red" />
<option value="blue" label="blue" />
<option value="green" label="green" />
</select>
</div>
)}
</Field>
//checkbox field
<Field name="checkbox">
{({ input, meta }) => (
<div className={styles.formRow}>
<label>
<input {...input} name="checkbox" type="checkbox" />
Accept Terms & Conditions
</label>
</div>
)}
</Field>
//radio field
<div className={styles.formRow}>
2 <Field
name="radio"
component="input"
type="radio"
value="Option 1"
>
{({ input, meta }) => (
<div>
<label>
One
<input {...input} type="radio" value="Option 1" />
</label>
</div>
)}
</Field>
<Field
name="radio"
component="input"
type="radio"
value="Option 2"
>
{({ input, meta }) => (
<div>
<label>
Two
<input {...input} type="radio" value="Option 2" />
</label>
</div>
)}
</Field>
</div>
<button type="submit" className={"disabled-btn"}>
Sign In
</button>
</form>
)}
/>
</div>
);
}
The Form
component is a special wrapper provided by RFF that manages the state of the form. The main props
when using RFF are onSubmit
, validate
, and render
. You can get more details on the form props RFF works with.
We start by setting up the necessary input fields. render
handles the rendering of the form. Through the render props, we have access to the FormState
object. It contains form methods like handleSubmit
, and other useful properties regarding the state of the form.
Like Formik, RFF has its own Field
component for rendering input fields. The Field
component registers any input field in it, subscribes to the input field’s state, and injects both field state and callback functions, onBlur
, onChange
, and onFocus
via a render prop.
Unlike Formik and RHF, RFF does not provide support for any validation Schema, so we have to set up custom validation rules.
const onSubmit = (values) => {
alert(JSON.stringify(values));
};
const validate = (values) => {
const errors = {};
if (!values.email) {
errors.email = "Email is Required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = "Invalid emaill address";
}
if (!values.checkbox) {
errors.checkbox = "You must accept our terms";
}
if (!values.select) {
errors.select = "Select is required";
}
if (!values.radio) {
errors.radio = "You must accept our terms";
}
return errors;
};
export default function App() {
return (
<div className={styles.container}>
<Form
onSubmit={onSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field name="email">
{({ input, meta }) => (
<div className={styles.formRow}>
<label>Email</label>
<input {...input} type="email" placeholder="Email" />
{meta.error && meta.touched && (
<span className={styles.error}>{meta.error}</span>
)}
</div>
)}
</Field>
<Field name="select" component="select">
{({ input, meta }) => (
<div className={styles.formRow}>
<label htmlFor="select">Select a color to continue</label>
<select {...input}>
<option value="" label="Select a color" />
<option value="red" label="red" />
<option value="blue" label="blue" />
<option value="green" label="green" />
</select>
{meta.error && meta.touched && (
<span className={styles.error} style={{ display: "block" }}>
{meta.error}
</span>
)}
</div>
)}
</Field>
<Field name="checkbox">
{({ input, meta }) => (
<div className={styles.formRow}>
<label>
<input {...input} name="checkbox" type="checkbox" />
Accept Terms & Conditions
</label>
{meta.error && meta.touched && (
<span className={styles.error}>{meta.error}</span>
)}
</div>
)}
</Field>
<div className={styles.formRow}>
<Field
name="radio"
component="input"
type="radio"
value="Option 1"
>
{({ input, meta }) => (
<div>
<label>
One
<input {...input} type="radio" value="Option 1" />
</label>
</div>
)}
</Field>
<Field
name="radio"
component="input"
type="radio"
value="Option 2"
>
{({ input, meta }) => (
<div>
<label>
Two
<input {...input} type="radio" value="Option 2" />
</label>
{meta.error && meta.touched && (
<span className={styles.error}>{meta.error}</span>
)}
</div>
)}
</Field>
</div>
<button type="submit" className={"disabled-btn"}>
Sign In
</button>
</form>
)}
/>
</div>
);
}
The validate
function handles the validation for the form. The onSubmit
function will be called with the values of your form when the user submits the form and all validation passes. Validation runs on input change by default. However, we can also pass a validateonBlur
prop to the Form
component validation so it also runs on blur.
To display the validation errors, we make use of the meta
object. We can get access to the metadata and state of each input field through the meta
object. We can find out if the form has been touched or has any errors through the meta
’s touched
and error
properties respectively. These metadata are part of the props of the Field
component. If the input fields have been touched, and there is an error for we display that error.
Usage With Custom Components
Working with custom input components in RFF is straightforward. Using the render props method in the Field
component, we can access the input
and meta
of each input field.
const onSubmit = (values) => {...};
const validate = (values) => {...};
export default function App() {
return (
<Form
onSubmit={onSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field name="email" placeholder="email" validate={validate}>
{({ input, meta }) => (
<div>
<TextField label="email" type="email" {...input} />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>
<Button type="submit"> Sign In </Button>
</form>
)}
></Form>
);
}
}
RFF’s Field
component bundles all of the props that your input component needs into one object prop, called input
, which contains name
, onBlur
, onChange
, onFocus
, and value
. The input
prop is what we spread to the TextField
component. The custom form component you plan on using must support these props in order to be compatible with RFF.
Alternatively, we could render Material UI’s TextField
using the component Field in RFF. However, we won’t be able to access the input
and meta
data using this method.
///other form stuff above
<Field
name="email"
component={TextField}
type="email"
label="Email"
/>
//we won’t be able to access the input
//and meta data using this method.
///other form stuff below
Dependent Fields
The values
object can be accessed from the render prop. From here, we can track the state of the remember
field, and if true
, render a message, or whatever use case fits your app’s needs.
<Form
onSubmit={onSubmit}
render={({ handleSubmit, values }) => (
<form onSubmit={handleSubmit}>
<Field name="remember" type="checkbox">
{({ input }) => (
<div>
<label>Remember me</label>
<input {...input} type="checkbox" />
</div>
)}
</Field>
{values.remember && (
<p>Thank you for accepting our terms. You can now submit the form.</p>
)}
<button type="submit"> Submit</button>
</form>
)}
/>
Learning Curve
Compared to other form libraries, RFF does not have as many learning resources. Also, the docs do not go into detail to show how RFF can be used with Yup or any other validation schema, and that would be a helpful addition.
Bundle Size
RFF is 8.9kb minified and 3.2kb gzipped.
Unform
A core aspect of Unform’s API design is how straightforward it is to hook form inputs to Unform. Initially, Unform came with its built-in input components, however, it no longer follows that pattern. This means you have to register each field you want Unform to track. We can do that with the registerField
method Unform provides.
Installation
npm i @unform/web @unform/core
yarn add @unform/web @unform/core
Implementation
The first step in using Unform is registering the input fields we want the library to track.
The useField
hook is the heart of Unform. From this hook, we get access to the fieldName
, defaultValue
, registerField
, error
, and more. Let’s see what they do and how they work.
import { useEffect, useRef } from "react";
import { useField } from "@unform/core";
const Input = ({ name, label, ...rest }) => {
const inputRef = useRef();
const {
fieldName,
defaultValue,
registerField,
error,
clearError
} = useField(name);
useEffect(() => {
registerField({
name: fieldName,
ref: inputRef.current,
getValue: (ref) => {
return ref.value;
}
});
}, [fieldName, registerField]);
return (
<>
<label htmlFor={fieldName}>{label}</label>
<input
id={fieldName}
ref={inputRef}
onFocus={clearError}
defaultValue={defaultValue}
{...rest}
/>
{error && <span className="error">{error}</span>}
</>
);
};
export default Input;
fieldName
: a unique field name.defaultValue
: the default value of the field.registerField
: this method is used to register a field on Unform. When registering a field, you can pass some properties to theregisterField
method.error
: the error message of the registered input field.
Whenever the input component loads, we call the registerField
method in the useEffect
to register the input. registerField
accepts some options:
name
: the name of the field that needs to be registered.ref
: the reference to the field.getValue
: this function returns the value of the field.
We pass the methods; fieldName
, defaultValue
, clearError
and any other props to the input component. clearError
clears the error of an input field on focus if there is any. This is how we register input fields with Unform. We do the same thing for the select, checkbox, and radio input fields.
Now that we have registered the input fields, we have to bring everything together.
import React, { useRef } from "react";
import { Form } from "@unform/web";
import * as Yup from "yup";
import Input from "./Input";
import Radio from "./Radio";
import Checkbox from "./Checkbox";
import Select from "./Select";
const radioOptions = [
{ value: "option 1", label: "One" },
{ value: "option 2", label: "Two" }
];
const selectOptions = [
{ value: "", label: "Select a color" },
{ value: "red", label: "Red" },
{ value: "blue", label: "Blue" },
{ value: "green", label: "Green" }
];
const App = () => {
const formRef = useRef();
async function handleSubmit(data) {
//form validation goes here
}
return (
<div className="container">
<Form ref={formRef} onSubmit={handleSubmit}>
<div className="form-row">
<Input name="email" label="Email" type="email" />
</div>
<div className="form-row">
<Select
name="select"
label="Select a color to continue"
options={selectOptions}
/>
</div>
<div className="form-row">
<Checkbox name="checkbox" label="Accept terms and conditions" />
</div>
<div>
<Radio name="radio" options={radioOptions} />
</div>
<button className="button" type="submit">
Sign in
</button>
</Form>
</div>
);
};
export default App;
We import the Form
component from Unform and set up a form reference for the Form
component. We get access to several helpful methods from this reference.
Now that we’ve registered the input fields, created a form reference, and set up the form, the next step is to handle the form validation and submission.
import React, { useRef } from "react";
const validationSchema = Yup.object().shape({
select: Yup.string().required("Color is required!"),
email: Yup.string().email().required("Email is required"),
checkbox: Yup.bool().oneOf([true], "Checkbox is required"),
radio: Yup.string().required("Radio is required!")
});
const App = () => {
const formRef = useRef();
async function handleSubmit(data) {
try {
await validationSchema.validate(data, {
abortEarly: false
});
// Validation passed - do something with data
alert(JSON.stringify(data));
} catch (err) {
const errors = {};
// Validation failed - do show error
err.inner.forEach((error) => {
errors[error.path] = error.message;
});
formRef.current.setErrors(errors);
}
}
return (
<div className="container">
<Form ref={formRef} onSubmit={handleSubmit}>
//other form stuffs below
}
Unform supports validation with Yup, so we create a schema
. By default, the schema’s validate(
) method will reject the promise as soon as it finds the error and won’t validate any further fields. So to avoid that you need to pass the abortEarly
option and set the boolean to false { abortEarly: false }
. We use the setErrors
method from the form reference we created to set the errors for the form if any.
Similar to the handleSubmit
function that handles submit validation, a handleChange
function can be created that will handle form validation as the user types.
import { useRef, useState } from "react";
export default function App() {
const [form, setForm] = useState({
name: ""
});
const formRef = useRef(null);
async function handleSubmit(data) {
//handleSubmit logic
}
const handleNameChange = async ({ target: { value } }) => {
setForm({
...form,
name: value
});
try {
formRef.current.setErrors({});
let schema = Yup.object().shape({
name: Yup.string()
.required()
.max(20)
.matches(
/^[a-zA-Z]+(([',. -][a-zA-Z ])?[a-zA-Z]*)*$/,
"Please enter valid name"
)
});
await schema.validate(form, {
abortEarly: false
});
console.log(form);
} catch (err) {
console.log(err);
const validationErrors = {};
if (err instanceof Yup.ValidationError) {
err.inner.forEach((error) => {
validationErrors[error.path] = error.message;
});
formRef.current.setErrors(validationErrors);
}
}
};
return (
<div className="container">
<Form ref={formRef} noValidate onSubmit={handleSubmit}>
<div className="form-row">
<Input name="name" label="Full name" onChange={handleNameChange} />
</div>
<button type="submit" className="button">
Save
</button>
</Form>
</div>
);
}
Both validation functions work the same way, so the logic for the submit and onChange
validation are the same. However, there are some differences. Unlike validation on submit, where we can get access to and validate the form data with handleSubmit
, we need a way to handle the input’s data in handleNameChange
. To do that, we set up a form state where we will store the input’s value. Then as the user types, we update the form state through setForm
. Now that we have access to the form data, we validate it in the same manner we did in handleSubmit
. Lastly, we have to pass handleNameChange
to the input’s onChange
prop, which we do.
You will note that in handleNameChange
, I created a different validation schema than the one I used in handleSubmit
. I did this so there will be two different errors to make the onChange
and onSubmit
error different. However, in a real-world project, you would use the same validation schema.
The sandbox below provides a demo for validation on input change.
Usage with Custom Components
We integrate third-party UI components with Unform the same way we do with native HTML inputs, through the registerField
method.
import TextField from "@material-ui/core/TextField";
const MaterialUIInput = ({ name, label, ...rest }) => {
const inputRef = useRef();
const { fieldName, defaultValue, registerField, error } = useField(name);
useEffect(() => {
//form registration here
}, [fieldName, registerField]);
return (
<>
<TextField
inputRef={inputRef}
/>
Dependent Fields
Unform currently does not provide any watch functionality to help in creating dependent fields. However, there are plans in the library’s roadmap to create a useWatch
hook for tracking the form state.
Learning Curve
Unform is not the easiest library to get started with. The process of registering input fields is not developer-friendly compared to other articles. Also, validation and accessing errors
object in Unform is more tedious than it should be. It also doesn’t help that there is no way to access form values to set up dependent fields. It doesn’t come with a lot of features that would be needed when working with forms, and there are very few resources out there that show different use cases in using Unform. However, the documentation provides a few examples.
Overall, it is not the most straightforward library to use. I believe more work can be done in providing a better documentation.
With that being said, Unform is still under development, and the team is currently working on new features.
Bundle Size
Unform is 10.4kb minified and 3.7kb gzipped.
Creating Multi Step Forms
I have created multi-step form demos for each library. I left this to the last section because the implementation for the libraries remains similar. The only different thing is the multi-step form UI. Let’s see the structure of the UI.
Breaking Down The Form Wizard
For the multi step wizard, let’s see the file structure and how it works.
├── components
| ├── FormCard.js
| ├── FormCompleted.js
| └── Forms
| ├── BillingInfo.js
| ├── ConfirmPurchase.js
| ├── index.js
| └── PersonalInfo.js
├── context
| └── index.js
├── pages
| ├── api
| | └── hello.js
| ├── index.js
| └── _app.js
└── styles
├── globals.css
└── styles.module.scss
Let’s look at the App.js
file.
const App = () => {
const [formStep, setFormStep] = useState(0);
const nextFormStep = () => setFormStep((currentStep) => currentStep + 1);
const prevFormStep = () => setFormStep((currentStep) => currentStep - 1);
return (
<div className={styles.container}>
<Head>
<title>Next.js Multi Step Form</title>
</Head>
<h1>Formik Multi Step Form</h1>
<FormCard currentStep={formStep} prevFormStep={prevFormStep}>
{formStep >= 0 && (
<PersonalInfo formStep={formStep} nextFormStep={nextFormStep} />
)}
{formStep >= 1 && (
<BillingInfo formStep={formStep} nextFormStep={nextFormStep} />
)}
{formStep >= 2 && (
<ConfirmPurchase formStep={formStep} nextFormStep={nextFormStep} />
)}
{formStep > 2 && <FormCompleted />}
</FormCard>
</div>
);
};
Here, we define a formStep
state, which holds the state of the current step of the form wizard. We also define prevFormStep
and nextFormStep
functions to go back and forth in the form wizard.
Next, we pass the formStep
state prevFormStep
to the FormCard
. This will enable us to go a step backward and also display the current step of the form.
Finally, we pass formStep
and nextFormStep
to the form components. We conditionally render each form based on the value of formStep
. We use >=
to render the forms because we want the forms to remain rendered even though it’s step has been passed. This makes tracking the form values easier.
Now the FormCard
component. This is the container for each form.
export default function FormCard({ children, currentStep, prevFormStep }) {
return (
<div className={styles.formCard}>
{currentStep < 3 && (
<>
{currentStep > 0 && (
<button
className={styles.back}
onClick={prevFormStep}
type="button"
>
back
</button>
)}
<span className={styles.steps}>Step {currentStep + 1} of 3</span>
</>
)}
{children}
</div>
);
}
FormCard
does 3 things: conditionally render the back button based on the value of formStep
, show the value of the current step to the user as a form of progress tracker, and render its children, which is the current form being displayed.
The form components have a similar structure. Let’s see PersonalInfo
.
import { useFormData } from "../../context";
export default function PersonalInfo({ formStep, nextFormStep }) {
const { setFormValues } = useFormData();
const handleSubmit = (values) => {
setFormValues(values);
nextFormStep();
};
return (
<div className={formStep === 0 ? styles.showForm : styles.hideForm}>
<h2>Personal Info</h2>
<form>
<div className={styles.formRow}>
<label htmlFor="email">Email</label>
<input type="email" name="email" id="email" />
</div>
<button type="button" onClick={nextFormStep}>
Next
</button>
</form>
</div>
);
}
In the root div
, we conditionally apply showForm
and hideForm
styles to show or hide the form. We do this because of how we implemented the form wizard.
<FormCard currentStep={formStep} prevFormStep={prevFormStep}>
{formStep >= 0 && (
<PersonalInfo formStep={formStep} nextFormStep={nextFormStep} />
)}
//other forms below
We display a particular form based on the value of formStep
. However, we use the >=
conditional to conditionally render a form. This means that all the forms will render as formStep
increases. We want them to render, but also be hidden. That’s why we conditionally apply the showForm
and hideForm
styles.
Finally, we have the handleSumbit
. Let’s look into how that works.
Handling form submission starts with creating a context to store the values of the forms.
import { useState, createContext, useContext } from "react";
export const FormContext = createContext();
export default function FormProvider({ children }) {
const [data, setData] = useState({});
const setFormValues = (values) => {
setData((prevValues) => ({
...prevValues,
...values,
}));
};
return (
<FormContext.Provider value={{ data, setFormValues }}>
{children}
</FormContext.Provider>
);
}
export const useFormData = () => useContext(FormContext);
setFormValues
is a function that takes the data from each form and uses those values to update the state of data
, which will hold the values of each form.
We can access the form data and the setFormValues
function from useFormData
.
import { useFormData } from "../../context";
export default function PersonalInfo({ formStep, nextFormStep }) {
const { setFormValues } = useFormData();
const handleSubmit = (values) => {
setFormValues(values);
nextFormStep();
};
In each form, we pull setFormValues
from useFormData
and pass in the values of the form. This way, as we submit each form, the data is being stored.
Finally, if the form has been successfully filled, the FormCompleted
component renders.
export default function FormCompleted() {
return <h2>Thank you for your purchase! 🎉</h2>;
}
The core aspect of creating the multi-step form is the wizard. The form validation and submission for each library are the same as what we covered above. I attached these sandbox demos for integration with each library.
Summary
In comparing these 4 form libraries, we have considered different factors. The image below is a tabular representation of how these libraries stand against each other.
I use either Formik or RHF in handling forms in my projects. These are my top choices because they are the most popular, have the clearest and most extensive documentation, and the most learning resources in terms of YouTube videos and articles.
Conclusion
Forms will remain a critical part of how users interact with the web, and these libraries, among others, have done a good job in creating form management solutions for the common developer use cases, and much more.
Resources
- SurveyJS Documentation
- Formik Docs
- React Hook Form Docs
- React Final Form Docs
- Unform Docs
- Yup Docs
- React Form Validation With Formik And Yup by Nefe Emadamerho-Atori for Smashing Magazine