Динамические поля
Иногда форма меняется: добавляются поля, удаляются, клонируются. Такие формы называются динамическими.
Простой пример: список
function TagsForm() {
const [tags, setTags] = useState<string[]>(['']);
function update(index: number, value: string) {
setTags((t) => t.map((tag, i) => (i === index ? value : tag)));
}
function add() {
setTags((t) => [...t, '']);
}
function remove(index: number) {
setTags((t) => t.filter((_, i) => i !== index));
}
return (
<form>
{tags.map((tag, i) => (
<div key={i}>
<input value={tag} onChange={(e) => update(i, e.target.value)} />
<button type="button" onClick={() => remove(i)}>Удалить</button>
</div>
))}
<button type="button" onClick={add}>Добавить</button>
</form>
);
}
Важно: если поля можно удалять из середины — не используй индекс как key. Используй уникальный ID.
С уникальным ID
let nextId = 1;
type Tag = { id: number; value: string };
const [tags, setTags] = useState<Tag[]>([{ id: 0, value: '' }]);
function add() {
setTags((t) => [...t, { id: nextId++, value: '' }]);
}
function update(id: number, value: string) {
setTags((t) => t.map((tag) => (tag.id === id ? { ...tag, value } : tag)));
}
function remove(id: number) {
setTags((t) => t.filter((tag) => tag.id !== id));
}
{tags.map((tag) => (
<div key={tag.id}>
<input value={tag.value} onChange={(e) => update(tag.id, e.target.value)} />
<button type="button" onClick={() => remove(tag.id)}>×</button>
</div>
))}
С объектами
type Phone = { id: number; number: string; type: string };
const [phones, setPhones] = useState<Phone[]>([
{ id: 1, number: '', type: 'mobile' },
]);
function update(id: number, patch: Partial<Phone>) {
setPhones((p) => p.map((phone) => (phone.id === id ? { ...phone, ...patch } : phone)));
}
useFieldArray (React Hook Form)
Для динамических полей в React Hook Form есть useFieldArray:
import { useForm, useFieldArray } from 'react-hook-form';
type FormValues = {
tags: { value: string }[];
};
function Form() {
const { register, control, handleSubmit } = useForm<FormValues>({
defaultValues: { tags: [{ value: '' }] },
});
const { fields, append, remove } = useFieldArray({
control,
name: 'tags',
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`tags.${index}.value`)} />
<button type="button" onClick={() => remove(index)}>Удалить</button>
</div>
))}
<button type="button" onClick={() => append({ value: '' })}>Добавить</button>
<button>Сохранить</button>
</form>
);
}
fields — массив с уникальным id. Используй его как key.
Вложенные поля
const { fields, append, remove } = useFieldArray({
control,
name: 'contacts',
});
{fields.map((contact, i) => (
<div key={contact.id}>
<input {...register(`contacts.${i}.name`)} />
<input {...register(`contacts.${i}.email`)} />
<button onClick={() => remove(i)}>Удалить</button>
</div>
))}
Клонирование
<button onClick={() => append({ ...getValues('contacts.0') })}>
Копировать первый
</button>
Валидация динамических полей
<input
{...register(`tags.${index}.value`, {
required: 'Обязательно',
minLength: { value: 2, message: 'Минимум 2 символа' },
})}
/>
{errors.tags?.[index]?.value && (
<p>{errors.tags[index].value.message}</p>
)}
Итоги
- Динамические поля — через массив в стейте
- Уникальный
idдляkey useFieldArrayв React Hook Form- Вложенные поля — через точечную нотацию
- Валидация по индексу