Crafting Dynamic Forms with React Hook Form and Yup
Elevate your form handling in React with React Hook Form and Yup for robust validation
Jul 04, 2024 - 00:19 • 5 min read
Forms are the cornerstone of user interactivity on the web, but handling them effectively can be a challenging task, especially when considering validation, state management, and performance.
React Hook Form is a powerful library that simplifies form handling in React applications by leveraging hooks and the Context API. In conjunction with Yup for validation, it can significantly streamline the process of building and maintaining dynamic forms.
Getting Started
First things first, let's set up our project. Ensure you have Node.js installed, and create a new React project:
npx create-react-app dynamic-forms
cd dynamic-forms
Install React Hook Form and Yup:
npm install react-hook-form yup @hookform/resolvers
Setting Up Your Form
Let's start by creating a basic form utilizing React Hook Form. Create a Form.js
file in your src
directory:
import React from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as Yup from "yup";
const schema = Yup.object().shape({
name: Yup.string().required("Name is required"),
email: Yup.string().email("Invalid email format").required("Email is required"),
age: Yup.number().positive("Age must be positive").integer("Age must be an integer"),
});
function Form() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(schema),
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Name:</label>
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label>Email:</label>
<input {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label>Age:</label>
<input {...register("age")} />
{errors.age && <p>{errors.age.message}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
}
export default Form;
In this example, we create a form that collects a user's name, email, and age. We use Yup to define our validation schema, which React Hook Form integrates seamlessly into using its yupResolver
function.
Conditional Fields and Dynamic Validation
One of the strengths of React Hook Form is its flexibility to manage complex forms, including conditional fields. Suppose we want to show a 'Phone Number' field only if the user chooses to provide it. Additionally, we'll validate this field only if it's filled.
Let's modify our form to include a checkbox that toggles the 'Phone Number' field:
import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as Yup from "yup";
const schema = Yup.object().shape({
name: Yup.string().required("Name is required"),
email: Yup.string().email("Invalid email format").required("Email is required"),
age: Yup.number().positive("Age must be positive").integer("Age must be an integer"),
phone: Yup.string().when('providePhone', {
is: true,
then: Yup.string().required("Phone number is required").matches(/^\d{10}$/, "Phone number must be 10 digits")
})
});
function Form() {
const [showPhone, setShowPhone] = useState(false);
const { register, handleSubmit, control, formState: { errors } } = useForm({
resolver: yupResolver(schema),
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Name:</label>
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label>Email:</label>
<input {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label>Age:</label>
<input {...register("age")} />
{errors.age && <p>{errors.age.message}</p>}
</div>
<div>
<label>
<input type="checkbox" onChange={() => setShowPhone(!showPhone)} /> Provide phone number
</label>
</div>
{showPhone && (
<div>
<label>Phone:</label>
<input {...register("phone")} />
{errors.phone && <p>{errors.phone.message}</p>}
</div>
)}
<button type="submit">Submit</button>
</form>
);
}
export default Form;
Here, we used the when
function from Yup to conditionally apply validation rules based on the providePhone
state. The Controller
component from React Hook Form can help integrate more complex state or components into your forms.
Advanced Usage: Nested Forms
Another common requirement is nested forms or repeating fields. React Hook Form handles this elegantly using array fields. Let’s create a form to collect multiple addresses:
import React from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as Yup from "yup";
const schema = Yup.object().shape({
addresses: Yup.array().of(
Yup.object().shape({
street: Yup.string().required("Street is required"),
city: Yup.string().required("City is required"),
})
).required("At least one address is required"),
});
function AddressForm() {
const { register, control, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(schema),
defaultValues: { addresses: [{ street: "", city: "" }] },
});
const { fields, append, remove } = useFieldArray({
control,
name: "addresses",
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((item, index) => (
<div key={index}>
<label>Street:</label>
<input {...register(`addresses.${index}.street`)} />
{errors.addresses && errors.addresses[index]?.street && <p>{errors.addresses[index].street.message}</p>}
<label>City:</label>
<input {...register(`addresses.${index}.city`)} />
{errors.addresses && errors.addresses[index]?.city && <p>{errors.addresses[index}.city.message}</p>}
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}
<button type="button" onClick={() => append({ street: "", city: "" })}>Add Address</button>
<button type="submit">Submit</button>
</form>
);
}
export default AddressForm;
In this example, the useFieldArray
hook is used to dynamically manage an array of address fields. Each address is validated with Yup schemas, ensuring proper data integrity.
Performance Considerations
React Hook Form is optimized for performance, as it minimizes re-renders by isolating field updates and leveraging uncontrolled component principles. Here are a few tips to ensure optimal performance:
- Use defaultValues: Define default values for fields using the
defaultValues
property in theuseForm
hook. - Isolate Re-Renders: Use
Controller
only when necessary, which lets you synchronize a controlled input. - Avoid Unnecessary State: Keep form state minimal and within the
useForm
hook context to avoid unnecessary re-renders.
For example, using Controller
can help manage more complex form components without unnecessary re-renders:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField } from "@material-ui/core";
function Form() {
const { control, handleSubmit } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="username"
control={control}
defaultValue=""
render={({ field }) => <TextField {...field} label="Username" />}
/>
<button type="submit">Submit</button>
</form>
);
}
export default Form;
Conclusion
React Hook Form combined with Yup offers a powerful and flexible solution for handling forms in React. Whether dealing with simple forms, conditional fields, or complex nested structures, these libraries together provide scalability and maintainability to your form logic.
Leveraging these tools can enhance the user experience by providing immediate validation feedback and robust error handling while maintaining optimal performance. Give them a try in your next React project to create dynamic, user-friendly forms effortlessly.