+1

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

image.png

Đặ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>
  );
}

validate chấp nhận một hàm — trả về true nế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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí