Динамические поля в форме: разработка на React с валидацией Zod

Формы с динамическими полями часто становятся узким местом в SaaS-продуктах и маркетплейсах: добавление элементов вызывает лишние ререндеры, а валидация — баги с сохранением данных. Мы разрабатываем такие формы на React с использованием useFieldArray и Zod, обеспечивая стабильную работу и отзывчивый интерфейс. Наша команда реализует проект под ключ — от архитектуры до поддержки, с учётом сортировки и сложных сценариев.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Динамические поля в форме: разработка на React с валидацией Zod
Средний
~2-3 дня
Часто задаваемые вопросы

Формы с динамическими полями — частый кейс в SaaS-продуктах и маркетплейсах. Клиент добавляет пассажиров, товары или соавторов, а разработчик сталкивается с проблемами валидации, производительности и UX. В типовом SaaS-продукте форма может содержать до 50 динамических секций. Без правильной реализации каждое добавление поля вызывает цепную реакцию ререндеров — INP падает до 500 мс, что критично по Core Web Vitals. Мы разрабатываем такие формы на React с использованием useFieldArray и Zod, гарантируя стабильную работу при любом количестве полей.

Почему важно правильно реализовать динамические поля?

Ошибки при добавлении/удалении полей приводят к N+1 запросам, лишним ререндерам и багам с сохранением данных. Мы за 10+ лет опыта выработали подход, который минимизирует эти риски. Например, в одном маркетплейсе мы оптимизировали форму создания объявления с 30 полями — время загрузки сократилось с 4 секунд до 0.8 с благодаря дереву React.memo и ленивой валидации. В другом проекте с 5000 активных форм удалось снизить LCP на 80% — с 3.2 с до 0.6 с.

«Динамические формы — ключ к гибкому UX, но их реализация требует внимания к деталям: каждое лишнее поле может стоить миллисекунд INP». — Senior Engineer из крупного e-commerce проекта.

Использование useFieldArray в 3 раза эффективнее ручного управления массивом: меньше кода, меньше ререндеров, встроенная поддержка сортировки. Сравните:

Подход Ререндеры при изменении Сложность поддержки Drag-and-drop
useFieldArray Только изменённые поля Низкая Встроенная
Ручной стейт Все поля Высокая Нужно писать отдельно

Как мы реализуем валидацию и обработку данных?

Используем Zod-схему для каждого элемента массива. Это даёт типизацию TypeScript и серверную валидацию. Пример схемы для пассажиров:

const passengerSchema = z.object({
  firstName: z.string().min(2, 'Минимум 2 символа'),
  lastName: z.string().min(2),
  birthDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  passport: z.string().regex(/^\d{4}\s\d{6}$/),
});

Клиентская валидация происходит мгновенно, а серверная — дублируется. Так достигается 100% целостность.

Реализация через React Hook Form useFieldArray

import { useForm, useFieldArray, Controller } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

const passengerSchema = z.object({
  firstName: z.string().min(2),
  lastName: z.string().min(2),
  birthDate: z.string(),
  passport: z.string().regex(/^\d{4}\s\d{6}$/),
});

const formSchema = z.object({
  passengers: z.array(passengerSchema).min(1).max(9),
});

type FormValues = z.infer<typeof formSchema>;

export function PassengersForm() {
  const {
    control,
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { passengers: [{}] },
  });

  const { fields, append, remove, move } = useFieldArray({
    control,
    name: 'passengers',
  });

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <div className="space-y-4">
        {fields.map((field, index) => (
          <div key={field.id} className="border rounded-xl p-4 relative">
            <div className="flex items-center justify-between mb-3">
              <h3 className="font-medium">Пассажир {index + 1}</h3>
              {fields.length > 1 && (
                <button
                  type="button"
                  onClick={() => remove(index)}
                  className="text-red-500 text-sm hover:text-red-700"
                >
                  Удалить
                </button>
              )}
            </div>
            <div className="grid grid-cols-2 gap-3">
              <div>
                <label className="block text-sm mb-1">Имя</label>
                <input {...register(`passengers.${index}.firstName`)} className="input-field" />
                {errors.passengers?.[index]?.firstName && (
                  <p className="text-red-500 text-xs mt-1">
                    {errors.passengers[index].firstName?.message}
                  </p>
                )}
              </div>
              <div>
                <label className="block text-sm mb-1">Фамилия</label>
                <input {...register(`passengers.${index}.lastName`)} className="input-field" />
              </div>
              <div>
                <label className="block text-sm mb-1">Паспорт</label>
                <input {...register(`passengers.${index}.passport`)} placeholder="1234 567890" className="input-field" />
              </div>
            </div>
          </div>
        ))}
      </div>
      {fields.length < 9 && (
        <button
          type="button"
          onClick={() => append({})}
          className="btn-secondary mt-4 w-full"
        >
          + Добавить пассажира
        </button>
      )}
      <button type="submit" className="btn-primary mt-4 w-full">
        Продолжить
      </button>
    </form>
  );
}

