Собственные resolver функции

В экосистеме react-hook-form механизм resolver выступает промежуточным слоем между формой и валидационной схемой. Он преобразует входные данные в формат, пригодный для проверки, запускает валидацию и возвращает структурированный результат, содержащий либо валидированные значения, либо ошибки.

YupResolver представляет собой адаптер, связывающий схемы валидации библиотеки Yup с механизмом resolver. Однако встроенная реализация не всегда покрывает сложные сценарии: динамические схемы, частичные проверки, каскадные зависимости полей, условную логику и гибридные источники правил. В таких случаях используется создание собственных resolver-функций.

Контракт resolver-функции

Resolver-функция в контексте react-hook-form имеет строго определённую сигнатуру:

  • принимает values формы
  • принимает контекст и конфигурацию
  • возвращает объект с результатами валидации

Структура результата:

  • values — нормализованные данные формы
  • errors — объект ошибок, сопоставленный с полями

Обобщённая форма:

const customResolver = async (values, context, options) => {
  return {
    values: {},
    errors: {}
  };
};

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

Базовая структура собственной реализации

Простейшая реализация resolver поверх Yup выглядит как прямой вызов схемы:

import * as yup from "yup";

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

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

    return {
      values: validatedValues,
      errors: {}
    };
  } catch (validationError) {
    const errors = {};

    validationError.inner.forEach((err) => {
      if (!err.path) return;

      errors[err.path] = {
        type: err.type ?? "validation",
        message: err.message
      };
    });

    return {
      values: {},
      errors
    };
  }
};

Данная структура отражает ключевой принцип: Yup возвращает массив ошибок, который требуется преобразовать в плоскую карту ошибок формы.

Преобразование ошибок Yup в формат react-hook-form

Yup формирует ошибки в виде массива inner, где каждая ошибка содержит:

  • path — путь к полю
  • message — текст ошибки
  • type — тип нарушения

Однако react-hook-form ожидает объект с вложенной структурой ошибок:

{
  fieldName: {
    type: string,
    message: string
  }
}

При сложных структурах данных (вложенные объекты, массивы) требуется трансформация путей:

const setNestedError = (errors, path, error) => {
  const keys = path.split(".");
  let current = errors;

  keys.forEach((key, index) => {
    if (index === keys.length - 1) {
      current[key] = error;
    } else {
      current[key] = current[key] || {};
      current = current[key];
    }
  });
};

Такой подход позволяет корректно обрабатывать поля вида:

  • user.email
  • addresses[0].city
  • profile.settings.theme

Интеграция с динамическими схемами Yup

Одной из причин написания собственного resolver является необходимость динамического формирования схемы:

  • зависимость полей друг от друга
  • переключение режимов формы
  • условные блоки валидации

Пример динамической схемы:

const getSchema = (mode) => {
  return yup.object({
    name: yup.string().required(),
    company: mode === "business"
      ? yup.string().required()
      : yup.string().notRequired()
  });
};

Resolver в этом случае строится на лету:

const dynamicResolver = async (values, context) => {
  const schema = getSchema(context.mode);

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

    return {
      values: result,
      errors: {}
    };
  } catch (err) {
    const errors = {};

    err.inner.forEach((e) => {
      errors[e.path] = {
        type: e.type,
        message: e.message
      };
    });

    return {
      values: {},
      errors
    };
  }
};

Частичная валидация и режимы проверки

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

  • onSubmit
  • onChange
  • onBlur

Оптимизация достигается через управление глубиной проверки:

const optimizedResolver = async (values, context, options) => {
  const shouldValidateAll = options?.criteriaMode === "all";

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

  try {
    const result = await schema.validate(values, {
      abortEarly: !shouldValidateAll
    });

    return {
      values: result,
      errors: {}
    };
  } catch (err) {
    const errors = {};

    err.inner.forEach((e) => {
      if (!errors[e.path]) {
        errors[e.path] = {
          type: e.type,
          message: e.message
        };
      }
    });

    return {
      values: {},
      errors
    };
  }
};

Нормализация входных данных

Перед передачей данных в Yup часто требуется нормализация:

  • приведение пустых строк к undefined
  • удаление лишних полей
  • преобразование типов
