Introduction
Creating a form within React application involves numerous steps and can be quite tedious if it is a complex one. We need to manage the state (both initial state and changes made by user), validations and form submission.
A few libraries have come up to stream-line this process, such as SurveyJS, Formik, React Hook Form and React Final Form. Out of these, React Hook Form has become very popular. Its main advantages are:
- Easy form state management.
- Ability to seamlessly integrate with other validation libraries.
- Performance improvement due to lesser re-renders.
React Hook Form can handle form validation by itself, as well as integrate with other schema declaration and validation libraries such as Yup, Zod and Mui. In this blog, we will cover the basics steps of creating a react form using React Hook Form for state management and submission and Yup for validation. The steps would be as follows:
- Adding Form controls
- Setting Default values
- Disabling form fields
- Validation
- Form submission
1. Adding Form controls
Let us take the example of a form within a library application. This form will be used to add new book details to the application. We can have following fields :
- Serial Number
- Title
- Author
- Price
- Date of Purchase
Serial Number field will be auto populated, its value can be fetched from the database. We will need to enter appropriate values in the remaining fields. A new record should be created in the database on clicking the Submit button.
First, we need to install react-hook-form library
## Code snippet 1
npm install react-hook-form
Then, we can import the useForm hook and destructure the register function. Each of the form controls need to be registered with the form object. This enables react-hook-form to start tracking form values.
// Code snippet 2
import { useForm } from 'react-hook-form';
export const AddBookForm = () => {
const form = useForm();
const { register } = form;
return(
<div>
<form>
<div className="wrapper">
<label htmlFor="serialno"> Serial # </label>
<input type="text" id="serialno" {...register("serialno")} />
</div>
<div className="wrapper">
<label htmlFor="title"> Title </label>
<input type="text" id="title" {...register("title")} />
</div>
<div className="wrapper">
<label htmlFor="author"> Author </label>
<input type="text" id="author" {...register("author")}/>
</div>
<div className="wrapper">
<label htmlFor="price"> Price in ₹</label>
<input type="text" id="price" {...register("price")}/>
</div>
<div className="wrapper">
<label htmlFor="dop"> Date Of Purchase</label>
<input type="date" id="dop" {...register("dop")}/>
</div>
<button> Submit </button>
</form>
</div>
)
}
2. Setting Initial values
We can set initial values for the form fields, both synchronously as well as asynchronously.
This is done by passing the defaultValues object as a parameter to the useForm hook. Example of setting default values synchronously -
// Code snippet 3
const form = useForm ({
defaultValues:{
serialno: "123",
author:"",
title:"",
price: 0,
dop: "2021-01-01"
}
});
Example of setting default values asynchronously – (For demo purposes we are using jsonplaceholder API)
// Code snippet 4
const defaultPrice = 1;
const form = useForm ({
defaultValues: async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts/12"
);
const data = await response.json();
return({
serialno: data.id,
author:"",
title:"",
price: defaultPrice.toFixed(2),
dop: moment(new Date()).format("YYYY-MM-DD")})
}
});
The form will now appear as
3. Disabling form fields
Since we are populating the Serial Number from the backend, we can disable this field.
We can disable, either using the disabled attribute of the input field or adding disabled property to the register function.
// Code snippet 5
<input type="text" id="serialno" disabled {...register("serialno")} />
OR
<input type="text" id="serialno" {...register("serialno", {disabled:true})} />
Setting the disabled property to true using react-hook-form, will make the value of the field as undefined, so we cannot use it in this example. It can be used in a scenario where disabled field’s value is not being used. The disabled property can also be conditionally set to true based on the value of any other form field.
4. Validation
React-hook-form can be very easily integrated with yup. Validation can be carried out by yup and error messages displayed by react-hook-form. To integrate with yup we need to install the following dependencies
## Code snippet 6
npm i yup @hookform/resolvers
Then, we can create a yup schema object and integrate it with the yup form object. For each form field, we can specify the validations along with the error message to be displayed in case validation fails.
// Code snippet 7
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
const schema = yup.object({
serialno: yup.string().required("Serial # is required"),
title: yup.string().required("Title is required"),
author: yup.string().required("Author is required"),
price: yup.number()
.typeError("Please enter a number")
.positive("Price cannot be negative")
.required("Price is required")
.test(
"decimal-places",
"Only up to 2 decimal places allowed",
(value) => value == null || /^\d+(\.\d{1,2})?$/.test((value || "").toString())
),
dop: yup.date()
.typeError("Please select a date")
.required("Date of Purchase is required")
.max(new Date(),"Date cannot be greater than today")
})
export const AddBookForm = () => {
const form = useForm ({resolver:yupResolver(schema)});
. . .
Validations for the first three fields are simple, they need a string value and must not be empty. For Price, we specify that it is a positive number and only up to 2 decimal places is allowed. Date of Purchase must be a date not greater than today.
The error messages are displayed with help of errors object.
// Code snippet 8
const { register, formState } = form;
const {errors} = formState;
<div className="wrapper">
<label htmlFor="serialno"> Serial # </label>
<input type="text" id="serialno" {...register("serialno")} />
</div>
<p className='error'> {errors.serialno?.message}</p>
<div className="wrapper">
<label htmlFor="title"> Title </label>
<input type="text" id="title" {...register("title")} />
</div>
<p className='error'> {errors.title?.message}</p>
. . .
Error messages will appear as
By default, validation occurs when the form is submitted, i.e. the error messages will be displayed when user clicks on the Submit button. Validation mode can be changed using the mode key in the useForm hook object parameter.
// Code snippet 9
const form = useForm ({mode:"onSubmit"})
As mentioned, by default the mode is “onSubmit,” it can be changed to the following values:
onBlur
– validation is triggered when user leaves a form field.onTouched
- validation is triggered when user first leaves a form field and while making changes to the input.OnChange
- validation is triggered on every change to the input.all
– this is a combination of “onBlur” and “onChange” modes.
Based on the application requirements we can set the mode to any of the above.
5. Submitting a form
Normally form submission is handled by a submitHandler function which will calls an API and will contain both the success and error paths. With react-hook-forms, there is a small difference. The submitHandler function will handle only success path and a separate errorHandler function will handle the error path. Both these handler functions are provided as input to the form's handleSubmit method. Form data is available as data object to the submitHandler function. Similarly, errors object is available to the errorHandler function. In our example, handler functions will just print the data and the errors object. In actual application, submitHandler will call the post API to create a new book record in the database. Within the errorHandler, further processing can be done or the error message can be displayed in application or logs.
// Code snippet 10
const form = useForm ();
const { register, handleSubmit } = form;
const submitHandler = (data) => {
console.log("Data which will be submitted is", data);
}
const errorHandler = (errors) => {
console.log("Error in form submission", errors);
}
. . .
<form onSubmit={handleSubmit(submitHandler, errorHandler)}>
. . .
If form is successfully submitted, we get the following output.
However, if the code fails, errors object will be displayed as
Sometimes, we may want to disable form submission until all fields are filled. We can do so with the help of isDirty and isValid flags. They are available from the formState object and can be used within the Submit button in following manner.
// Code snippet 11
const {errors, isValid, isDirty} = formState;
. . .
<button disabled = {!isValid || !isDirty}> Submit </button>
We can use reset method and isSubmitSuccessful flag to reset all form values after Submit button is clicked. Once Submit button is clicked, isSubmitSuccessful has value true if form submission is successful.
// Code snippet 11
const { register, handleSubmit, formState, reset} = form;
const {errors, isValid, isDirty, isSubmitSuccessful} = formState;
useEffect(() => {
if(isSubmitSuccessful) {
reset();
}
}, [isSubmitSuccessful])}
Conclusion
In this article we have seen how to create and manage form states using react-hook-form. We have created a yup schema object and used it for validation. We have gone through the various validation modes and finally form submission and error handling. Using such a library greatly simplifies and standardizes the form creation process. A schema object helps define all validation rules in one place along with the error messages. Validation code is thus localized and is not spread throughout the entire form, within various events of each field. The primary benefit however is performance, as the form does not re-render every time user input changes. This is because React Hook form follows uncontrolled form behavior.
Hope you have found this article useful and will consider using react hook forms for your applications. Thanks for reading.