Joi resolver

Установка и подключение связки с использованием резолвера для схем валидации на основе Joi строится вокруг интеграции с React Hook Form через пакет адаптеров резолверов, предоставляемый экосистемой @hookform/resolvers.

Механизм Joi resolver представляет собой функцию-обёртку, преобразующую синтаксис схемы Joi в формат, совместимый с внутренним контрактом React Hook Form. Основная задача заключается в унификации результатов валидации: ошибок, значений и метаданных формы.


React Hook Form использует ленивую модель регистрации полей и минимальное количество перерисовок. В этой модели валидация выносится за пределы ядра и подключается через резолверы.

Joi resolver выполняет три ключевые функции:

  • преобразует входные данные формы в структуру, ожидаемую Joi-схемой;
  • выполняет синхронную или асинхронную валидацию через Joi;
  • нормализует ошибки в формат, совместимый с React Hook Form (FieldErrors).

Ключевой принцип заключается в том, что React Hook Form не зависит от Joi напрямую. Связь реализуется через адаптер.


Базовая установка и подключение

Экосистема требует наличия трёх основных компонентов:

  • React Hook Form
  • Joi
  • пакет @hookform/resolvers

Установка:

npm install react-hook-form joi @hookform/resolvers

Простейшая схема Joi

Joi строит схемы декларативно, описывая ограничения на уровне типов и правил.

import Joi from "joi";

const schema = Joi.object({
  email: Joi.string().email({ tlds: false }).required(),
  password: Joi.string().min(8).max(32).required()
});

Каждое поле описывается цепочкой методов:

  • string() — тип строки
  • email() — проверка email формата
  • min() и max() — ограничения длины
  • required() — обязательность поля

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

Интеграция осуществляется через функцию joiResolver.

import { useForm } from "react-hook-form";
import { joiResolver } from "@hookform/resolvers/joi";
import Joi from "joi";

const schema = Joi.object({
  email: Joi.string().email({ tlds: false }).required(),
  password: Joi.string().min(8).required()
});

const form = useForm({
  resolver: joiResolver(schema)
});

В этом случае вся валидация формы делегируется Joi, а React Hook Form получает стандартизированные результаты.


Модель данных и обработка ошибок

Joi возвращает объект ошибки, содержащий массив деталей (details). Каждая ошибка имеет:

  • путь (path) к полю
  • сообщение (message)
  • тип ошибки (type)

Joi resolver преобразует это в структуру:

{
  fieldName: {
    type: "validation_type",
    message: "Описание ошибки"
  }
}

Пример поведения:

const schema = Joi.object({
  username: Joi.string().alphanum().min(3).required()
});

При вводе ab будет сформирована ошибка:

  • путь: username
  • сообщение: "length must be at least 3 characters long"

Подключение через register

React Hook Form использует регистрацию полей без controlled state:

function Form() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: joiResolver(schema)
  });

  return (
    <form onSub mit={handleSubmit(data => console.log(data))}>
      <input {...register("email")} />
      {errors.email && <p>{errors.email.message}</p>}
    </form>
  );
}

Здесь Joi resolver выполняется при сабмите или при изменении (в зависимости от режима mode).


Режимы валидации

React Hook Form поддерживает несколько стратегий:

  • onSubmit — проверка при отправке формы
  • onBlur — проверка при потере фокуса
  • onChange — проверка при вводе
  • all — комбинация

Пример:

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

Joi resolver при этом вызывается в соответствии с жизненным циклом формы.


Работа с вложенными объектами

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

const schema = Joi.object({
  user: Joi.object({
    name: Joi.string().required(),
    age: Joi.number().min(18)
  })
});

React Hook Form отражает это через точечную нотацию:

register("user.name");
register("user.age");

Ошибки возвращаются в аналогичной структуре:

errors.user?.name?.message

Массивы и динамические поля

Joi позволяет описывать массивы через array():

const schema = Joi.object({
  tags: Joi.array().items(Joi.string().min(2))
});

