Использование с Final Form

Библиотека Final Form управляет состоянием формы через подписочную модель, где каждое поле регистрируется как отдельная сущность с собственным жизненным циклом значений, ошибок и состояния взаимодействия. Yup используется как декларативный валидатор схем, позволяющий описывать структуру данных формы и правила проверки через единый объект схемы.

Final Form ожидает функцию валидации, которая возвращает объект ошибок с ключами, совпадающими с именами полей. Yup, в свою очередь, работает со схемой и возвращает либо валидированные данные, либо структурированное исключение ValidationError. Связка формируется через адаптер, преобразующий ошибки Yup в формат Final Form.

Ключевая особенность интеграции заключается в том, что Yup выполняет всю валидацию централизованно, а Final Form распределяет результат по конкретным полям.

Базовая схема Yup для формы

Схема описывает структуру данных и правила проверки:

import * as Yup from 'yup';

const schema = Yup.object({
  email: Yup.string()
    .email('Некорректный email')
    .required('Email обязателен'),

  password: Yup.string()
    .min(8, 'Минимум 8 символов')
    .required('Пароль обязателен')
});

Схема отражает структуру состояния формы и используется как единый источник истины для валидации.

Адаптация Yup под Final Form

Final Form требует функцию вида (values) => errors. Yup возвращает исключение, поэтому требуется преобразование:

const validate = async (values) => {
  try {
    await schema.validate(values, { abortEarly: false });
    return {};
  } catch (err) {
    const errors = {};

    if (err.inner) {
      err.inner.forEach((error) => {
        if (!errors[error.path]) {
          errors[error.path] = error.message;
        }
      });
    }

    return errors;
  }
};

Параметр abortEarly: false необходим для накопления всех ошибок за одну проверку, иначе возвращается только первая найденная ошибка.

Подключение к Final Form

Final Form принимает функцию валидации при создании формы:

import { createForm } from 'final-form';

const form = createForm({
  onSubmit: (values) => {
    console.log(values);
  },
  validate
});

Каждое изменение значения запускает проверку, результат которой синхронизируется с состоянием формы.

Сопоставление вложенных структур

Yup поддерживает вложенные объекты и массивы, Final Form требует точного соответствия путей ошибок.

const schema = Yup.object({
  user: Yup.object({
    name: Yup.string().required('Имя обязательно'),
    contacts: Yup.array().of(
      Yup.object({
        phone: Yup.string().required('Телефон обязателен')
      })
    )
  })
});

Ошибки должны возвращаться в формате:

{
  "user.name": "Имя обязательно",
  "user.contacts[0].phone": "Телефон обязателен"
}

Для корректного преобразования используется рекурсивная обработка error.path.

Рекурсивное преобразование ошибок Yup

Структурирование ошибок для Final Form требует нормализации путей:

const formatYupErrors = (yupError) => {
  const errors = {};

  yupError.inner.forEach((err) => {
    const path = err.path;
    if (!errors[path]) {
      errors[path] = err.message;
    }
  });

  return errors;
};

В сложных формах с массивами требуется расширение логики для поддержки индексов.

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

Final Form поддерживает асинхронную валидацию, что позволяет использовать Yup с серверными проверками:

const validate = async (values) => {
  try {
    await schema.validate(values, { abortEarly: false });

    const response = await fetch('/api/check', {
      method: 'POST',
      body: JSON.stringify(values)
    });

    const data = await response.json();

    if (!data.ok) {
      return {
        email: 'Email уже используется'
      };
    }

    return {};
  } catch (err) {
    return formatYupErrors(err);
  }
};

Асинхронная логика выполняется последовательно: сначала схема, затем серверные ограничения.

Условная валидация

Yup поддерживает динамические правила через when, что особенно полезно в формах с зависимыми полями:

const schema = Yup.object({
  password: Yup.string().required(),
  confirmPassword: Yup.string().when('password', (password, field) =>
    password ? field.required('Подтверждение обязательно') : field
  )
});

Final Form передает актуальные значения, что позволяет Yup пересчитывать условия на лету.

Частичная валидация и производительность

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

const fieldSchemas = {
  email: Yup.string().email().required(),
  password: Yup.string().min(8)
};

const validate = (values) => {
  const errors = {};

  Object.keys(fieldSchemas).forEach((field) => {
    try {
      fieldSchemas[field].validateSync(values[field]);
    } catch (e) {
      errors[field] = e.message;
    }
  });

  return errors;
};

Такой подход снижает нагрузку при частых обновлениях состояния формы.

Работа с массивами значений

Final Form использует индексированные пути, Yup описывает структуру через array().of():

const schema = Yup.object({
  tags: Yup.array()
    .of(Yup.string().required('Значение обязательно'))
    .min(1, 'Минимум один элемент')
});

Ошибки для массивов должны учитывать позицию элемента:

{
  "tags[2]": "Значение обязательно"
}

Синхронизация состояния ошибок

Final Form хранит ошибки в виде плоского объекта, обновляемого после каждой валидации. Yup генерирует иерархическую структуру ошибок, которая требует нормализации.

Механизм обновления включает:

  • запуск validate при изменении поля
  • пересборку всего объекта ошибок
  • частичное обновление состояния формы

Кастомные валидаторы в связке

Yup допускает расширение через test:

const schema = Yup.object({
  username: Yup.string().test(
    'unique',
    'Имя занято',
    async (value) => {
      const res = await fetch(`/check?username=${value}`);
      const data = await res.json();
      return data.available;
    }
  )
});

Такие проверки становятся частью общей цепочки валидации Final Form.

Интеграция с типизированными формами

При использовании TypeScript схема Yup может быть источником типов:

import * as Yup from 'yup';

const schema = Yup.object({
  age: Yup.number().required()
});

type FormValues = Yup.InferType<typeof schema>;

Final Form получает строго типизированное состояние, что снижает количество ошибок на этапе разработки.

Обработка submit-потока

Final Form вызывает validate перед onSubmit. При наличии ошибок onSubmit не выполняется.

Последовательность:

  1. изменение значения
  2. запуск validate
  3. обновление ошибок
  4. проверка состояния формы
  5. вызов onSubmit при отсутствии ошибок

Кросс-полевые зависимости

Yup позволяет описывать взаимосвязи между полями:

const schema = Yup.object({
  startDate: Yup.date(),
  endDate: Yup.date().min(
    Yup.ref('startDate'),
    'Дата окончания раньше начала'
  )
});

Final Form передает актуальные значения всей формы, что обеспечивает корректную проверку зависимостей.

Нормализация значений перед валидацией

Final Form поддерживает parse и format, которые изменяют данные до передачи в Yup:

<Field
  name="age"
  parse={(value) => Number(value)}
  format={(value) => String(value)}
/>

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