Частые проблемы и их решения

Несоответствие структуры схемы данным формы

Одна из самых распространённых проблем возникает при несовпадении структуры объекта формы и схемы Yup.

Некорректный пример:

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

Форма:

useForm({
  resolver: yupResolver(schema),
  defaultValues: {
    email: ''
  }
})

Поле email находится в корне формы, тогда как схема ожидает его внутри user.

Правильный вариант:

useForm({
  resolver: yupResolver(schema),
  defaultValues: {
    user: {
      email: ''
    }
  }
})

Либо схема должна быть изменена:

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

Ошибка Cannot read property of undefined

Проблема часто появляется при вложенных объектах.

Пример:

const schema = yup.object({
  profile: yup.object({
    age: yup.number().required()
  })
})

Если profile отсутствует:

defaultValues: {}

Yup может выбрасывать ошибки при обращении к вложенным полям.

Решение:

defaultValues: {
  profile: {
    age: ''
  }
}

Дополнительно рекомендуется использовать .default():

const schema = yup.object({
  profile: yup.object({
    age: yup.number().required()
  }).default({})
})

Проблемы с обязательными полями

required() не работает для пустых строк

Многие ожидают, что поле автоматически отфильтрует строки из пробелов.

Пример:

yup.string().required()

Строка " " считается валидной.

Решение:

yup.string()
  .trim()
  .required('Поле обязательно')

Теперь пробелы будут удаляться перед проверкой.

required() и number()

Частая проблема:

yup.number().required()

Пустое поле формы обычно приходит как строка:

''

Yup пытается преобразовать значение в число и получает NaN.

Результат — сообщение о неправильном типе вместо сообщения об обязательности поля.

Решение:

yup.number()
  .typeError('Введите число')
  .required('Поле обязательно')

Дополнительно можно использовать transform():

yup.number()
  .transform((value, originalValue) => {
    return originalValue === '' ? undefined : value
  })
  .required()

Ошибки типов данных

Строка вместо числа

HTML input всегда возвращает строку.

Даже если используется:

<input type="number" />

значение всё равно приходит как строка.

Проблемный пример:

const schema = yup.object({
  age: yup.number()
})

Решение №1:

register('age', {
  valueAsNumber: true
})

Решение №2:

yup.number().transform(value => {
  return isNaN(value) ? undefined : value
})

Boolean-поля работают неправильно

Checkbox может возвращать:

'on'

или:

undefined

Некорректная схема:

yup.boolean().required()

Для checkbox лучше использовать:

yup.boolean().oneOf([true], 'Необходимо согласие')

Проблемы асинхронной валидации

Медленная проверка формы

Асинхронная проверка:

const schema = yup.object({
  email: yup.string().test(
    'unique-email',
    'Email уже существует',
    async (value) => {
      const response = await checkEmail(value)
      return response.ok
    }
  )
})

Если валидация запускается на каждый ввод символа:

mode: 'onChange'

возникает большое количество запросов.

Решения:

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

const debouncedCheck = debounce(checkEmail, 500)

Переключение режима проверки

useForm({
  mode: 'onBlur'
})

Кеширование запросов

const cache = new Map()

Ошибки с nullable и optional значениями

nullable() не делает поле необязательным

Неверное ожидание:

yup.string().nullable().required()

nullable() разрешает только null, но поле всё ещё обязательно.

Корректное использование:

yup.string().nullable().notRequired()

undefined и null обрабатываются по-разному

Пример:

yup.string().nullable()

Допускает:

null

Но не всегда корректно работает с:

undefined

Для полной гибкости:

yup.string().nullable().optional()

Проблемы transform()

transform() не срабатывает

Причина — неправильный порядок вызова методов.

Проблемный код:

yup.string()
  .required()
  .transform(value => value.trim())

required() может выполниться раньше transform.

Правильный вариант:

yup.string()
  .transform(value => value?.trim())
  .required()

transform() ломает типизацию

Опасный пример:

yup.number().transform(() => 'text')

После transform схема перестаёт соответствовать типу number.

Рекомендуется сохранять тип данных:

yup.number().transform(value => {
  return Number(value)
})

Ошибки при работе с массивами

Массив не валидируется

Проблемный код:

yup.array(
  yup.string().required()
)

Корректный вариант:

yup.array().of(
  yup.string().required()
)

Пустой массив проходит проверку

Пример:

yup.array().required()

Пустой массив [] считается валидным.

Решение:

yup.array()
  .min(1, 'Минимум один элемент')

Ошибки индексов массива

При удалении элементов индексы могут смещаться.

