Mô hình chuẩn là một mảng object trong form state, mỗi dòng là một phần tử; validate bằng schema mảng để lỗi bám đúng chỉ số dòng.
jsx
const { fields, append, remove } = useFieldArray({ control, name: 'items' })
fields.map((field, i) => (
<div key={field.id}>
<input {...register(`items.${i}.name`, { required: 'Bắt buộc' })} />
<input type="number" {...register(`items.${i}.qty`, { min: 1, valueAsNumber: true })} />
<button type="button" onClick={() => remove(i)}>Xoá</button>
</div>
))Lỗi kinh điển là dùng key={i}. Khi xoá dòng giữa, React coi các dòng sau là "cùng key, đổi nội dung" nên giữ lại DOM node cũ — giá trị trong input, focus và trạng thái lỗi bị lệch sang dòng khác. Phải dùng id ổn định của từng dòng (field.id, hoặc crypto.randomUUID() sinh lúc append).
Các điểm cần nói thêm:
- Validate cấp mảng chứ không chỉ từng field: tối thiểu 1 dòng, không trùng sản phẩm, tổng số lượng không vượt tồn kho.
- Hiệu năng: mỗi dòng nên là một component riêng dùng uncontrolled input (như react-hook-form) hoặc
memo, tránh render lại toàn bộ 50 dòng mỗi lần gõ một ký tự. - Giá trị dẫn xuất (thành tiền, tổng đơn) tính lúc render từ giá trị hiện tại, đừng lưu thành state riêng rồi phải đồng bộ tay.
- Nút xoá đặt
type="button"; để mặc địnhsubmitsẽ gửi form khi bấm.