Mục lục

Full Source Code: Form UI Kit (Context API & Performance Optimized)

Phiên bản tối ưu nhất của Form UI Kit. Sử dụng FormProvider (Context) để loại bỏ Prop Drilling. Tích hợp React.memo để tối đa hóa hiệu năng render.

Đâ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 đề:

  1. DX (Trải nghiệm code): Phải truyền control={control} vào mọi input là quá thừa thãi.
  2. 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.

tsx:
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.

tsx:
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:

  1. Không cần control={control}.
  2. Không cần Generic <FormSchema>.
tsx:
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?

  1. Zero Prop Drilling: Không phải truyền control qua 10 tầng component nếu form bạn chia nhỏ thành SectionA, SectionB.
  2. Loose Coupling: InputField có thể vứt ở bất cứ đâu trong cây DOM, miễn là nằm trong FormProvider.
  3. 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.

Quảng cáo
mdhorizontal