Skip to main content

React-хуки

React-часть ядра живёт в отдельном входе @reformer/core/react (React 18+). Корень @reformer/core от React свободен и хуков не экспортирует. Хуки чтения построены на useSyncExternalStore и обновляют компонент точечно — только когда меняются сигналы, на которые подписан хук.

ХукЧто возвращаетКогда брать
useFormBundle(factory)стабильную сборку createFormсобрать форму в компоненте один раз
useFormControl(control)полное состояние ноды (объект)нужны value, issues, touched, disabled и т.д.
useFormControlValue(control)только значение (T напрямую)нужно лишь значение — условный рендер, вычисления
useArrayLength(array)длину массива (number)перерисовать список на добавление и удаление строк
useFormFlow(flow)снимок потока шаговсвой индикатор, кнопки или заголовок визарда
useIssueMessage(messages?)функцию «замечание → текст»показать текст замечания своим компонентом
useFormBundleContext()сборку из контекста или nullкомпонент внутри FormRenderer / FormBundleContext
import {
useFormBundle,
useFormControl,
useFormControlValue,
useArrayLength,
useFormFlow,
} from '@reformer/core/react';
Доступ к нодам — по имени поля

Ноды берут из формы по имени: form.email, form.address.city, form.phones — напрямую, без промежуточного контейнера полей. Поле, имя которого совпадает с членом ноды (value, issues, reset, …), доступно через form.$: form.$.value.

useFormBundle​

Собирает форму один раз за время жизни компонента: фабрика вызывается ленивым инициализатором useState. Заодно армирует живую проверку, если у сборки задана стратегия (validation: { strategy: 'blur' }), и снимает её при размонтировании. Сборку целиком хук не снимает: bundle.dispose() зовёт владелец сборки, когда форма больше не нужна (см. Отправка и сброс).

import { createForm } from '@reformer/core';
import { useFormBundle } from '@reformer/core/react';

function ContactPage() {
const bundle = useFormBundle(() =>
createForm<ContactForm>({ initial: { name: '', email: '' }, schema: contactSchema })
);
const { form, model } = bundle;
// …
}

useMemo для этого не годится: React вправе сбросить его кэш и пересобрать форму, потеряв введённое. Передавайте именно фабрику, а не готовую сборку.

useFormControl​

Подписка на всё состояние ноды. Компонент перерисовывается только при реальном изменении данных контрола.

import { useFormControl, useIssueMessage } from '@reformer/core/react';
import type { FormControl } from '@reformer/core';

function TextField({ control, label }: { control: FormControl<string>; label: string }) {
const { value, disabled, issues, shouldShowError, shouldShowIssues, pending } =
useFormControl(control);
const message = useIssueMessage(); // код замечания → текст из словаря локали

return (
<div className="field">
<label>{label}</label>
<input
value={value ?? ''}
disabled={disabled}
onChange={(e) => control.setValue(e.target.value)}
onBlur={() => control.markAsTouched()}
aria-invalid={shouldShowError}
/>
{shouldShowIssues && issues[0] && <span className="error">{message(issues[0])}</span>}
{pending && <span className="loading">Проверка…</span>}
</div>
);
}

Состояние для FormControl​

СвойствоТипОписание
valueTтекущее значение
issuesValidationIssue[]замечания валидации ([], когда их нет)
validbooleanблокирующих замечаний нет
invalidbooleanесть блокирующие замечания
touchedbooleanпользователь взаимодействовал (был blur)
dirtybooleanзначение меняли
disabledbooleanполе отключено
pendingbooleanидёт асинхронная проверка
shouldShowErrorbooleanсостояние ошибки (aria-invalid): поле невалидно, его трогали или меняли
shouldShowIssuesbooleanпора показать тексты замечаний — ошибок и предупреждений: они есть, поле трогали или меняли
componentPropsRecord<string, unknown>пропсы компонента из схемы (label, options, …)

Замечание — { code, params?, severity? }: текста в нём нет. Текст по коду даёт словарь (defineIssueMessages) — через useIssueMessage(messages) или провайдер ValidationMessagesProvider messages из @reformer/cdk, который читают FormField и useIssueMessageResolver().

Состояние для FormArray​

То же самое, плюс length; у массива нет shouldShowError, shouldShowIssues и componentProps.

СвойствоТипОписание
valueT[]значения всех элементов
lengthnumberколичество элементов
issuesValidationIssue[]замечания массива и его строк
valid / invalidbooleanвалиден ли массив и все строки
touchedbooleanбыло взаимодействие с любой строкой
dirtybooleanмассив изменился
disabledbooleanмассив отключён
pendingbooleanидёт асинхронная проверка
tip
componentProps — из схемы

label, placeholder, options и прочие пропсы задаются в узле схемы и читаются из useFormControl(control).componentProps, а не из JSX-пропсов. Подробнее — в разделе Свои компоненты полей.


useFormControlValue​

Подписка только на значение. Компонент не перерисовывается при изменении issues, touched, valid и других свойств — это дешевле, чем useFormControl, когда нужно лишь значение.

Не деструктурируйте результат

useFormControlValue возвращает значение T напрямую, а не объект. Присваивайте его переменной целиком.

const email = useFormControlValue(form.email); // ✅ верно
const { value } = useFormControlValue(form.email); // ❌ значение — не объект

Типичный сценарий — условный рендер по значению другого поля:

import type { FormGroup } from '@reformer/core';

