Сегодня разбираем материал smashingmagazine.com о теме «Создание динамических форм в React и Next.js». Практический разбор с шагами и примерами, который можно быстро применить в своей работе.
В среде React-разработчиков существует устойчивая практика, предполагающая, что формы следует рассматривать как компоненты

React Hook Form для локального состояния (минимум повторных рендеров, удобная регистрация полей, императивное взаимодействие).
Zod для валидации (корректность ввода, граничная проверка, типобезопасный парсинг).
React Query для бэкенда: отправка, повторные попытки, кэширование, синхронизация с сервером и так далее.
Этот подход хорошо работает для форм, например, экранов входа, страниц настроек или CRUD-модалок. Каждый компонент выполняет свою задачу и легко интегрируется в различные части приложения, что улучшает общую пользовательскую практику
Порой форма усложняется правилами видимости, зависящими от предыдущих ответов, или производными значениями, влияющими на несколько полей. В некоторых случаях это даже целые страницы, которые необходимо показывать или скрывать в зависимости от сумм
Первое условие вы обрабатываете через useWatch и встроенное ветвление — это нормально. Потом ещё одно. Потом вы уже тянетесь к superRefine, чтобы закодировать межполевые правила, которые Zod-схема не может выразить обычным способом. Затем навигация по шагам начинает утекать в бизнес-логику. В какой-то момент смотришь на то, что построил, и понимаешь: форма — это уже не совсем UI. Это скорее процесс принятия решений, а дерево компонентов — просто место, где вы его случайно разместили
В таких ситуациях использование стека RHF + Zod может стать менее эффективным. Мы продолжаем его применять даже тогда, когда его абстракции слабо соответствуют текущей задаче, поскольку альтернатива требует нового подхода к работе с формами
В данной статье мы рассмотрим альтернативный метод. Мы покажем, как реализовать одну и ту же многошаговую форму дважды:
- С React Hook Form + Zod, подключёнными к React Query для отправки данных,
- С SurveyJS, который рассматривает форму как данные — простую JSON-схему — а не как дерево компонентов.
Мы сохраним одинаковые требования, условную логику и API-вызов, чтобы затем рассмотреть, что изменилось, а что осталось неизменным, и разобраться, как выбрать подходящую модель и в каких ситуациях её применять.
- Форма, которую мы строим
- Часть 1: Компонентный подход (React Hook Form + Zod)
- Установка
- Zod-схема
- Компонент формы
- Часть 2: Подход на основе схемы (SurveyJS)
- Установка
- Та же форма, но в виде данных
- Рендеринг и отправка данных
- Что переехало из React?
- Типичные ошибки при выборе подхода
- Когда использовать каждый подход
- Ответы на эти вопросы могут быть для вас полезными
Форма, которую мы строим
Форма использует 4-шаговый процесс:
Шаг 1: Данные
- Имя (обязательно)
- Email (обязательно, корректный формат)
Шаг 2: Заказ
- Цена за единицу
- Количество
- Ставка налога
- Производные: Подытог, Налог, Итого
Шаг 3: Создание аккаунта и обратная связь — У вас есть аккаунт? (Да/Нет) — если Да: введите имя пользователя и пароль, оба обязательны; если Нет: укажите email, уже собранный на шаге 1 — Оцените свой опыт (1–5) — если ≥ 4: спросите «Что вам понравилось?»; если ≤ 2: спросите «Что можно улучшить?».
Шаг 4: Проверка
- Появляется только если итого >= 100
- Финальная отправка
Это не экстремальный случай. Но его достаточно, чтобы выявить архитектурные различия.
Часть 1: Компонентный подход (React Hook Form + Zod)
Установка
Zod-схема
Начнем с Zod-схемы, так как именно здесь определяется структура данных формы. Для первых двух шагов — ввода личных данных и полей заказа — все достаточно просто: необходимо следить за обязательными полями, минимальными значениями чисел и корректными перечислениями. Интересные задачи возникают, когда требуется реализовать условные правила.
Обратите внимание, что username и password типизированы как optional(), хотя они условно обязательны, — потому что схема Zod на уровне типов описывает форму объекта, а не правила, определяющие, когда поля важны.
Условные требования должны быть определены внутри superRefine, который срабатывает после проверки формы и имеет доступ ко всему объекту. Это разделение удобно, так как superRefine предназначен для логики, которую невозможно выразить в схеме.
Эта схема не определяет концепцию страниц, не описывает, какие поля видны в данный момент, и не учитывает навигацию — это будет реализовано в другом месте.
Компонент формы
Смотрите Pen [SurveyJS-03-RHF [forked]](https://codepen.io/smashingmag/pen/gbwwmNO) by sixthextinction.
Здесь происходит довольно много всего, и стоит замедлиться, чтобы заметить, где что оказалось.
Производные значения — subtotal, tax, total — вычисляются в компоненте через useWatch и useMemo, потому что они зависят от живых значений полей, и для них нет другого естественного места.
Правила видимости для username, password, positiveFeedback и improvementFeedback живут в JSX в виде встроенных условий.
Логика пропуска шагов — страница проверки появляется только при total >= 100 — встроена в переменную showSubmit и условие рендера на шаге 3.
Сама навигация — это просто счётчик useState, который мы вручную увеличиваем.
React Query обрабатывает повторные попытки, кэширование и инвалидацию. Форма просто вызывает mutation.mutate с валидированными данными.
Ничего из этого само по себе не является неправильным. Это всё ещё идиоматический React, и компонент весьма производителен благодаря тому, как RHF изолирует повторные рендеры.
Но если бы вы передали это кому-то, кто не писал этот код, и попросили объяснить, при каких условиях появляется страница проверки, им пришлось бы проследить через showSubmit, условие рендера шага 3 и логику кнопки навигации — три отдельных места — чтобы восстановить правило, которое можно было бы сформулировать в одной строке.
Форма работает, да, но поведение не поддаётся реальной инспекции как система. Его нужно мысленно выполнять.
Что ещё важнее, изменение требует участия инженеров. Даже небольшая правка, например изменение условия появления шага проверки, означает редактирование компонента, обновление валидации, открытие pull request, ожидание ревью и повторное развёртывание.
Часть 2: Подход на основе схемы (SurveyJS)
Теперь построим тот же поток с использованием схемы — и посмотрим, что изменится.
Установка
survey-core — независимый от платформы движок выполнения с лицензией MIT, который лежит в основе рендеринга форм SurveyJS. Именно та часть, которая нас здесь интересует. Он принимает JSON-схему, строит из неё внутреннюю модель и берёт на себя всё, что иначе пришлось бы размещать в вашем React-компоненте: вычисление выражений видимости, расчёт производных значений, управление состоянием страниц, отслеживание валидации и определение того, что означает «завершение» с учётом реально показанных страниц.
survey-react-ui — слой UI/рендеринга, который связывает эту модель с React. По сути, это компонент <Survey model={model} />, который перерисовывается при каждом изменении состояния движка. UI-библиотеки SurveyJS также доступны для Angular, Vue3 и многих других фреймворков.
Вместе они дают вам полностью функциональный многостраничный рантайм форм без написания ни единой строки управляющей логики.
Формат схемы — это просто JSON, никакого DSL или чего-либо проприетарного. Вы можете встроить его прямо в код, импортировать из файла, получить через API или хранить в столбце базы данных и гидратировать во время выполнения.
Та же форма, но в виде данных
Вот та же форма, на этот раз выраженная в виде JSON-объекта. Схема определяет всё: структуру, валидацию, правила видимости, производные вычисления, навигацию по страницам — и передаёт это модели Model, которая вычисляет всё это во время выполнения.
Сравните это с версией на RHF.
Блок superRefine, который условно требовал username и password, исчез. visibleIf: "{hasAccount} = 'Yes'" в сочетании с isRequired: true решает обе задачи вместе, прямо на самом поле, там, где вы и ожидаете их найти.
Цепочка useWatch + useMemo, вычислявшая subtotal, tax и total, заменена тремя полями типа expression, которые ссылаются друг на друга по имени.
Условие страницы review, которое в версии на RHF можно было восстановить только путём трассировки через showSubmit и ветку рендеринга шага 3, теперь — единственное свойство visibleIf на объекте страницы.
Та же логика присутствует. Просто схема даёт ей место для существования, где она видна изолированно, а не размазана по всему компоненту.
Также обратите внимание, что схема использует type: 'expression' для subtotal, tax и total. Expression — это поле только для чтения, которое используется главным образом для отображения вычисленных значений. SurveyJS также поддерживает type: 'html' для статического контента, но для вычисленных значений правильный выбор — expression.
Рендеринг и отправка данных
Всё очень просто. Подключите onComplete к вашему API тем же способом — через useMutation или обычный fetch:
Смотрите Pen [SurveyJS-03-SurveyJS [forked]](https://codepen.io/smashingmag/pen/emddWNV) by sixthextinction.
onComplete срабатывает, когда пользователь достигает конца последней видимой страницы. Таким образом, если total никогда не превышает 100 и страница review пропускается, событие всё равно срабатывает корректно, потому что SurveyJS вычисляет видимость перед тем, как определить, что является «последней страницей».
sender.data содержит все ответы вместе с вычисленными значениями (subtotal, tax, total) как поля первого класса, поэтому полезная нагрузка API идентична тому, что версия на RHF собирала вручную в onSubmit.
Паттерн mutationRef — это тот же приём, к которому вы прибегаете везде, где нужен стабильный обработчик событий для значения, меняющегося при каждом рендере, — ничего специфичного для SurveyJS здесь нет.
React-компонент больше не содержит никакой бизнес-логики вообще. Нет useWatch, нет условного JSX, нет счётчика шагов, нет цепочки useMemo, нет superRefine. React делает то, в чём он действительно хорош: рендерит компонент и подключает его к вызову API.
Что переехало из React?
В React остаются разметка, стилизация, подключение отправки данных и интеграция с приложением — то есть именно то, для чего React на самом деле и предназначен.
Всё остальное переехало в схему, и поскольку схема — это просто JSON-объект, её можно хранить в базе данных, версионировать независимо от кода приложения или редактировать через внутренние инструменты без необходимости деплоя.
Продакт-менеджер, которому нужно изменить порог, при котором активируется страница review, может сделать это, не трогая компонент. Для команд, где поведение форм часто меняется и не всегда управляется инженерами, это существенное операционное отличие — и именно этот момент я считаю ключевым при выборе подхода.
Типичные ошибки при выборе подхода
Самая распространённая ошибка — продолжать использовать RHF + Zod, когда форма уже давно перестала быть формой в привычном смысле. Признаки того, что вы перешли черту: superRefine разрастается до десятков строк, навигация по шагам управляется флагами состояния, а условный JSX занимает больше места, чем сами поля.
Обратная ошибка — тянуться к движку схем ради трёх полей и одного условия. SurveyJS добавляет зависимость и требует понимания формата схемы; для простых CRUD-форм это избыточно.
Третья ошибка — смешивать модели. Если вы начали с RHF и добавляете SurveyJS для одного сложного блока, вы получаете два источника истины о состоянии формы. На нашем опыте лучше выбрать одну модель на весь поток и придерживаться её.
Когда использовать каждый подход
Вот практическое правило, которое работает для меня: представьте, что вы полностью удаляете форму. Что вы потеряете?
Если это экраны — вам нужны формы, управляемые компонентами.
Если это бизнес-логика — пороговые значения, правила ветвления и условные требования, кодирующие реальные решения, — вам нужен движок схем.
Аналогично, если предстоящие изменения касаются главным образом меток, полей и макета, RHF справится отлично. Если же они касаются условий, результатов и правил, которые команда операций или юридический отдел может захотеть скорректировать во вторник днём, не создавая тикет, — модель схем с SurveyJS подходит честнее.
Эти два подхода на самом деле не конкурируют друг с другом. Они решают разные классы проблем, и ошибка, которую стоит избегать, — это несоответствие абстракции весу логики: обращаться с системой правил как с компонентом, потому что это привычный инструмент, или тянуться к движку политик, потому что форма выросла до трёх шагов и обзавелась условным полем.
Форма, которую мы здесь построили, намеренно находится вблизи границы — достаточно сложная, чтобы обнажить разницу, но не настолько крайняя, чтобы сравнение казалось подтасованным. Большинство реальных форм, которые стали громоздкими в вашей кодовой базе, вероятно, находятся вблизи той же границы, и вопрос обычно лишь в том, дал ли кто-нибудь им название, соответствующее тому, чем они на самом деле являются.
Используйте React Hook Form + Zod, когда:
- Формы ориентированы на CRUD
- Логика поверхностна и управляется UI
- Инженеры контролируют всё поведение
- Бэкенд остаётся источником истины
Используйте SurveyJS, когда:
- Формы кодируют бизнес-решения
- Правила развиваются независимо от UI
- Логика должна быть видимой, проверяемой или версионируемой
- Не-инженеры влияют на поведение
- Одна и та же форма должна работать на нескольких фронтендах
Ответы на эти вопросы могут быть для вас полезными
Можно ли использовать SurveyJS вместе с React Hook Form в одном проекте?
Технически — да, но в рамках одного потока это создаёт два источника истины о состоянии формы. Лучше выбрать одну модель на весь флоу: RHF для простых форм, SurveyJS для форм с разветвлённой лог