const normalizeValues = (values) => {
  const result = {};

  Object.keys(values).forEach((key) => {
    const value = values[key];

    if (value === "") {
      result[key] = undefined;
      return;
    }

    if (typeof value === "string" && !isNaN(value)) {
      result[key] = Number(value);
      return;
    }

    result[key] = value;
  });

  return result;
};

Resolver становится точкой контроля данных до валидации:

const resolver = async (values) => {
  const normalized = normalizeValues(values);

  try {
    const result = await schema.validate(normalized, {
      abortEarly: false
    });

    return {
      values: result,
      errors: {}
    };
  } catch (e) {
    const errors = {};

    e.inner.forEach((err) => {
      errors[err.path] = {
        type: err.type,
        message: err.message
      };
    });

    return {
      values: {},
      errors
    };
  }
};

Кэширование результатов валидации

При частых изменениях формы возможно повторное вычисление одинаковых состояний. Ввод кэширования снижает нагрузку:

const cache = new Map();

const hashValues = (values) => JSON.stringify(values);

const cachedResolver = async (values) => {
  const hash = hashValues(values);

  if (cache.has(hash)) {
    return cache.get(hash);
  }

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

    const response = {
      values: result,
      errors: {}
    };

    cache.set(hash, response);

    return response;
  } catch (e) {
    const errors = {};

    e.inner.forEach((err) => {
      errors[err.path] = {
        type: err.type,
        message: err.message
      };
    });

    const response = {
      values: {},
      errors
    };

    cache.set(hash, response);

    return response;
  }
};

Композиция нескольких схем Yup

Сложные формы часто разбиваются на независимые схемы, которые затем объединяются в resolver.

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

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

const composedResolver = async (values) => {
  try {
    const user = await userSchema.validate(values);
    const profile = await profileSchema.validate(values);

    return {
      values: {
        ...user,
        ...profile
      },
      errors: {}
    };
  } catch (e) {
    const errors = {};

    e.inner?.forEach((err) => {
      errors[err.path] = {
        type: err.type,
        message: err.message
      };
    });

    return {
      values: {},
      errors
    };
  }
};

Обработка вложенных массивов и структур

Yup активно используется для массивов объектов, что требует корректной обработки путей:

  • users[0].email
  • items[3].price

При формировании ошибок важно сохранять индексную структуру:

err.inner.forEach((error) => {
  const path = error.path.replace(/\[(\d+)\]/g, ".$1");

  errors[path] = {
    type: error.type,
    message: error.message
  };
});

Такая нормализация упрощает последующую работу с состоянием формы.

Асинхронные зависимости и внешние проверки

Resolver может включать внешние проверки:

  • запросы к API
  • проверка уникальности email
  • валидация токенов
const asyncResolver = async (values) => {
  const schema = yup.object({
    email: yup.string().email().required()
  });

  const emailExists = await api.checkEmail(values.email);

  try {
    await schema.validate(values, { abortEarly: false });

    const errors = {};

    if (emailExists) {
      errors.email = {
        type: "exists",
        message: "Email уже используется"
      };
    }

    return {
      values,
      errors
    };
  } catch (e) {
    const errors = {};

    e.inner.forEach((err) => {
      errors[err.path] = {
        type: err.type,
        message: err.message
      };
    });

    return {
      values: {},
      errors
    };
  }
};

Такой подход объединяет схему Yup и произвольную бизнес-логику в одном resolver-слое.

Управление типами ошибок и приоритетами

При комбинировании нескольких источников ошибок возникает необходимость определения приоритетов:

  • ошибки схемы
  • бизнес-ошибки
  • серверные ошибки
const mergeErrors = (schemaErrors, businessErrors) => {
  return {
    ...schemaErrors,
    ...Object.keys(businessErrors).reduce((acc, key) => {
      acc[key] = {
        type: "business",
        message: businessErrors[key]
      };
      return acc;
    }, {})
  };
};

Такая композиция обеспечивает предсказуемую модель отображения ошибок в форме.

Расширение поведения YupResolver через декораторы

Resolver можно оборачивать для расширения функциональности:

const withLogging = (resolver) => {
  return async (values, context, options) => {
    const start = Date.now();

    const result = await resolver(values, context, options);

    const duration = Date.now() - start;

    logger.log("validation", { duration });

    return result;
  };
};

Подобная декорация позволяет внедрять:

  • логирование
  • метрики
  • трассировку
  • отладочные режимы

без изменения основной логики валидации.