Đây là phiên bản "Tối thượng" cải tiến từ bản trước. Chúng ta sẽ giải quyết 2 vấn đề:
- DX (Trải nghiệm code): Phải truyền
control={control}vào mọi input là quá thừa thãi. - Performance: Tối ưu hóa render.
Giải pháp: Sử dụng FormProvider (Context). Input tự động kết nối với Form mà không cần props.
1. Base Wrapper (Memoized) -> FieldWrapper
Chúng ta dùng memo để đảm bảo Wrapper không re-render oan uổng nếu props không đổi.
import { ReactNode, memo } from "react";
import { FieldError } from "react-hook-form";
// Tách riêng Error Message để memo hóa
const ErrorMessage = memo(({ error }: { error?: FieldError }) => {
if (!error) return null;
return (
<span role="alert" className="text-xs text-red-500 font-medium animate-pulse">
{error.message}
</span>
);
});
interface FieldWrapperProps {
label?: string;
error?: FieldError;
children: ReactNode;
className?: string;
required?: boolean;
helperText?: string;
}
export function FieldWrapper({
label, error, children, className, required, helperText
}: FieldWrapperProps) {
return (
<div className={`flex flex-col gap-1.5 ${className}`}>
{label && (
<label className="text-sm font-medium text-gray-700 dark:text-gray-200 flex items-center gap-1">
{label}
{required && <span className="text-red-500">*</span>}
</label>
)}
{children}
{helperText && !error && (
<span className="text-xs text-gray-500">{helperText}</span>
)}
<ErrorMessage error={error} />
</div>
);
}2. Input Field (Context Aware)
Chúng ta dùng useFormContext thay vì bắt user truyền control từ ngoài vào.
import { useFormContext, useController } from "react-hook-form";
interface InputProps {
name: string; // Không cần Generic FieldValues nữa vì Context lo rồi
label?: string;
type?: "text" | "email" | "number" | "password";
placeholder?: string;
required?: boolean;
helperText?: string;
}
export function InputField({
name, label, type = "text", placeholder, required, helperText
}: InputProps) {
// 🚀 Lấy control từ Context tự động
const { control } = useFormContext();
const { field, fieldState: { error } } = useController({ name, control });
return (
<FieldWrapper label={label} error={error} required={required} helperText={helperText}>
<input
{...field}
id={name}
type={type}
placeholder={placeholder}
value={field.value ?? ""} // Fix lỗi uncontrolled component cảnh báo
className={`...styles...`}
/>
</FieldWrapper>
);
}3. Cách dùng mới: Chuẩn "Clean Code"
Bây giờ code form của bạn sạch đến mức khó tin:
- Không cần
control={control}. - Không cần Generic
<FormSchema>.
import { FormProvider, useForm } from "react-hook-form";
export function EmployeeForm() {
// methods chứa control, handleSubmit, register...
const methods = useForm<EmployeeForm>({
resolver: zodResolver(EmployeeSchema),
});
const onSubmit = (data) => console.log(data);
return (
// Bọc toàn bộ form bằng FormProvider
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} className="space-y-6">
{/* ✅ Siêu gọn: Chỉ cần name */}
<InputField name="fullname" label="Họ Tên" required />
<InputField name="email" label="Email" required />
<SelectField
name="role"
label="Chức vụ"
options={roleOptions}
/>
<button>Submit</button>
</form>
</FormProvider>
);
}4. Tại sao cách này Tối ưu nhất?
- Zero Prop Drilling: Không phải truyền
controlqua 10 tầng component nếu form bạn chia nhỏ thànhSectionA,SectionB. - Loose Coupling:
InputFieldcó thể vứt ở bất cứ đâu trong cây DOM, miễn là nằm trongFormProvider. - Maintainability: InputField giờ đây độc lập hoàn toàn.
Lưu ý quan trọng về Performance
Khi dùng useFormContext, nếu bạn dùng watch() ở root component -> Vẫn bị re-render toàn bộ.
Hãy tuân thủ quy tắc Isolation: Chỉ dùng useWatch bên trong các component nhỏ cần reactive data.
Các component Input ở trên (InputField, SelectField) sử dụng useController -> Chúng chỉ tự re-render chính nó khi bản thân field của nó thay đổi. Chúng KHÔNG làm re-render các anh em hàng xóm. Đây là kiến trúc tối ưu.