Những Gì Mình Ước Biết Sớm Hơn Về React Hook Form

Đặt vấn đề
Khi xử lý form "thuần" trong React, bạn phải tự quản lý state cho từng field, tự validate, tự hiển thị lỗi... Với form 3 trường đã lằng nhằng, 10–15 trường thì codebase rất khó bảo trì.
React Hook Form (RHF) giúp bạn làm tất cả điều đó với ít code hơn, và hiệu năng tốt hơn nhờ dùng ref thay vì state — tức là form không re-render liên tục khi user gõ.
npm install react-hook-form
Cách dùng cơ bản
import { useForm } from 'react-hook-form';
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} placeholder="Email" />
<input {...register('password')} type="password" placeholder="Mật khẩu" />
<button type="submit">Đăng nhập</button>
</form>
);
}
3 thứ quan trọng nhất cần nhớ:
| Mô tả | |
|---|---|
register |
Đăng ký input với RHF — luôn dùng spread {...register('tên')} |
handleSubmit |
Bọc hàm submit, tự chặn reload trang và chạy validation |
errors |
Object chứa lỗi của từng field sau khi validate |
Case 1: Validation cơ bản
Truyền rules trực tiếp vào register:
function RegisterForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input
{...register('email', {
required: 'Vui lòng nhập email',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Email không hợp lệ',
},
})}
placeholder="Email"
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
<input
type="password"
{...register('password', {
required: 'Vui lòng nhập mật khẩu',
minLength: { value: 8, message: 'Tối thiểu 8 ký tự' },
})}
placeholder="Mật khẩu"
/>
{errors.password && <p style={{ color: 'red' }}>{errors.password.message}</p>}
<button type="submit">Đăng ký</button>
</form>
);
}
Các rules thường dùng: required, minLength, maxLength, pattern, min, max.
Case 2: Xác nhận mật khẩu (Confirm Password)
Dùng watch để đọc giá trị của field khác, rồi so sánh trong validate:
function RegisterForm() {
const { register, handleSubmit, watch, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input
type="password"
{...register('password', { required: 'Bắt buộc nhập' })}
placeholder="Mật khẩu"
/>
<input
type="password"
{...register('confirmPassword', {
required: 'Bắt buộc nhập',
validate: (value) =>
value === watch('password') || 'Mật khẩu xác nhận không khớp',
})}
placeholder="Xác nhận mật khẩu"
/>
{errors.confirmPassword && (
<p style={{ color: 'red' }}>{errors.confirmPassword.message}</p>
)}
<button type="submit">Xác nhận</button>
</form>
);
}
validatechấp nhận một hàm — trả vềtruenếu hợp lệ, hoặc trả về string thông báo lỗi nếu không hợp lệ.
Case 3: Edit form — điền sẵn dữ liệu từ API
Dùng reset để đổ data vào form sau khi fetch về:
function EditUserForm({ userId }) {
const { register, handleSubmit, reset } = useForm();
useEffect(() => {
fetchUser(userId).then((user) => {
reset({
fullName: user.fullName,
email: user.email,
});
});
}, [userId]);
return (
<form onSubmit={handleSubmit((data) => updateUser(userId, data))}>
<input {...register('fullName')} placeholder="Họ tên" />
<input {...register('email')} placeholder="Email" />
<button type="submit">Lưu</button>
</form>
);
}
reset()không truyền tham số sẽ clear toàn bộ form — tiện dùng sau khi submit thành công.
Case 4: Disable nút submit khi đang gửi
Dùng isSubmitting từ formState — RHF tự set true khi hàm onSubmit của bạn đang chạy (kể cả async):
function ContactForm() {
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm();
const onSubmit = async (data) => {
await sendEmail(data); // API call
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('message', { required: true })} placeholder="Tin nhắn" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Đang gửi...' : 'Gửi'}
</button>
</form>
);
}
Case 5: Dùng với UI Library (MUI, React Select...)
Các component không phải input HTML gốc (như <Select> của MUI hay React Select) không nhận ref theo cách thông thường, nên không dùng register được. Giải pháp là dùng Controller:
import { useForm, Controller } from 'react-hook-form';
import Select from 'react-select';
const roleOptions = [
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' },
];
function UserForm() {
const { control, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="role"
control={control}
rules={{ required: 'Vui lòng chọn vai trò' }}
render={({ field }) => (
<Select {...field} options={roleOptions} placeholder="Chọn vai trò" />
)}
/>
{errors.role && <p style={{ color: 'red' }}>{errors.role.message}</p>}
<button type="submit">Lưu</button>
</form>
);
}
Controller đóng vai trò "cầu nối" — prop render nhận field object gồm value, onChange, onBlur, ref, spread thẳng vào component là xong.
Bonus: Tích hợp Yup cho form phức tạp
Khi form có nhiều trường và nhiều rules, tách logic validate ra Yup schema sẽ gọn và dễ maintain hơn nhiều:
npm install @hookform/resolvers yup
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
const schema = yup.object({
email: yup.string().required('Bắt buộc').email('Email không hợp lệ'),
password: yup.string().required('Bắt buộc').min(8, 'Tối thiểu 8 ký tự'),
confirmPassword: yup
.string()
.oneOf([yup.ref('password')], 'Mật khẩu không khớp')
.required('Bắt buộc'),
});
function RegisterForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(schema), // chỉ cần thêm dòng này
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register('email')} placeholder="Email" />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" {...register('password')} placeholder="Mật khẩu" />
{errors.password && <p>{errors.password.message}</p>}
<input type="password" {...register('confirmPassword')} placeholder="Xác nhận" />
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
<button type="submit">Đăng ký</button>
</form>
);
}
Ngoài Yup, bạn cũng có thể dùng Zod (zodResolver) nếu dự án đang dùng TypeScript — cú pháp tương tự, type-safe hơn.
Tổng kết
| Case | Công cụ |
|---|---|
| Form cơ bản | register + handleSubmit + errors |
| Validation | Rules trong register(name, { ... }) |
| Confirm password | validate + watch |
| Edit form / Clear form | reset(data) |
| Loading / disable button | isSubmitting từ formState |
| UI library (MUI, Select...) | Controller |
| Form phức tạp | Yup/Zod + resolver |
React Hook Form giải quyết được hầu hết bài toán form thường gặp mà không cần nhiều code. Nếu bạn chưa dùng, đây là lúc thử!
All rights reserved