Урок 08

Динамические поля

Иногда форма меняется: добавляются поля, удаляются, клонируются. Такие формы называются динамическими.

Простой пример: список

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
  • Вложенные поля — через точечную нотацию
  • Валидация по индексу