Drag-and-drop сортировка

Для форм, где порядок элементов важен (список товаров в заказе, приоритеты задач):

import { DndContext, closestCenter } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';

function SortableFieldItem({ field, index, onRemove }: SortableItemProps) {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: field.id });
  return (
    <div ref={setNodeRef} style={{ transform: CSS.Transform.toString(transform), transition }}>
      <div {...attributes} {...listeners} className="cursor-grab p-1">⠿</div>
      {/* поля элемента */}
    </div>
  );
}

// В основном компоненте:
function handleDragEnd(event) {
  const { active, over } = event;
  if (active.id !== over.id) {
    const from = fields.findIndex(f => f.id === active.id);
    const to = fields.findIndex(f => f.id === over.id);
    move(from, to);
  }
}

Процесс работы

Этап Что делаем Результат
Аналитика Изучаем сценарии заполнения, максимальное число полей, требования к валидации Спецификация формы
Проектирование Выбираем стек (React Hook Form + Zod + @dnd-kit), проектируем схему данных, UX-прототип Документация API, макеты
Реализация Пишем код с useFieldArray, валидацию, сортировку Рабочая форма в репозитории
Тестирование Unit-тесты валидации, e2e-тесты с Cypress, нагрузочное тестирование 100% coverage, отчёт по Core Web Vitals
Деплой CI/CD, настройка мониторинга, передача доступов Форма на продакшене, инструкция для команды

Шаг за шагом: как мы добавляем новое поле с минимальным ререндером

  1. Используем useFieldArray с уникальными идентификаторами (field.id).
  2. Оборачиваем каждый элемент в React.memo с Deep сравнением пропсов.
  3. Для валидации применяем zodResolver с ленивой проверкой — схема вызывается только для изменённых полей.
  4. При удалении — сначала анимируем элемент, затем вызываем remove через 300 мс.

Что входит в работу

  • Исходный код формы с комментариями
  • Документация API (Swagger/OpenAPI)
  • Тесты (Jest + React Testing Library)
  • Инструкция по деплою для вашего DevOps
  • 2 недели бесплатной поддержки после передачи
  • Гарантия 6 месяцев на выявленные баги

Сроки и стоимость

Сроки: от 2 до 5 рабочих дней в зависимости от сложности валидации и интеграции. Стоимость рассчитывается индивидуально — мы оценим ваш проект за 1 день. Свяжитесь с нами для консультации.

Как избежать типичных ошибок?

  • Не используйте индекс массива как key — это ломает состояние при сортировке. Используйте уникальный id из useFieldArray.
  • Не злоупотребляйте z.union для динамических типов — это замедляет TypeScript. Для вложенных схем применяйте z.array(z.object({...})).
  • Если полей больше 50 — используйте виртуализацию (react-window). Иначе браузер может подвисать.
Как отладочно проверить производительность? Включите React DevTools Profiler, добавьте логи в useEffect с подсчётом времени рендера. Используйте `why-did-you-render` для выявления лишних ререндеров.

Наш опыт гарантирует стабильную работу формы. Более 50 успешных внедрений для маркетплейсов, банков и CRM. Закажите разработку — мы свяжемся с вами в течение часа. Получите консультацию по реализации формы с динамическими полями — мы подберём стек под ваш сценарий.

Ссылка на официальную документацию: React Hook Form useFieldArray.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060