Перейти к содержимому

Назад к блогу

reactформыzodcodegen

FromSchema: генерируем React-форму из одной строки Zod-схемы

Form.FromSchema генерирует полную форму автоматически. Четыре уровня контроля: FromTemplate → FromSchema → AutoFields → Field.* — начинаешь с автогенерации, детализируешь по мере необходимости.

15 мая 2026 г.

--TL;DR:

    генерирует полную форму из Zod-схемы за одну строку — маппинг типов на компоненты автоматическийЧетыре уровня контроля: FromTemplate -> FromSchema -> AutoFields -> Field.* — начинаете с автогенерации, детализируете по мере необходимостиДля CRUD-форм, прототипирования и админ-панелей FromSchema экономит тысячи строк кода

Кому полезно:

    Junior: научиться создавать формы в одну строку без ручной вёрстки каждого поляMiddle: освоить комбинирование AutoFields с ручными полями и паттерны FromTemplate/Builder/ConversationalModeSenior: оценить архитектуру маппера Zod type -> компонент и стратегию четырёх уровней контроля

Седьмая статья из цикла «@letar/forms — от боли к декларативным формам». Как

автоматически генерирует полную форму из Zod-схемы — и когда этого достаточно.


Идея: а что, если ноль JSX?

В предыдущих статьях мы показали, как Compound Components (

) делают JSX чистым. Но что если и этот JSX убрать?

// Вся форма — одна строка
<Form.FromSchema schema={UserSchema} initialValue={data} onSubmit={save} />

Библиотека сама:

    Обходит Zod-схемуОпределяет тип каждого поляПодбирает компонент (
    ,
    ,
    , ...)Читает
    для label, placeholder, описанияРендерит форму с кнопкой Submit

Как это работает

Шаг 1: обход схемы

// Упрощённая версия schema-traversal.ts
function enumerateFields(schema) {
  const shape = unwrapToBaseSchema(schema)._zod?.def?.shape
  if (!shape) return []

  return Object.entries(shape).map(([name, fieldSchema]) => ({
    name,
    zodType: getZodType(fieldSchema), // string, number, boolean, ...
    required: isRequired(fieldSchema),
    constraints: extractConstraints(fieldSchema),
    meta: getMeta(fieldSchema),
  }))
}

Для схемы:

const UserSchema = z.object({
  name: z
    .string()
    .min(2)
    .meta({ ui: { title: 'Имя' } }),
  email: z
    .string()
    .email()
    .meta({ ui: { title: 'Email' } }),
  role: z.enum(['admin', 'user']).meta({ ui: { title: 'Роль' } }),
  age: z
    .number()
    .min(18)
    .meta({ ui: { title: 'Возраст' } }),
  bio: z
    .string()
    .max(500)
    .optional()
    .meta({ ui: { title: 'О себе' } }),
})

Получаем:

[
  { name: 'name',  zodType: 'string',  required: true,  meta: { title: 'Имя' } }
  { name: 'email', zodType: 'string',  required: true,  meta: { title: 'Email' }, constraints: { inputType: 'email' } }
  { name: 'role',  zodType: 'enum',    required: true,  meta: { title: 'Роль' }, enumValues: ['admin', 'user'] }
  { name: 'age',   zodType: 'number',  required: true,  meta: { title: 'Возраст' } }
  { name: 'bio',   zodType: 'string',  required: false, meta: { title: 'О себе' }, constraints: { maxLength: 500 } }
]

Шаг 2: маппинг типов на компоненты

string               → FieldString
string + email()     → FieldString (type="email")
string + max > 200   → FieldTextarea
number               → FieldNumber
boolean              → FieldCheckbox
date                 → FieldDate
enum                 → FieldNativeSelect
array(string)        → FieldTags

// Переопределение через fieldType в meta:
fieldType: 'currency'    → FieldCurrency
fieldType: 'richText'    → FieldRichText
fieldType: 'phone'       → FieldPhone

Шаг 3: рендер

