Efficiently Handling Form State in React with Immer and React-Hook-Form
Streamline form management by leveraging Immer and React-Hook-Form
Jul 23, 2024 - 18:14 • 5 min read
Managing form state is a critical aspect of web development, and when building complex React applications, it can become quite challenging. In this blog post, we'll explore two powerful tools, Immer and React-Hook-Form, to enhance form state management. By the end of this post, you'll have a solid understanding of how to use these libraries together to create more manageable and efficient forms.
Why Immer and React-Hook-Form?
Immer simplifies the process of immutability in JavaScript. It's particularly useful for managing state in a React application because it allows you to work with immutable data structures in a more intuitive way.
With Immer, you can write code that looks and feels natural, while behind the scenes, Immer ensures that your state remains immutable.
React-Hook-Form (RHF) is a powerful library for managing form state in React. It provides a simple API for integrating forms with minimal re-renders, reducing performance bottlenecks. RHF also supports validations, and it's highly customizable, making it an excellent choice for handling forms in complex applications.
Setting Up the Project
First, let's initialize our project and install the necessary dependencies:
npx create-react-app form-management
cd form-management
npm install immer react-hook-form
Creating a Form Component
Start by creating a Form.js
file in the src
directory. We'll use RHF to handle the form state and Immer to manage an immutable copy of our form data.
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import produce from 'immer';
const Form = () => {
const { control, handleSubmit, setValue } = useForm();
const onSubmit = data => {
console.log(data);
};
const handleInputChange = (field, value) => {
setValue(field, value);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="firstName"
control={control}
render={({ field }) => (
<input
{...field}
onChange={(e) => handleInputChange("firstName", e.target.value)}
/>
)}
/>
<Controller
name="lastName"
control={control}
render={({ field }) => (
<input
{...field}
onChange={(e) => handleInputChange("lastName", e.target.value)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
};
export default Form;
Using Immer to Update Form State
In this section, we'll integrate Immer into our form handling logic to ensure that our state management remains immutable.
import React, { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import produce from 'immer';
const Form = () => {
const { control, handleSubmit } = useForm();
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
});
const onSubmit = data => {
console.log(data);
};
const handleInputChange = (field, value) => {
setFormData(produce((draft) => {
draft[field] = value;
}));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="firstName"
control={control}
render={({ field }) => (
<input
{...field}
value={formData.firstName}
onChange={(e) => handleInputChange("firstName", e.target.value)}
/>
)}
/>
<Controller
name="lastName"
control={control}
render={({ field }) => (
<input
{...field}
value={formData.lastName}
onChange={(e) => handleInputChange("lastName", e.target.value)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
};
export default Form;
Adding Validation Rules
One of the strengths of React-Hook-Form is its support for validation. Let's add some basic validation rules for our form fields.
import React, { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import produce from 'immer';
const Form = () => {
const { control, handleSubmit, formState: { errors } } = useForm();
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
});
const onSubmit = data => {
console.log(data);
};
const handleInputChange = (field, value) => {
setFormData(produce((draft) => {
draft[field] = value;
}));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="firstName"
control={control}
rules={{ required: "First name is required" }}
render={({ field }) => (
<div>
<input
{...field}
value={formData.firstName}
onChange={(e) => handleInputChange("firstName", e.target.value)}
/>
{errors.firstName && <span>{errors.firstName.message}</span>}
</div>
)}
/>
<Controller
name="lastName"
control={control}
rules={{ required: "Last name is required" }}
render={({ field }) => (
<div>
<input
{...field}
value={formData.lastName}
onChange={(e) => handleInputChange("lastName", e.target.value)}
/>
{errors.lastName && <span>{errors.lastName.message}</span>}
</div>
)}
/>
<button type="submit">Submit</button>
</form>
);
};
export default Form;
Now our form has basic validation for the first name and last name fields. If the user submits the form without filling in these fields, an error message will be displayed.
Handling Nested Form Data
In real-world applications, form data often includes nested objects. Let's see how to handle nested fields with Immer and React-Hook-Form.
import React, { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import produce from 'immer';
const Form = () => {
const { control, handleSubmit, formState: { errors } } = useForm();
const [formData, setFormData] = useState({
user: {
firstName: '',
lastName: '',
}
});
const onSubmit = data => {
console.log(data);
};
const handleInputChange = (field, value) => {
setFormData(produce((draft) => {
draft.user[field] = value;
}));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="user.firstName"
control={control}
rules={{ required: "First name is required" }}
render={({ field }) => (
<div>
<input
{...field}
value={formData.user.firstName}
onChange={(e) => handleInputChange("firstName", e.target.value)}
/>
{errors.user?.firstName && <span>{errors.user.firstName.message}</span>}
</div>
)}
/>
<Controller
name="user.lastName"
control={control}
rules={{ required: "Last name is required" }}
render={({ field }) => (
<div>
<input
{...field}
value={formData.user.lastName}
onChange={(e) => handleInputChange("lastName", e.target.value)}
/>
{errors.user?.lastName && <span>{errors.user.lastName.message}</span>}
</div>
)}
/>
<button type="submit">Submit</button>
</form>
);
};
export default Form;
In this example, our form data includes a user
object with firstName
and lastName
fields. By using dotted paths in the name
attribute of the Controller
component, React-Hook-Form can map these fields to the nested structure correctly.
Conclusion
By integrating Immer and React-Hook-Form, we can create a more intuitive and efficient way to manage form state in React applications. Immer helps us handle immutable state updates seamlessly, while React-Hook-Form simplifies form management and validation with minimal re-renders. Try incorporating these libraries into your next project and experience the power of streamlined form handling.