Skip to main content

Реакции на изменения

Побочные эффекты в ответ на изменение поля: загрузка зависимых опций, аналитика, ревалидация. Основной оператор — onChange; низкоуровневый примитив — watchField; для перевалидации схемы — revalidateWhen.

onChange

onChange(source, cb, { debounce?, immediate? }) вызывает cb(value, { signal }) при изменении поля. Колбэк выполняется вне effect-контекста — в нём можно безопасно писать сигналы и ноды (updateComponentProps / reset) без «Cycle detected». Для async-колбэков вторым аргументом приходит { signal } (AbortSignal): при следующей смене значения предыдущий вызов аннулируется — передавай signal в fetch.

import { defineFormBehavior, onChange } from '@reformer/core/behaviors';

type AddressForm = { country: string; city: string };

const behavior = defineFormBehavior<AddressForm>(({ model, form }) => {
onChange(
model.$.country,
async (country, { signal }) => {
if (!country) {
form.city.updateComponentProps({ options: [] });
return;
}
try {
const cities = await fetchCities(country, { signal }); // отмена устаревших запросов
form.city.updateComponentProps({ options: cities });
} catch (error) {
if ((error as Error).name === 'AbortError') return;
form.city.updateComponentProps({ options: [] });
}
},
{ debounce: 300 } // не дёргать сеть на каждый keystroke
);
});

Опции

ОпцияНазначение
debounce: 300не вызывать колбэк на каждое изменение (300–500 мс для сети)
immediate: trueвызвать колбэк сразу при регистрации (по умолчанию false)

signal полезен не только для fetch — на нём можно чистить любой ресурс при следующей смене значения:

onChange(model.$.livePreview, (enabled, { signal }) => {
if (!enabled) return;
const interval = setInterval(refreshPreview, 1000);
// signal аннулируется на следующем изменении — чистим интервал
signal.addEventListener('abort', () => clearInterval(interval));
});

watchField

watchField(source, cb, { immediate? }) из @reformer/core — базовая подписка на изменение сигнала (без debounce и AbortSignal), поверх которой построен onChange. Это примитив: возвращает cleanup и вызывается императивно. Для простых синхронных реакций:

import { watchField } from '@reformer/core';

// вызывается при каждом изменении (по умолчанию НЕ на инициализации)
const stop = watchField(model.$.country, () => {
model.city = ''; // сброс зависимого поля
});
// stop() — отписаться
onChange vs watchField

onChange (DSL) — async-реакции с debounce и AbortSignal внутри defineFormBehavior. watchField (примитив) — простая синхронная подписка, когда нужна отписка вручную (например, в useEffect).

revalidateWhen

Валидация — по требованию (validateModel(model, schema)): снапшот модели прогоняется через схему на submit/шаге, а не реактивно. Поэтому если правило одного поля зависит от другого поля, изменение этого другого поля само по себе проверку не перезапустит. revalidateWhen(deps, revalidate) — мост поведение→валидация: вызывает колбэк ревалидации при изменении любой из зависимостей (не на инициализации). Само поведение валидацией не владеет — оно лишь инициирует внешний прогон validateModel.

import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
import { validateModel } from '@reformer/core/validation';
import { schema } from './validation'; // defineValidationSchema<RegistrationForm>(({ model }) => …)

type RegistrationForm = { password: string; confirmPassword: string };

const behavior = defineFormBehavior<RegistrationForm>(({ model }) => {
// при смене password перегоняем схему — cross-field правило confirmPassword перепроверится
revalidateWhen([model.$.password], () => {
void validateModel(model, schema); // ошибки сами доедут до нод, устаревший прогон отменится
});
});
Триггеры — ДРУГИЕ поля

В deps передавай поля, от которых зависит правило, а не само проверяемое поле (оно и так валидируется при собственном изменении). И убедись, что правило реально читает триггер — это cross-field правило cross(sig, fn), где fn читает снапшот всей модели (model.get()) и сравнивает поля между собой (см. Валидацию).

Дальше