function FormFromSchema({ schema, initialValue, onSubmit, submitLabel, exclude }) {
  return (
    <Form schema={schema} initialValue={initialValue} onSubmit={onSubmit}>
      <VStack align="stretch" gap={4}>
        <FormAutoFields exclude={exclude} />
        <HStack justify="flex-end">
          <Form.Button.Submit>{submitLabel ?? 'Сохранить'}</Form.Button.Submit>
        </HStack>
      </VStack>
    </Form>
  )
}

— компонент, который обходит схему и рендерит каждое поле через маппер.


Кастомизация без отказа от автогенерации

Исключение полей

<Form.FromSchema
  schema={ProductSchema}
  initialValue={data}
  onSubmit={save}
  exclude={['id', 'createdAt', 'updatedAt']} // Скрыть служебные поля
/>

AutoFields + ручные поля

Когда нужен контроль над частью формы:

<Form schema={Schema} initialValue={data} onSubmit={save}>
  {/* Автоматически все поля, кроме description */}
  <Form.AutoFields exclude={['description']} />

  {/* description — вручную, с кастомной обёрткой */}
  <Box p={4} bg="gray.50" borderRadius="md">
    <Form.Field.RichText name="description" />
  </Box>

  <Form.Button.Submit />
</Form>

генерирует все поля автоматически, а
вы контролируете сами. Лучшее из двух миров.

include: только нужные поля

<Form schema={UserSchema} initialValue={data} onSubmit={save}>
  {/* Только name и email */}
  <Form.AutoFields include={['name', 'email']} />
  <Form.Button.Submit />
</Form>

Четыре уровня контроля

Уровень 1: FromSchema      — ноль JSX, всё автоматически
           ↓ нужна кастомная вёрстка?
Уровень 2: AutoFields      — автогенерация + ручные поля
           ↓ нужен полный контроль?
Уровень 3: Form.Field.*    — Compound Components
           ↓ нужна императивная логика?
Уровень 4: useAppForm      — хук, полный доступ к TanStack Form

Начинаете с FromSchema. Когда нужно больше контроля — спускаетесь. Не нужно переписывать — уровни совместимы.


Когда FromSchema — идеальный выбор

CRUD-формы

Модель Product с 10 полями. Нужна форма создания и редактирования:

// Создание
<Form.FromSchema
  schema={ProductCreateSchema}
  initialValue={emptyProduct}
  onSubmit={createProduct}
  submitLabel="Создать"
/>

// Редактирование
<Form.FromSchema
  schema={ProductUpdateSchema}
  initialValue={product}
  onSubmit={updateProduct}
  submitLabel="Сохранить"
  exclude={['id']}
/>

Две формы — две строки.

Прототипирование

Быстро набросать форму, пока дизайн не готов:

const schema = z.object({
  title: z.string().meta({ ui: { title: 'Название' } }),
  price: z.number().meta({ ui: { title: 'Цена', fieldType: 'currency' } }),
  category: z.enum(['A', 'B', 'C']).meta({ ui: { title: 'Категория' } }),
})

// Форма готова к тестированию
<Form.FromSchema schema={schema} initialValue={{}} onSubmit={console.log} />

Админ-панели

50 моделей × 2 формы (create + edit) = 100 форм. С FromSchema — 100 строк вместо 5000+.


Ещё больше автоматизации

Form.FromTemplate — готовые шаблоны

10 шаблонов для типичных форм. Ноль конфигурации:

// Контактная форма
<Form.FromTemplate template="contact" onSubmit={handleContact} />

// Логин
<Form.FromTemplate template="login" onSubmit={handleLogin} />

// Регистрация
<Form.FromTemplate template="register" onSubmit={handleRegister} />

Доступные шаблоны:

,
,
,
,
,
,
,
,
,
.

Каждый шаблон — готовая Zod-схема + подобранные компоненты + валидация. Можно кастомизировать через

:

<Form.FromTemplate template="contact" overrides={{ fields: { phone: { required: true } } }} onSubmit={handleSubmit} />

Form.Builder — JSON-конфигурация