Пример:

users[2]

после удаления элемента:

users[1]

React Hook Form может сохранять старые ошибки.

Решение:

clearErrors('users')

или:

reset(getValues())

Проблемы conditional validation

when() не работает

Частая ошибка — отсутствие поля-зависимости.

Проблемный пример:

yup.object({
  password: yup.string(),
  confirmPassword: yup.string().when('password', {
    is: value => value.length > 0,
    then: schema => schema.required()
  })
})

Если password равен undefined, возникает ошибка.

Безопасный вариант:

is: value => !!value

Циклические зависимости

Ошибка:

fieldA -> fieldB
fieldB -> fieldA

Пример:

a: yup.string().when('b')
b: yup.string().when('a')

Yup может вызвать бесконечную рекурсию.

Решение — вынести логику в test():

yup.object({
  a: yup.string(),
  b: yup.string()
}).test(
  'cross-validation',
  'Одно из полей обязательно',
  values => {
    return !!values.a || !!values.b
  }
)

Проблемы производительности

Большие схемы тормозят интерфейс

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

Причины:

  • повторное создание схемы;
  • частая валидация;
  • сложные when() условия;
  • большое количество test().

Плохой пример:

const schema = yup.object({
  ...
})

внутри компонента React.

Решение:

const schema = useMemo(() => {
  return yup.object({
    ...
  })
}, [])

Частые перерендеры

Если используется:

mode: 'onChange'

валидация запускается при каждом символе.

Оптимизация:

useForm({
  mode: 'onBlur',
  reValidateMode: 'onSubmit'
})

Проблемы с TypeScript

Типы Yup и формы не совпадают

Пример:

type FormData = {
  age: number
}

Схема:

yup.object({
  age: yup.string()
})

TypeScript не сможет корректно сопоставить типы.

Решение:

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

type FormData = yup.InferType<typeof schema>

Потеря nullable типов

Проблемный пример:

yup.string().nullable()

TypeScript иногда выводит:

string

Решение:

type FormData = yup.Asserts<typeof schema>

или явное указание типа:

const schema: yup.ObjectSchema<{
  name: string | null
}>

Проблемы интеграции с React Hook Form

Ошибки не отображаются

Причина — неправильное имя поля.

Форма:

register('user.email')

Ошибка проверяется так:

errors.email

Правильно:

errors.user?.email

Resolver не подключён

Частая ошибка:

useForm({
  schema
})

Корректно:

useForm({
  resolver: yupResolver(schema)
})

reset() не очищает ошибки

После reset ошибки могут оставаться.

Решение:

reset({}, {
  keepErrors: false
})

или:

clearErrors()

Ошибки кастомных test()

test() не возвращает boolean

Некорректный пример:

test(value => {
  if (value === 'admin') {
    return
  }
})

Всегда необходимо возвращать значение:

test(value => {
  return value !== 'admin'
})

Потеря контекста this

Нельзя использовать стрелочную функцию, если нужен this.

Проблемный код:

test(
  'custom',
  'Ошибка',
  (value) => {
    return this.parent
  }
)

Правильно:

test(
  'custom',
  'Ошибка',
  function(value) {
    return this.parent
  }
)

Проблемы локализации сообщений

Сообщения ошибок дублируются

Плохой пример:

yup.string().required('Required')

во всех схемах.

Решение — централизованная локализация:

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

Локализация не применяется

Причина — setLocale() вызывается после создания схем.

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

const schema = yup.object({...})

yup.setLocale({...})

Правильно:

yup.setLocale({...})

const schema = yup.object({...})

Ошибки сериализации данных

Date превращается в string

После отправки формы:

JSON.stringify(data)

объект Date становится строкой.

Решение:

const parsed = {
  ...data,
  date: new Date(data.date)
}

Потеря undefined значений

JSON удаляет:

undefined

Если сервер ожидает поле:

null

необходимо преобразование:

Object.entries(data).reduce((acc, [key, value]) => {
  acc[key] = value === undefined ? null : value
  return acc
}, {})

Диагностика проблем YupResolver

Логирование ошибок

Полезный приём:

const onEr ror = (errors) => {
  console.log(errors)
}

Проверка схемы отдельно от формы

schema.validate(data)

или:

schema.validateSync(data)

Получение всех ошибок

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

Для полного списка:

schema.validate(data, {
  abortEarly: false
})

Анализ результата resolver

const resolver = yupResolver(schema)

const result = await resolver(data)

console.log(result)

Позволяет увидеть:

  • values;
  • errors;
  • трансформации;
  • итоговые данные после валидации.