Интеграция Vest с React часто строится вокруг разделения ответственности между состоянием формы и логикой валидации. React управляет рендерингом и жизненным циклом компонентов, тогда как Vest отвечает за декларативное описание правил проверки и выполнение тестов с поддержкой синхронной и асинхронной логики.
Основная сложность связывания этих двух слоёв заключается в необходимости синхронизации результатов валидации с реактивным состоянием интерфейса без избыточных перерендеров. Vest предоставляет детализированный результат выполнения suite, включающий структуру ошибок по полям, что позволяет напрямую маппировать его на состояние формы.
Типичная схема интеграции строится вокруг трёх элементов:
useState или внешние
менеджеры состояния)create,
test, group, skipWhen)Состояние формы хранит значения полей, тогда как результаты Vest не сохраняются постоянно, а пересчитываются при изменении данных.
import { create, test, enforce } from 'vest';
const loginSuite = create((data = {}) => {
test('email', 'Некорректный email', () => {
enforce(data.email).isEmail();
});
test('password', 'Пароль слишком короткий', () => {
enforce(data.password).longerThan(5);
});
});
Suite остаётся чистой функцией, зависящей только от входных данных. Это позволяет легко вызывать её из React-компонентов без побочных эффектов.
На уровне компонента форма обычно представлена через
useState, где хранится объект значений полей.
import React, { useState } from 'react';
import { loginSuite } from './validation';
function LoginForm() {
const [values, setValues] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const validate = (nextValues) => {
const result = loginSuite(nextValues);
setErrors(result.getErrors());
};
const onCha nge = (field) => (e) => {
const nextValues = {
...values,
[field]: e.target.value
};
setValues(nextValues);
validate(nextValues);
};
return (
<form>
<input value={values.email} onCha nge={onChange('email')} />
<input value={values.password} onCha nge={onChange('password')} />
</form>
);
}
Результаты getErrors() представляют собой плоскую или
структурированную коллекцию ошибок, которую удобно привязывать к
конкретным полям интерфейса.
Vest возвращает результаты, оптимизированные для обработки, но в React часто требуется нормализация под формат:
{
email: 'Некорректный email',
password: 'Пароль слишком короткий'
}
Функция преобразования может учитывать приоритет ошибок и группировку тестов.
const mapErrors = (result) => {
return result.tests.reduce((acc, test) => {
if (test.errors.length) {
acc[test.fieldName] = test.errors[0];
}
return acc;
}, {});
};
Такой подход обеспечивает стабильную привязку UI к результатам валидации без необходимости хранить внутреннюю структуру suite.
При прямом вызове suite на каждое изменение input возникает риск избыточных вычислений. React-компоненты могут ререндериться слишком часто, особенно в формах с большим количеством полей.
Распространённый подход — разделение событий onChange и
onBlur:
onChange обновляет состояние без полной валидацииonBlur инициирует проверку конкретного поля или всей
формыconst validateField = (field, values) => {
const result = loginSuite(values);
const errors = mapErrors(result);
return errors[field];
};
Валидация всего набора данных используется при сабмите формы, тогда как частичная — для интерактивной обратной связи.
Повторяющаяся логика взаимодействия с Vest часто выносится в абстракцию, основанную на кастомном хуке.
import { useState } from 'react';
export function useVestValidation(suite, initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const validate = (nextValues) => {
const result = suite(nextValues);
setErrors(mapErrors(result));
};
const setField = (field, value) => {
const nextValues = { ...values, [field]: value };
setValues(nextValues);
validate(nextValues);
};
return {
values,
errors,
setField
};
}
Подобная инкапсуляция уменьшает связность компонента формы и делает логику валидации переиспользуемой.
Vest поддерживает асинхронные проверки, что важно для сценариев проверки уникальности email, логина или внешних данных API.
import { create, test, enforce } from 'vest';
const asyncSuite = create(async (data = {}) => {
test('email', 'Email уже занят', async () => {
enforce(data.email).isNotEmpty();
const res = await fetch(`/api/check-email?email=${data.email}`);
const { exists } = await res.json();
enforce(exists).equals(false);
});
});
В React это требует обработки состояния загрузки, так как результат становится доступным не мгновенно.
const validate = async (values) => {
const result = await asyncSuite(values);
setErrors(mapErrors(result));
};
Дополнительно вводится состояние isValidating,
позволяющее контролировать UI-индикацию процесса проверки.
Каждое поле формы становится контролируемым компонентом, где значение берётся из состояния, а изменения передаются через единый обработчик.
<input
value={values.email}
onCha nge={(e) => setField('email', e.target.value)}
/>
<span>{errors.email}</span>
Такой подход позволяет синхронизировать UI с результатами Vest без промежуточных слоёв.
При росте формы suite может становиться громоздким. Для улучшения структуры используется разделение логики:
const emailRules = (data) => {
test('email', 'Некорректный email', () => {
enforce(data.email).isEmail();
});
};
const passwordRules = (data) => {
test('password', 'Слишком короткий пароль', () => {
enforce(data.password).longerThan(5);
});
};
const loginSuite = create((data) => {
emailRules(data);
passwordRules(data);
});
Модульность облегчает поддержку и повторное использование правил в разных формах.
В формах с большим количеством полей полная перезапись suite может быть затратной. Используется стратегия частичной проверки:
const dirtyFields = new Set();
const onCha nge = (field, value) => {
dirtyFields.add(field);
setField(field, value);
const result = suite(values);
const filteredErrors = filterErrors(result, dirtyFields);
setErrors(filteredErrors);
};
При использовании TypeScript типизация состояния и результатов Vest повышает предсказуемость интеграции.
type FormValues = {
email: string;
password: string;
};
type FormErrors = Partial<Record<keyof FormValues, string>>;
Suite при этом остаётся типобезопасным при корректной настройке входных данных.
В крупных приложениях React часто используется совместно с глобальными стореджами. Vest интегрируется через подписку на изменения состояния:
Принцип остаётся одинаковым: suite вызывается при изменении store, результат маппится в структуру ошибок и сохраняется обратно в состояние UI.
Важным аспектом является контроль частоты вызовов suite. В React это
решается через useEffect, зависящий от значений формы.
useEffect(() => {
const result = loginSuite(values);
setErrors(mapErrors(result));
}, [values]);
При этом возможна дополнительная оптимизация через мемоизацию значений или дебаунсинг, чтобы снизить нагрузку при быстрых изменениях input.
Vest может использоваться поверх специализированных решений для форм, таких как React Hook Form или Formik. В таком случае Vest не управляет состоянием, а выступает исключительно слоем валидации.
const validate = (values) => {
const result = loginSuite(values);
return mapErrors(result);
};
Форма получает только результат проверки, тогда как управление значениями остаётся за библиотекой форм.
Для сложных бизнес-логик используется композиция условий внутри suite:
test('password', 'Пароль не соответствует требованиям', () => {
enforce(data.password)
.longerThan(8)
.matches(/[A-Z]/)
.matches(/[0-9]/);
});
Такая декларативная модель хорошо сочетается с React, так как не требует императивного управления состоянием валидации и легко масштабируется при росте формы.