Для динамических форм (CMS, конструкторы, no-code) — JSON-driven подход. Менеджер или продакт описывает форму в JSON, разработчик не трогает JSX:

import { FormBuilder } from '@letar/forms'

const config = {
  sections: [
    {
      title: 'Контактные данные',
      fields: [
        { name: 'name', type: 'string', label: 'Имя' },
        { name: 'email', type: 'string', label: 'Email' },
        { name: 'phone', type: 'phone', label: 'Телефон' },
      ],
    },
    {
      title: 'Заказ',
      fields: [
        { name: 'price', type: 'currency', label: 'Цена' },
        { name: 'category', type: 'select', label: 'Категория', options: categories },
        { name: 'quantity', type: 'number', label: 'Количество' },
      ],
    },
  ],
}

<FormBuilder
  config={config}
  initialValue={{ name: '', email: '', phone: '' }}
  onSubmit={save}
/>

Поддерживаемые типы полей:

,
,
,
,
,
,
,
,
,
,
,
,
,
. Тип
определяет компонент из Zod-схемы автоматически. JSON-конфиг можно хранить в БД и подгружать через API — форма строится без единой строчки JSX.

ConversationalMode — Typeform-style

Одно поле за раз. Идеально для опросов, NPS и анкет:

import { ConversationalMode } from '@letar/forms'

<Form
  schema={SurveySchema}
  initialValue={{ name: '', satisfaction: 0, feedback: '' }}
  onSubmit={submitSurvey}
>
  <ConversationalMode
    showProgress
    showQuestionNumber
    completedScreen={<Text>Спасибо за ответы!</Text>}
  >
    <Form.Field.String name="name" label="Как вас зовут?" />
    <Form.Field.Rating name="satisfaction" label="Оцените сервис" />
    <Form.Field.Textarea name="feedback" label="Что можно улучшить?" />
  </ConversationalMode>
</Form>

оборачивает обычные
— каждый дочерний элемент становится отдельным шагом. Анимация fade-in-up, навигация Enter/Alt+стрелки, прогресс-бар и нумерация вопросов. Работает с любыми полями из библиотеки.

Иерархия уровней контроля

Макс. автоматизация → Макс. контроль

FromTemplate → FromSchema → AutoFields → Builder → ConversationalMode → Field.*
   ↑              ↑            ↑           ↑              ↑                ↑
 Шаблоны     Вся форма    Часть полей   JSON-driven   По одному       Ручной JSX

Когда что использовать

СценарийИнструмент
Типовая форма
Линейная форма
Частичная автогенерация
CMS / динамические
Опросники / анкеты
Сложный layout
(Compound Components)

Когда FromSchema НЕ подходит

    Сложная вёрстка — двухколоночный layout, табы, аккордеоны → используйте Compound ComponentsМультистеп — FromSchema не поддерживает Steps → вручнуюУсловный рендеринг — When не работает внутри AutoFields → вручнуюНестандартный UX — кастомные анимации, inline-editing → вручную

Правило: если форма «линейная» (поля идут сверху вниз) — FromSchema. Если сложный layout — Compound Components.


Итоги

ЧтоКак
Полная автогенерация
Частичная автогенерация
Готовые шаблоны
JSON-конфигурация
Typeform-style
внутри
Исключение полей
Включение полей
Маппинг типовАвтоматический (Zod type → компонент)
Переопределение
в

Попробовать

    AutoFields: forms-example.letar.best/examples/auto-fieldsПродвинутое: forms-example.letar.best/examples/auto-fields-advancedИсходный код: auto-fields | auto-fields-advancedКлонировать:

В следующей статье — full-stack pipeline: от

(база данных) через ZenStack до готовой формы за 5 минут (подробнее — в статье 8: ZenStack pipeline).


Это седьмая статья из цикла «@letar/forms — от боли к декларативным формам». Предыдущая: Массивы и группы | Следующая: ZenStack pipeline.

Комментарии (0)

Войдите, чтобы оставить комментарий. Войти

Пока нет комментариев. Будьте первым!