В React Hook Form используется useFieldArray:

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

Joi resolver обрабатывает каждый элемент массива отдельно и агрегирует ошибки по индексам.


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

Joi поддерживает асинхронные правила через кастомные валидаторы:

const schema = Joi.object({
  username: Joi.string().external(async (value) => {
    const exists = await checkUsername(value);
    if (exists) throw new Error("Username already exists");
  })
});

Joi resolver ожидает завершения Promise и передаёт результат обратно в React Hook Form.


Производительность и стратегия валидации

Joi resolver выполняет полную проверку схемы при каждом вызове. Это важно учитывать при:

  • больших формах (100+ полей)
  • сложных вложенных структурах
  • частых событиях onChange

Оптимизация достигается через:

  • режим onBlur
  • мемоизацию схемы через useMemo
  • разделение схем на подформы

Пример:

const schema = useMemo(() => Joi.object({...}), []);

Кастомные сообщения ошибок

Joi позволяет централизованно задавать сообщения:

const schema = Joi.object({
  email: Joi.string()
    .email()
    .messages({
      "string.email": "Некорректный email",
      "string.empty": "Email обязателен"
    })
});

Joi resolver сохраняет эти сообщения без изменений, что делает возможной унификацию UX-логики.


TypeScript и типизация

В связке с TypeScript типизация формы выводится из схемы через дженерики:

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

const schema: Joi.ObjectSchema<FormValues> = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required()
});

React Hook Form:

const { register } = useForm<FormValues>({
  resolver: joiResolver(schema)
});

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


Сравнение с другими резолверами

В экосистеме резолверов существуют альтернативы:

  • Yup resolver
  • Zod resolver
  • Valibot resolver

Особенности Joi resolver:

  • строгая серверная модель валидации
  • развитая система правил
  • гибкая обработка кастомных проверок
  • высокая выразительность схем

При этом Joi чаще используется в backend-ориентированных архитектурах, что делает резолвер полезным в унифицированных full-stack приложениях.


Обработка сложных ошибок и кастомных трансформаций

Joi позволяет трансформировать данные до валидации:

const schema = Joi.object({
  email: Joi.string().trim().lowercase().email().required()
});

В этом случае resolver получает уже нормализованное значение, что снижает нагрузку на UI-логику.

Также возможно использование alter() для динамического изменения схем:

const schema = Joi.object({
  password: Joi.string().min(8)
}).tailor("signup");

Интеграция с бизнес-логикой

Joi resolver часто используется как единый слой валидации, синхронизирующий frontend и backend.

Типичный сценарий:

  • схема Joi используется на сервере (Node.js)
  • та же схема подключается в React через resolver
  • правила валидации не дублируются

Это уменьшает риск расхождения логики между слоями приложения.


Поведение при частично валидных данных

При частичной валидации Joi resolver возвращает все найденные ошибки, не прерывая выполнение на первой:

abortEarly: false

Это поведение соответствует требованиям UX форм, где важно показать весь список проблем сразу.


Контроль глубины и точности ошибок

Joi формирует путь ошибки в виде массива:

["user", "address", "city"]

Resolver преобразует его в строку:

"user.address.city"

Это обеспечивает совместимость с React Hook Form и позволяет корректно отображать ошибки в глубоко вложенных структурах.


Работа с условной логикой

Joi поддерживает зависимости между полями:

const schema = Joi.object({
  password: Joi.string().required(),
  confirmPassword: Joi.any().valid(Joi.ref("password")).required()
});

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


Поведение при сбросе формы

При использовании reset() React Hook Form очищает состояние, но схема Joi остаётся неизменной. Resolver не хранит состояние между вызовами, что делает его полностью чистой функцией.


Структурная роль в архитектуре приложений

Joi resolver выполняет функцию промежуточного слоя между декларативной схемой данных и реактивной формой. Он позволяет:

  • отделить UI от правил валидации
  • централизовать бизнес-ограничения
  • унифицировать обработку ошибок
  • обеспечить согласованность данных на клиенте и сервере