Плагины для Yup

YupResolver — адаптер между библиотекой валидации Yup и формами, чаще всего используемый вместе с React Hook Form. Основная задача резолвера — преобразование схемы Yup в механизм проверки данных формы.

На практике YupResolver:

  • получает значения формы;
  • передаёт их в схему Yup;
  • перехватывает ошибки;
  • преобразует ошибки в формат, понятный библиотеке форм;
  • возвращает результат валидации.

Наиболее распространённая реализация подключается через пакет:

npm install yup @hookform/resolvers

Импорт:

import { yupResolver } from "@hookform/resolvers/yup";

Подключение YupResolver к React Hook Form

Базовая интеграция:

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

const schema = yup.object({
  email: yup
    .string()
    .email("Некорректный email")
    .required("Поле обязательно"),

  password: yup
    .string()
    .min(6, "Минимум 6 символов")
    .required("Введите пароль")
});

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm({
    resolver: yupResolver(schema)
  });

  const onSub mit = data => {
    console.log(data);
  };

  return (
    <form onSub mit={handleSubmit(onSubmit)}>
      <input {...register("email")} />
      <p>{errors.email?.message}</p>

      <input type="password" {...register("password")} />
      <p>{errors.password?.message}</p>

      <button type="submit">Отправить</button>
    </form>
  );
}

Принцип работы резолвера

Внутри yupResolver происходит вызов:

schema.validate(data, options)

Если проверка успешна:

{
  values: validatedData,
  errors: {}
}

Если есть ошибки:

{
  values: {},
  errors: {
    email: {
      type: "required",
      message: "Поле обязательно"
    }
  }
}

Именно такой формат ожидает React Hook Form.


Настройка режима валидации

React Hook Form позволяет определять момент запуска YupResolver.

Проверка при отправке

useForm({
  resolver: yupResolver(schema),
  mode: "onSubmit"
});

Проверка при изменении

useForm({
  resolver: yupResolver(schema),
  mode: "onChange"
});

Проверка при потере фокуса

useForm({
  resolver: yupResolver(schema),
  mode: "onBlur"
});

Все режимы

Режим Описание
onSubmit Проверка после submit
onChange Проверка при вводе
onBlur Проверка после blur
all Проверка и при blur, и при change
onTouched После первого касания поля

Передача опций в YupResolver

Резолвер поддерживает дополнительные настройки.

abortEarly

По умолчанию Yup завершает проверку после первой ошибки.

resolver: yupResolver(schema, {
  abortEarly: false
})

Теперь ошибки собираются полностью.

Пример:

const schema = yup.object({
  username: yup
    .string()
    .required("Введите имя")
    .min(5, "Минимум 5 символов")
});

Без abortEarly: false пользователь увидит только одну ошибку.


stripUnknown

Удаление лишних полей:

resolver: yupResolver(schema, {
  stripUnknown: true
})

Пример:

const schema = yup.object({
  name: yup.string()
});

const data = {
  name: "Alex",
  role: "admin"
};

После валидации:

{
  name: "Alex"
}

strict mode

Отключение автоматических преобразований.

resolver: yupResolver(schema, {
  strict: true
})

Без strict

yup.number().validateSync("25");

Результат:

25

Со strict

yup.number().strict().validateSync("25");

Ошибка:

this must be a `number` type

Вложенные объекты

YupResolver корректно работает со сложными структурами.

Схема

const schema = yup.object({
  profile: yup.object({
    firstName: yup.string().required(),
    lastName: yup.string().required()
  })
});

Регистрация полей

<input {...register("profile.firstName")} />
<input {...register("profile.lastName")} />

Ошибки

errors.profile?.firstName?.message

Работа с массивами

Валидация массива объектов

const schema = yup.object({
  users: yup.array().of(
    yup.object({
      name: yup.string().required(),
      age: yup.number().required()
    })
  )
});

Регистрация:

<input {...register("users.0.name")} />
<input {...register("users.0.age")} />

