Ở các công ty lớn (Meta, Airbnb), Developer không viết thủ công từng dòng input, id, htmlFor, errors.name?.message lặp đi lặp lại.
Họ xây dựng một hệ thống Abstraction Layer.
Mục tiêu:
- Write Less: Viết ít code nhất có thể.
- Type Safety: Sai tên field -> Code đỏ lòm ngay lập tức.
- Consistency: UI lỗi, Label, Helper text luôn đồng bộ.
1. The Wrapper Pattern (Component hóa)
Thay vì viết:
// ❌ Junior Way (Lặp lại logic xử lý lỗi ở khắp nơi)
<div>
<label>Email</label>
<input {...register('email')} className={errors.email ? 'border-red-500' : ''} />
{errors.email && <span className="text-red">{errors.email.message}</span>}
</div>Chúng ta xây dựng <InputField />:
// ✅ Senior Way
<InputField
control={control}
name="email"
label="Email Address"
/>2. Xây dựng <InputField> chuẩn TypeScript
Đây là phần "Hardcore" nhất: Làm sao để TypeScript gợi ý tên field trong chuỗi string name="..."?
import { Control, FieldValues, Path, useController } from "react-hook-form";
// Generic Type T: Schema của Form
interface InputFieldProps<T extends FieldValues> {
control: Control<T>;
name: Path<T>; // 🔥 Magic: Chỉ cho phép nhập key có trong Schema
label: string;
}
export function InputField<T extends FieldValues>({
control, name, label
}: InputFieldProps<T>) {
// useController: Hook mạnh mẽ để tạo Reusable Component
const {
field,
fieldState: { error }
} = useController({ name, control });
return (
<div className="flex flex-col gap-1">
<label htmlFor={name} className="text-sm font-medium">
{label}
</label>
<input
id={name}
{...field} // Tự động bind onChange, onBlur, value, ref
className={`border p-2 rounded ${error ? "border-red-500" : ""}`}
/>
{/* Tự động hiện lỗi nếu có */}
{error && <span className="text-red-500 text-xs">{error.message}</span>}
</div>
);
}3. The Form Provider Pattern (Context)
Nếu Form quá lớn và nhiều tầng (Nested Components), việc truyền control xuống từng component rất mệt (Prop Drilling).
RHF cung cấp FormProvider để dùng Context.
import { useForm, FormProvider, useFormContext } from "react-hook-form";
// 1. Tạo Context Wrapper
function SmartForm({ children, onSubmit, schema }) {
const methods = useForm({ resolver: zodResolver(schema) });
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{children}
</form>
</FormProvider>
);
}
// 2. Component Con tự lấy control không cần props
function SmartInput({ name, label }) {
const { control } = useFormContext(); // 🚀 Lấy từ Context
// ... Logic giống InputField trên
}
// ✅ Usage: Siêu sạch
<SmartForm onSubmit={...} schema={Schema}>
<SmartInput name="email" label="Email" />
<SmartInput name="password" label="Password" />
<Section>
<SmartInput name="address.city" label="City" /> {/* Nested vẫn nhận context */}
</Section>
<SubmitButton />
</SmartForm>4. Factory Pattern (Generators)
Một số team tiến xa hơn: Generate Form từ Config JSON. (Dùng cho CMS, Admin Dashboard cần dựng form nhanh).
const formConfig = [
{ type: 'text', name: 'firstName', label: 'First Name' },
{ type: 'select', name: 'role', options: ['Admin', 'User'] },
];
function FormGenerator({ config }) {
return config.map(field => {
if (field.type === 'text') return <InputField ... />;
if (field.type === 'select') return <SelectField ... />;
});
}Kết luận
"Senior" không có nghĩa là viết code phức tạp. "Senior" là viết code trừu tượng hóa (Abstract) những thứ phức tạp để Junior có thể dùng dễ dàng và không thể mắc lỗi.
Bằng cách tạo ra bộ Form UI Kit (InputField, SelectField, DatePickerField...), bạn đảm bảo:
- DX (Dev Experience) tuyệt vời.
- UX đồng nhất trên toàn hệ thống.
- Validation không bao giờ bị quên.