Интеграция с React

Интеграция Vest с React часто строится вокруг разделения ответственности между состоянием формы и логикой валидации. React управляет рендерингом и жизненным циклом компонентов, тогда как Vest отвечает за декларативное описание правил проверки и выполнение тестов с поддержкой синхронной и асинхронной логики.

Основная сложность связывания этих двух слоёв заключается в необходимости синхронизации результатов валидации с реактивным состоянием интерфейса без избыточных перерендеров. Vest предоставляет детализированный результат выполнения suite, включающий структуру ошибок по полям, что позволяет напрямую маппировать его на состояние формы.

Типичная схема интеграции строится вокруг трёх элементов:

  • локальное состояние формы в React (useState или внешние менеджеры состояния)
  • suite валидации, описанный через Vest (create, test, group, skipWhen)
  • функция трансформации результатов валидации в структуру ошибок UI

Состояние формы хранит значения полей, тогда как результаты 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-компонентов без побочных эффектов.

Связка с состоянием 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() представляют собой плоскую или структурированную коллекцию ошибок, которую удобно привязывать к конкретным полям интерфейса.

Структурирование ошибок для UI

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
  };
}

Подобная инкапсуляция уменьшает связность компонента формы и делает логику валидации переиспользуемой.

Асинхронная валидация в React

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 по полям

При росте формы 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 может быть затратной. Используется стратегия частичной проверки:

  • хранение dirty-state полей
  • проверка только изменённых полей
  • кэширование результатов предыдущих вычислений
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

При использовании TypeScript типизация состояния и результатов Vest повышает предсказуемость интеграции.

type FormValues = {
  email: string;
  password: string;
};

type FormErrors = Partial<Record<keyof FormValues, string>>;

Suite при этом остаётся типобезопасным при корректной настройке входных данных.

Синхронизация с внешними менеджерами состояния

В крупных приложениях React часто используется совместно с глобальными стореджами. Vest интегрируется через подписку на изменения состояния:

  • Redux
  • Zustand
  • Recoil

Принцип остаётся одинаковым: 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, так как не требует императивного управления состоянием валидации и легко масштабируется при росте формы.