useFieldArray и YupResolver

Типичная интеграция динамических полей:

import { useFieldArray } from "react-hook-form";

const {
  control,
  register
} = useForm({
  resolver: yupResolver(schema)
});

const { fields, append } = useFieldArray({
  control,
  name: "skills"
});

Схема:

const schema = yup.object({
  skills: yup.array().of(
    yup.object({
      title: yup.string().required()
    })
  )
});

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

YupResolver полностью поддерживает when.

const schema = yup.object({
  isCompany: yup.boolean(),

  companyName: yup.string().when("isCompany", {
    is: true,
    then: schema => schema.required("Введите название компании"),
    otherwise: schema => schema.notRequired()
  })
});

Кастомные проверки

test()

const schema = yup.object({
  password: yup
    .string()
    .test(
      "has-uppercase",
      "Нужна заглавная буква",
      value => /[A-Z]/.test(value)
    )
});

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

YupResolver умеет работать с async-проверками.

const schema = yup.object({
  username: yup.string().test(
    "checkUsername",
    "Имя уже занято",
    async value => {
      const response = await fetch(`/api/users/${value}`);

      return response.status === 404;
    }
  )
});

Контекст в YupResolver

Передача внешних данных:

useForm({
  resolver: yupResolver(schema),
  context: {
    minAge: 18
  }
});

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

const schema = yup.object({
  age: yup.number().test(
    "min-age",
    "Возраст слишком маленький",
    function(value) {
      return value >= this.options.context.minAge;
    }
  )
});

Трансформация данных

Yup умеет преобразовывать данные до возврата результата.

trim

yup.string().trim()

lowercase

yup.string().lowercase()

transform

const schema = yup.object({
  price: yup.number().transform((value, originalValue) => {
    return Number(originalValue.replace(",", "."));
  })
});

Обработка nullable значений

nullable()

yup.string().nullable()

Допускается:

null

optional и required

required

yup.string().required()

optional

yup.string().optional()

Валидация дат

const schema = yup.object({
  startDate: yup.date().required(),

  endDate: yup
    .date()
    .min(
      yup.ref("startDate"),
      "Дата окончания меньше даты начала"
    )
});

Валидация чисел

yup.number()
  .min(0)
  .max(100)
  .integer()
  .positive()

Валидация строк

yup.string()
  .min(2)
  .max(30)
  .matches(/^[A-Za-z]+$/)

Email и URL

yup.string().email()
yup.string().url()

Валидация пароля

const schema = yup.object({
  password: yup
    .string()
    .min(8)
    .matches(/[A-Z]/, "Нужна заглавная буква")
    .matches(/[0-9]/, "Нужна цифра")
    .matches(/[!@#$%^&*]/, "Нужен спецсимвол")
});

Подтверждение пароля

const schema = yup.object({
  password: yup.string().required(),

  confirmPassword: yup
    .string()
    .oneOf(
      [yup.ref("password")],
      "Пароли не совпадают"
    )
});

Lazy schemas

Динамические схемы:

const schema = yup.lazy(value => {
  if (typeof value === "string") {
    return yup.string();
  }

  return yup.object();
});

Комбинирование схем

concat

const baseSchema = yup.object({
  email: yup.string().required()
});

const profileSchema = yup.object({
  age: yup.number().required()
});

const schema = baseSchema.concat(profileSchema);

Кастомный resolver

Иногда требуется собственная логика.

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

    return {
      values,
      errors: {}
    };

  } catch (error) {
    return {
      values: {},
      errors: error.inner.reduce((allErrors, currentError) => {
        return {
          ...allErrors,
          [currentError.path]: {
            type: currentError.type ?? "validation",
            message: currentError.message
          }
        };
      }, {})
    };
  }
};

TypeScript и YupResolver

Типизация формы:

import * as yup from "yup";

const schema = yup.object({
  email: yup.string().required(),
  age: yup.number().required()
});

type FormData = yup.InferType<typeof schema>;

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

