Формы у React, керованыя схемай: адрасаванне та пераканальванне на адпаведнасць схемай JSON
Як адаптаваць падтверджаныя формы React безпасова з JSON Schema, карацься з $ref, oneOf і гілкамі if/then, дадзіць можлівасць выкарыстоўваць спецыяльныя віджэты і ухіліцца ад распашчыхся пры падтвержэнні проблем.
Рукапісны форма на React зазвычай ўтварае копію вялікага дакумента, які вже існуе. Схема запита API ведае, што паказначэнне email ўмовна і должна выглядаць як адреса электронной пошты, што age — це неамбівалентная цэлая числовая велічына, а role можа быць толькі однай з трох вялічынаў. Перапісваць гэтыя правіла ў JSX, праз бібліятэку верыфікацыі і ў поведамленнях пра адмоўкі значыць стварэнне трох разных варыянтаў адной і той жа структуры дадзеных, і яны пачынаюць несастоямасці ў момент змян у бэкенде.
У гэтым кансалтаты вы форму розглядаеце як тую, якая атрымвана з JSON Schema, пры чым як конкрэтную рэалізацыю выкарыстоўваецца адкрыты пакет react-simple-schema-form. Вы пазнаеце, як $ref, allOf, oneOf і if/then ператвараюцца на дынамічныя полья, як падключаць спецыяльныя элементы і якія правіла верыфікацыі дапамагаюць стварыць такую форму, якая выглядае як створаная вручную.
Чаму схема павінна кантролюваць форму
Змены ў структуре є прыгаднымі: новыя поля на бэкенде ніколі не прабачаюцца ў форму, а запиты на кшталт „паказаць толькі адресу для вырачунку за рахунком“ ператвараюцца на флаг useState, умовную візуалізацыю і галузь верыфікацыі, якія пасля калькоў выходзяць з синхронізаціі.
JSON Schema вже можа выражаць усі гэтыя правіла: типы, абмежэння, обавязковыя поля і умовную логіку. Часта це той самы документ, па якому ваш бэкенд верыфікуе запиты, і які ўключаецца ў спефікацыю OpenAPI. Якщо форма генеруецца з яго, змена схемы адразу ж апдэйтуе і інтерфейс, і процес верыфікацыі.
Мінімальная сформаваная форма
react-simple-schema-form прымае JSON Schema, напісана на адпаведнасці з draft-07, і атрыбутуе справяжаны форма. Згідна з яе дакументацыяй, у ёй немае жадных залежнасцей пад час выконання, за аднакоўшчынай React 18; ёй прылеглі сасавыя типы на TypeScript, а таксама ёсць неабяжны стайл-шіт. Інсталяцыя адбываецца за дапамою аднаго пакета:
npm install react-simple-schema-form
У прыкладзе нижэй описан маленький об’ект корыстніка: імя, адказнае пададзеные з format: 'email', цэлыкавое значэнне вяку, якое не можа быць меншым за ноль, і ролю, абмежаную за дапамою enum. Палічыкі name і email адзначаны як обавязковыя. Компонент прымае схему і функцію-вярненне onSubmit, і нічога больш.
import { SchemaForm } from 'react-simple-schema-form';
import 'react-simple-schema-form/styles.css';
const schema = {
type: 'object',
properties: {
name: { type: 'string', title: 'Name' },
email: { type: 'string', format: 'email', title: 'Email' },
age: { type: 'integer', minimum: 0, title: 'Age' },
role: { type: 'string', enum: ['Admin', 'Editor', 'Viewer'], title: 'Role' },
},
required: ['name', 'email'],
};
<SchemaForm schema={schema} onSubmit={(data) => save(data)} />
На адыме гэтага шымату бібліятэка адразоўвае поле для вводу тексту, поле для вводу электронной пашты, поле для вводу цифраў і спрабу для выбору з перыяду значэнняў, пазначае обавязковыя поля, апісвае падказкі пра бяглыя кантракцыі і вызывае onSubmit толькі калі даныя ў правільным стане. Компонент працуе ў обох режымах React: можна перадаць value і onChange, ў тым часе як ён керуецца, альбо defaultValue, каб ён сам кераваў своім станам.
Будзь-які генератор справляецца з такым плоскім об’ектам; справжнія випытанні — глэбокія і розгалужаныя шыматы.
Работа з шыматамі, якія не ў плоскім стане
Шыматы для працы ў рэальных умовах перадаюць застосоўваныя адзначэння, складаюць фрагменты і розгалужуюцца на адповіднасьць да даных. Бібліятэка рашае всё гэта на адной з асновае даных формы ў кожны раз пры адразоўванні, таму кожна ячэйка бачыць толькі плоскі шымат.
Павторны выкарыстоўванне адзначэнняў з $ref і allOf
Саюжная дэфініцыя, такая як address, можа быць выкарыстоўваная ў двух месцах і будзе адображацца як два незалежныя разделы. Ключавыя словы, распакаваныя ля $ref, заменяюць выкарыстоўваную дэфініцыю, таму { "$ref": "#/definitions/address", "title": "Shipping address" } стварае блок з адресам, пазначаным як „Адреса даставкі“. З allOf частыні з’еднаюцца глыбока: вярстаныя атрыбуты з’еднаюцца рекурсыўна, а масівы required сумаюцца ў іх спалучэнне.
Адносаванне oneOf да дискримінаванага спалучэння
Багатыя генераторы сталкнуцца з трудамі пад час выкарыстоўвання oneOf. Эфектывае рашэнне — дискримінаванае спалучэння: кожны варыянт прыкрепляе саюжны поле да фіксаванага значэння за дапамою const, а форма выкарыстоўвае гэта поле для выбору актывага варыянта.
У наведанай выкладзе платежнага шымату method — это элемент типу enum з значэннямі card або bank. Першы варіант задае method на card і выкалікае патрэбу ў значэнні number; другі задае яго на bank і выкалікае патрэбу ў значэнні iban.
{
"type": "object",
"properties": { "method": { "type": "string", "enum": ["card", "bank"] } },
"required": ["method"],
"oneOf": [
{ "title": "Card", "properties": { "method": { "const": "card" }, "number": { "type": "string" } }, "required": ["number"] },
{ "title": "Bank", "properties": { "method": { "const": "bank" }, "iban": { "type": "string" } }, "required": ["iban"] }
]
}
Змена значэння method з card на bank прыводзіць да замены поля з номерам карточкі на поле з IBAN. У вашам кодзе няма стану компонента і няма умовнага JSX; змены відбываюцца выключна за дапамою шымату. oneOf, чыяі варіанты складаюцца толькі з const, атрыбуецца як пасылак з пазначэнням.
Умовныя часткі з if/then/else і dependencies
Умовныя ключавыя слова перацэнвуюцца ўсё тады, калі змянююцыся даны, укладаючыся нават пасля кожнага нажатня клавішы. Корыстным рэшэньнем ёсць необавязковы раздзіл, які прабываецца толькі тады, калі корыстнік яго увялічвае. Наведзены выкладком фрагмент задае об’ект schedule з булевым флагам enabled (за значэнням false) і двумя полямі для дзён. Умова if выпалюецца, калі enabled равнае true, а умова then пры гэтым выклікае неабяжнасць указвання monday і tuesday.
"schedule": {
"type": "object",
"properties": {
"enabled": { "type": "boolean", "title": "Enable schedule", "default": false },
"monday": { "type": "string", "title": "Monday" },
"tuesday": { "type": "string", "title": "Tuesday" }
},
"if": { "properties": { "enabled": { "const": true } }, "required": ["enabled"] },
"then": { "required": ["monday", "tuesday"] }
}
Калі пакет увыклы, нічога ўнутрь роздзела не яўляецца обав’язковым, і адправка не блакуецца. Калі ён увыклы, оба поля з датамі паказваюць знакі обав’язковасці, і форму нельга адправіць, пакуль яны не заполнены. Паколькі пакет ўвыклы є часткаю дадзеных, а не локальнага стану інтэрфейсу, сервер можа пераверыць тыя ж дадзеныя за дапамою таго ж шэмы і выйсці до таго ж рашэння.
Лінію "required": ["enabled"] усередині if лёгкаа адзяць, але яна неабходная. У JSON Schema атрыбут properties дыяе толькі на ключы, якія ўжо ёсць. Таму об’ект без ключа enabled падтрымляе формат { "properties": { "enabled": { "const": true } } }, запускаецца блок then, і дні стаюць обавязковымі, нават якщо ця частка ніколі не была актывацавана. Абавязковасць ключа ў умове закрывае гэтыя праследы.
Выбір і налаштаванне віджэтаў
Сформаваны форма ўжывачны толькі тады, калі можна контролюваць, які элемент вводу выкарыстоўвае кожна польа. Бібліятэка зберагчыць реестр вбудованых віджэтам, укладаючы text, email, number, select, radio, checkboxes, textarea і date, і прысвячае іх тры спосабу:
- Атрыбут
uiSchema, ключаваны па шляху, з падтрымкай glob.tags.*націляецца на кожны элемент у масэ, а**.postalCode— на кожны поштовы код на будзь-яй глыбіні, нават усередине$ref, які выкарыстоўваецца ў двух месцах. Калі калькуюцца колькасць ключоў, перамагае найболей спецыфічны.
ui:*, а яго родны вузел можа мець вкладаны элемент uiSchema, які адначасова пазначаецца іменамі дзецячых вузлаў; такім чынам, той, хто апыляецца на спяльнае визначэнне, можа зменіць выгляд своіх дзецячых вузлаў.resolveWidget для выбора на адной з правіл, напрыклад: «Кожны цэлычасовы значэнне з format: epoch выкарыстоўвае элемент epoch». Яна прымеўа ўжо повнаста распрацаваную схему і може вернуць або назву элемента, або сам элемент.Прыоритэты зафіксаваны: uiSchema прыкладнення мае вышэйшы прыоритет за падзеі, убудованыя ў схему, якія зноў маюць вышэйшы прыоритет за правіла resolveWidget, а тыя — за стандартныя настройкі. Такая праўдападобнасць важная, калі схему апраносіць іншая команда: кліент завжды можа перакрыць ўсе падзеі, убудованыя ў схему.
Стварэнне спецыяльнага элемента
Віджэт — это компанент, які прыме текущую значэнне і функцыю-вызов onChange, а таксама атрыбуты такія як id, required, disabled і onBlur. У прыкладзе нижэй зберагаецца часовая метка у відэсах Unix, але корыстніку паказваецца стандартны выбірач datetime-local. Ён перакладае відэсы у роштку даты для адразлівання, а пасля змены знову парсуе вхідны данны, дзелячы мілісекунды на 1000, і прадае undefined, калі вхідны данны порожнія або некоректныя. Віджэт рэгіструецца пад іменем epoch і прызначаецца полю startsAt через uiSchema.
import type { Widget } from 'react-simple-schema-form';
const EpochWidget: Widget<number | undefined> = ({ id, value, onChange, onBlur, required, disabled }) => (
<input
type="datetime-local"
id={id}
required={required}
disabled={disabled}
value={value === undefined ? '' : new Date(value * 1000).toISOString().slice(0, 16)}
onBlur={onBlur}
onChange={(e) => {
const ms = new Date(e.target.value).getTime();
onChange(Number.isNaN(ms) ? undefined : Math.floor(ms / 1000));
}}
/>
);
<SchemaForm schema={schema} widgets={{ epoch: EpochWidget }} uiSchema={{ startsAt: { widget: 'epoch' } }} />
Схема паказвае integer, але корыстнік бачыць паказчык, і даныя зберагаюцца у відэсах Unix. Є адна прыметка: toISOString() генеруе даныя у формате UTC, тады як введэнне типу datetime-local і new Date(e.target.value) працуюць у місцевай часовой зоне корыстніка. За межами UTC паказаны час схыляецца на значэння зоновага ферзента, і кожная зміна пераменяе зберажанае значэння. Таму трэба форматаваць паказаныя даныя на адповідачны спосаб з урахоўванням місцевых частак даты, каб і тое, і другое было адпаведнае.
Віджэт таксама можа мяць цэлы об’ект або масів, прымоўваючы весь значэнне разам з усіма вкладзенымі памылкамі, і адрасаваць ўсе ўтварэнні через экспортаваны компонент <Field>. Самэ так раздзіл раскладу практыкуе функцыю увімкнення/закрыцця без таго, каб бібліятэка знала пра расклад.
Якщо схема апелюе даўняк, які ніколі не было зарэгістравана, бібліятэка фіксуе адзін паведамленне пра апазычнасць і пераходзіць на стандартны варыянт. Апазычнасць у схеме, якую аддаёт іншая каманда, должна спрыяць павольнам выключэнню функцыяналу, а не спрычыняць збой сторанкі.
Парадактаванне, яке адпавядае ачакванням пользователя
Вбудованный верыфікатор мае малы размер і не выкарыстоўвае залежнасці; большая частка його проектавання прызначана для выявлення момента, калі трэба адзвярнуць увагу на адмыслі, а не толькі для выявлення ўсіх адмысліў.
Адзвярнуць увагу на адмыслі ў правы момент
Адмыслі паказваюцца пасля таго, як пользователят выйдзе з поля, або ўсі разам пасля спробы адправкі, і ніколі не пад час першага атрыбутавання сторанкі. Калі адправка не успевае, фокус пераходзіць на пераша некоректнае поле.
Неўжытыя необавязковыя об’екты спрацоўваць як відсутныя
Ёнкэлівацыя заполнення для вярстаўных об’ектаў адбываецца за дапамогою {} у форме. Нявучы верыфікатор тады бы захацеў паведамленні street і city для необавязковага адресу, якім корыстнік ніколі не паводзіўся. Рашэння — спрацоўваць необавязковы об’ект, у якога всі значэння ў порожніх, як аб’ект, якога няма, тады не будзе стварвацца жадных адхэленаў. Об’ект, які є обавязковым, завжды верыфікуецца, і ў яго адхэленах паказваюцца тыя дзеціны, якіх не хватаяць, у заместо расплывчатага паведамлення „Адреса ўобов’язковая“.
Адхэлены атрыбута oneOf для актыўнай галузі
Калі жадны з варыянтаў oneOf не падтверджваеся, стандартная паведамленне „Даныя должны точна адпаваць адной схеме“ не мае значэння для пользователя. У замене верыфікатор выявляе, да каго варыянта належаць даныя, паўтараючы перапісванне на адміністратыўныя паказнікі та типы, але ігнаруючы атрыбут required, і паведамляе пра бягі на рэвэлі галузі. Для платежа з method: card без номера карткі бяг паказваецца ў поле з номерам карткі, дзе і будзе шукаты корыстнік.
Ніколі не дазволяйце скрытым полям блакаваць адправку
Залишаныя, непачатыя значэнні ў раздзеле, які быў выключаны, не должны спрычыняць абліквідацыю перапісвання pattern, якое корыстнік не можа бачыць. Правіла знаходзяцца ў схеме, але рашэнне — у віджэтэ: ён чыстае раздзел, калі той выключаны, а атрыбут errors паведамляе віджэт, якія бягі існуюць у частцы, яку ён скрывае.
Павтарна выкарыстоўванне правілаў за межамі React
Валідатор таксама вывозится як самостоятельны элемент. Функцыя validate(schema, data) вяртае спіс з элементам { path, keyword, message }, таму тыя ж правіла можна выконваць у сервісе Node.js, у тэстах на елементарныя часткі коду або пры перадрукаванні будь-чаго. Для альтэрнатывы, якая выкорыстоўвае TypeScript, адзірніце як раздзеліць адну схему Zod между React і Node.
Дасяглед, спрытаваны да памагчыкаў у напісанні коду
Формы часта ствараюцца за дапамогою ШІ-памагчыкаў у напісанні коду, таму пакет таксама містыць дасяглед, спрытаваны як для машын, так і для людзей:
- Файл з інструкцыямі пра навыкі агента пад адресе
skills/react-simple-schema-form/SKILL.mdунутроц пакета npm, які інструменты, які падтрымляюць формат навыкаў агента, можаць завантажыць зnode_modules. У яму раскладзеныя API, прыорітэты віджэтаў, вышэйзгаданыя способы рэалізацыі і вядомыя проблемы; габарыты файла станавілі адпраўна 7 kB на момент напісання. llms.txtіllms-full.txtна сайце-дэманстрацыі, якія аб’еднваюць файл README, інструкцыі пра навыкі та кожны прыклад схемы ў аднам файле, які можна скопіюваць у чат або індексаваць серверам MCP для документацыі.- JSDoc з прыкладамі да кожнага экспорту; калі паверх заяв пра типы ў рэдагувальніку пасунуць курсор, будзе адразу показана інструкцыя ўжывання.
- Файл
context7.json, які дапамагае репазітарыю чыста індексавацься ў Context7.
Гэта не змусіць модель выбраць бібліятэку, але падышае шансы на тое, што першая спроба асистента будзе успешной; гэта практыка, якую варта пераняць і для внутрашнях бібліятэкаў.
Пробаваецца
жывая дама размешчае рэдагувальнік схемы пад генераваным формой, а пад ягою — жывыя даны і памылкі. У ёй є прыклады для $ref, allOf, oneOf, if/then/else, dependencies і выбору віджэтов. Пакет выпускаецца на npm, а выкладкі коду і трэйкер проблем — на GitHub. Це молады проект, таму перапрацаваць я ў своіх схемах прычынна, перш чым паслужыцца яй.
Галоўныя выводы
- Якщо API ўжо публікуе JSON Schema, створэнне формы на ўснове яго пазбавляе ад дублікацыяў правілаў і прабачае валідазію на стороне UI і сервера ў адпаведнасці.
- Раскідаюце
$ref,allOf,oneOfі умовныя элементы на адпаведнасць рэальным дадзеным, каб кожна полькі мела простую структуру шымату. - Формы з разнымі варіантамі моделей трэба представляць як дискримінаваныя юніі з викорыстоўванням
const, і завжды дадаваць атрыбутrequiredу клазухахif. - Неабходна, каб выбір віджэтов можна было зменіць, задаўшы чыстую прадкладку, особліва для шыматоў, якія належаць іншай камандзе.
- Хорашы створаныя формы залежаць ад часу валідазіі: трэба фіксавацыю працэсу пад час выключэння фокуса аб выканання надсылкі, ігнараваць незмененыя необов’язковыя об’екты, а паказваць памылкі
oneOfдля актыўнай галузі.