Mục lục

Tại sao Form của bạn chậm? React Hook Form là cứu tinh

Phân tích sâu về hiệu năng Form. Tại sao Controlled Component lại là thảm họa với Form lớn? Kỹ thuật cô lập Re-render (Render Isolation) với React Hook Form.

Bạn có một form "khủng" với 50 ô input (ví dụ: Form khai báo thuế, Form nhập kho). Khi user gõ 1 ký tự vào ô "Tên", cả 49 ô còn lại... nháy sáng trong React DevTools Profiler.

Đây là thảm họa hiệu năng phổ biến nhất trong React.

1. Controlled Component: Kẻ giết chết Performance

Cách viết React truyền thống (Controlled):

tsx:
// ❌ BAD: The Performance Killer
export function SlowForm() {
  const [values, setValues] = useState({ name: '', email: '', note: '' });

  // Mỗi lần gõ 1 phím -> Gọi setValues -> Trigger Re-render toàn bộ Component
  // Nếu Component này chứa 50 input con -> Cả 50 input con đều bị Re-render
  const handleChange = (e) => {
    setValues(prev => ({ ...prev, [e.target.name]: e.target.value }));
  };

  return (
    <form>
      <input name="name" value={values.name} onChange={handleChange} />
      <HeavyComponent /> {/* Bị re-render oan uổng */}
    </form>
  );
}

Dẫn chứng:

  • User gõ "Nguyen Van A" (12 ký tự).
  • Component SlowForm render lại 12 lần.
  • Nếu HeavyComponent mất 50ms để render -> Total blocking time = 12 * 50ms = 600ms. -> Lag lòi mắt.

2. React Hook Form (Uncontrolled): Cứu tinh

Triết lý của RHF: "Hãy để DOM giữ giá trị (Uncontrolled), React chỉ đứng nhìn (Subscribe)".

RHF sử dụng ref để đăng ký trực tiếp với DOM Element. Khi user gõ phím -> Browser update DOM native (siêu nhanh) -> React KHÔNG HỀ BIẾT (Không Re-render).

tsx:
// ✅ GOOD: Zero Re-render
export function FastForm() {
  const { register, handleSubmit } = useForm();
  
  // Component này chỉ render ĐÚNG 1 LẦN khi mount.
  // Gõ phím thoải mái, component vẫn đứng im.
  console.log("Render Count"); 

  const onSubmit = (data) => console.log(data); // Chỉ lấy data khi Submit

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} /> {/* Đăng ký Ref ngầm */}
      <HeavyComponent /> {/* An toàn tuyệt đối */}
      <button>Submit</button>
    </form>
  );
}

3. Bài học xương máu: watch sai cách

Rất nhiều bạn dùng RHF nhưng vẫn làm app chậm. Lý do: Dùng watch bừa bãi.

Sai lầm: Watch ngay tại Root Component.

tsx:
// ❌ SAI LẦM: Làm mất hết tác dụng của RHF
function WrongUsage() {
  const { register, watch } = useForm();
  
  // Lắng nghe thay đổi của name để hiện thông báo
  // -> Mỗi lần name đổi -> Root Component re-render -> Lại lôi theo cả form re-render
  const name = watch("name"); 

  return (
    <form>
      <input {...register("name")} />
      <p>Hello {name}</p>
      <HeavyComponent /> {/* Vẫn bị re-render! */}
    </form>
  );
}

Tối ưu nhất: Cô lập Re-render (Component Isolation). Hãy đẩy phần cần re-render vào một component con riêng biệt.

tsx:
// ✅ TỐI ƯU: Chỉ component bé tí này re-render
function NamePreview({ control }) {
  const name = useWatch({ control, name: "name" }); // Hook chuyên biệt
  return <p>Hello {name}</p>;
}

function OptimalForm() {
  const { register, control } = useForm();
  
  return (
    <form>
      <input {...register("name")} />
      
      {/* Component này re-render liên tục (nhẹ) */}
      <NamePreview control={control} /> 
      
      {/* Component này KHÔNG BAO GIỜ re-render (nặng) */}
      <HeavyComponent /> 
    </form>
  );
}

4. Proxy State Mode (formState)

Một cái bẫy khác: Validation Mode. RHF có cơ chế Proxy thông minh. Nếu bạn không đọc errors, RHF sẽ không re-render khi có lỗi validation.

tsx:
const { formState: { errors } } = useForm();
// Chỉ khi bạn destructure { errors } ra, RHF mới bắt đầu tracking validation state.
// Nếu bạn xóa chữ `errors` đi, form sẽ chạy nhanh hơn nữa!

Kết luận

Để đạt hiệu năng tối đa với Form:

  1. Ưu tiên Uncontrolled: Dùng register cho input thuần. Controller cho UI Lib.
  2. Tránh watch ở Root: Dùng useWatch trong component con để cô lập vùng re-render.
  3. Proxy State: Chỉ lấy những gì bạn cần ở formState (isValid, isDirty, errors).
Quảng cáo
mdhorizontal