const {
  register
} = useForm<FormData>({
  resolver: yupResolver(schema)
});

Resolver и defaultValues

useForm({
  resolver: yupResolver(schema),

  defaultValues: {
    email: "",
    age: 18
  }
});

reset и YupResolver

const {
  reset
} = useForm({
  resolver: yupResolver(schema)
});

reset({
  email: "",
  password: ""
});

trigger для ручной проверки

const {
  trigger
} = useForm({
  resolver: yupResolver(schema)
});

await trigger();

Проверка конкретного поля:

await trigger("email");

setError и серверные ошибки

const {
  setError
} = useForm();

Пример:

setError("email", {
  type: "server",
  message: "Email уже существует"
});

Очистка ошибок

clearErrors();

Для одного поля:

clearErrors("email");

Валидация checkbox

const schema = yup.object({
  agree: yup
    .boolean()
    .oneOf([true], "Необходимо согласие")
});

Валидация select

const schema = yup.object({
  country: yup.string().required()
});

enum-подобные значения

const schema = yup.object({
  role: yup.string().oneOf([
    "admin",
    "user",
    "moderator"
  ])
});

nullable number

yup.number()
  .nullable()
  .transform((value, originalValue) => {
    return originalValue === ""
      ? null
      : value;
  });

Валидация файлов

const schema = yup.object({
  avatar: yup
    .mixed()
    .test(
      "fileSize",
      "Файл слишком большой",
      value => {
        if (!value?.length) {
          return true;
        }

        return value[0].size <= 1024 * 1024;
      }
    )
});

Локализация сообщений

Глобальная настройка:

yup.setLocale({
  mixed: {
    required: "Поле обязательно"
  },

  string: {
    email: "Некорректный email"
  }
});

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

Основные причины деградации производительности:

  • слишком большие схемы;
  • глубокие вложенности;
  • постоянное пересоздание схем;
  • режим onChange;
  • большое количество async-проверок.

Оптимизация

Вынос схемы за компонент

Плохо:

function App() {
  const schema = yup.object({
    email: yup.string().required()
  });
}

Хорошо:

const schema = yup.object({
  email: yup.string().required()
});

function App() {

}

memoization схем

const schema = useMemo(() => {
  return yup.object({
    email: yup.string().required()
  });
}, []);

Разделение больших схем

const authSchema = yup.object({
  email: yup.string().required()
});

const profileSchema = yup.object({
  age: yup.number().required()
});

Типичные ошибки

Неправильный импорт

Неправильно:

import yupResolver from "@hookform/resolvers/yup";

Правильно:

import { yupResolver } from "@hookform/resolvers/yup";

Отсутствует resolver

useForm({
  resolver: yupResolver(schema)
});

Несовпадение структуры

Плохо:

register("user.name")
yup.object({
  username: yup.string()
})

Хорошо:

yup.object({
  user: yup.object({
    name: yup.string()
  })
})

Сравнение YupResolver и других resolver-плагинов

Resolver Библиотека Особенности
yupResolver Yup Простота и популярность
zodResolver Zod Отличная TypeScript-интеграция
joiResolver Joi Мощная серверная валидация
ajvResolver AJV JSON Schema
vestResolver Vest Unit-test подход

Когда YupResolver подходит лучше всего

Наиболее удачные сценарии:

  • формы среднего размера;
  • CRUD-интерфейсы;
  • административные панели;
  • регистрация и авторизация;
  • много условной логики;
  • проекты без жёсткой типизации TypeScript;
  • приложения с уже существующим стеком Yup.

Когда стоит рассмотреть альтернативы

Zod часто оказывается удобнее при:

  • heavy TypeScript;
  • строгой типизации API;
  • schema-first архитектуре;
  • генерации типов;
  • tRPC-проектах.

AJV лучше подходит:

  • для JSON Schema;
  • OpenAPI;
  • высоконагруженной серверной валидации.

Joi чаще используется:

  • на backend;
  • в Node.js API;
  • валидации конфигураций.