function ShippingSection({ form }: { form: FormGroup<CheckoutForm> }) {
// Перерисовка только когда меняется hasShipping
const hasShipping = useFormControlValue(form.hasShipping);

if (!hasShipping) {
return null;
}

return (
<div className="shipping">
<FormField control={form.city} />
<FormField control={form.street} />
</div>
);
}

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

  • условный рендер секции по значению флага/селекта;
  • вычисляемое отображение (счётчик символов, превью, «итого по строке»);
  • показ значения только для чтения.

Если компоненту нужны issues, disabled или другие свойства — берите useFormControl: одна подписка эффективнее, чем несколько разных хуков на один контрол.


useArrayLength​

Длина массива строк. useArrayLength подписывается только на длину и не перерисовывает компонент при изменении полей внутри строк. Строки рисуют через ноду массива (form.phones.map), ключ строки — form.phones.keyAt(index): он держится за элемент и не меняется при перестановке.

import { FormField } from '@reformer/ui-kit';
import type { FormArray } from '@reformer/core';

function PhoneList({ phones }: { phones: FormArray<Phone> }) {
const length = useArrayLength(phones);

return (
<div>
{phones.map((phone, index) => (
<div key={phones.keyAt(index)} className="row">
<FormField control={phone.type} />
<FormField control={phone.number} />
<button type="button" onClick={() => phones.removeAt(index)}>
Удалить
</button>
</div>
))}

{length === 0 && <p>Телефонов пока нет</p>}

<button type="button" onClick={() => phones.push({ type: 'mobile', number: '' })}>
Добавить телефон
</button>
</div>
);
}
push принимает плоские значения

phones.push({ type, number }) — это значения элемента, а не узел схемы ({ model, component }). Компонент и его пропсы берутся из item узла массива в схеме. Без аргумента push() добавляет строку по шаблону модели (arrayOf(blank)).

Если строки переставляются (move / swap), подписывайтесь на массив целиком — useFormControl(phones): длина при перестановке не меняется. Готовые компоненты массива — FieldArraySection из @reformer/ui-kit и headless FieldArray из @reformer/cdk — делают это сами.


useFormFlow​

Снимок потока шагов (bundle.flow) для рендера: { current, index, completed, isFirst, isLast, validating }. Переходы — методами самого потока.

import { useFormFlow } from '@reformer/core/react';
import type { FormFlow } from '@reformer/core';

function StepFooter({ flow }: { flow: FormFlow }) {
const { current, isFirst, isLast, validating } = useFormFlow(flow);
return (
<div>
<button disabled={isFirst} onClick={() => flow.previous()}>
Назад
</button>
<button disabled={isLast || validating} onClick={() => void flow.next()}>
Далее ({current})
</button>
</div>
);
}

Подробнее о потоке — в FormWizard.


Отправка формы​

Правила валидации — отдельный слой от разметки: defineValidation(({ model }) => …) из @reformer/core/validation, переданная в сборку полем validation (у визарда — ещё и правила шагов flow). Отправка — объект сборки bundle.submission: bundle.submission.submit(handler) — один прогон всех правил; при замечаниях обработчик не вызывается, а вся форма помечается тронутой, при успехе — handler(model.get()).

import { createForm } from '@reformer/core';
import { useFormBundle } from '@reformer/core/react';
import { defineValidation, validate } from '@reformer/core/validation';
import { required, email } from '@reformer/core/validators';
import { FormField, Input, Button } from '@reformer/ui-kit';

type ContactForm = { name: string; email: string };

// Правила: отдельный слой, стабильная ссылка уровня модуля.
const contactRules = defineValidation<ContactForm>(({ model }) => {
validate(model.$.name, [required()]);
validate(model.$.email, [required(), email()]);
});

export function ContactFormView() {
const bundle = useFormBundle(() =>
createForm<ContactForm>({
initial: { name: '', email: '' },
// Разметка: привязка полей + компоненты. Валидаторов здесь НЕТ.
schema: (model) => ({
children: [
{ model: model.$.name, component: Input, componentProps: { label: 'Имя' } },
{
model: model.$.email,
component: Input,
componentProps: { label: 'Email', type: 'email' },
},
],
}),
validation: contactRules,
})
);

const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const outcome = await bundle.submission.submit((values) => api.send(values));
if (outcome.status === 'invalid') console.log('Проверьте форму');
};

return (
<form onSubmit={handleSubmit}>
<FormField control={bundle.form.name} />
<FormField control={bundle.form.email} />
<Button type="submit">Отправить</Button>
</form>
);
}
Источник вердикта — уровень формы

Вердикт на отправку даёт bundle.submission.submit (или bundle.validate() — проверка без отправки), данные — значения модели. Методов form.submit() / form.validate() у нод нет, как и bundle.submit у сборки. Правила с severity: 'warning' отправку не блокируют. Подробнее — Отправка и сброс.


Производительность​

Хуки чтения используют useSyncExternalStore и подписываются точечно, поэтому родительский компонент не перерисовывается при изменении отдельных полей.

function Form({ form }: { form: FormGroup<ContactForm> }) {
// Этот компонент не перерисовывается при изменении полей —
// перерисовывается только тот, кто подписан на конкретную ноду.
return (
<form>
<FormField control={form.name} /> {/* перерисовка только при смене name */}
<FormField control={form.email} /> {/* перерисовка только при смене email */}
</form>
);
}

Дальше​