Разделение ответственности

Архитектура современных веб-приложений требует строгого разграничения обязанностей между слоями логики. В контексте работы с формами в JavaScript-экосистеме это особенно заметно при использовании связки React Hook Form и Yup через YupResolver. Ключевая идея заключается в том, чтобы отделить управление состоянием формы от описания правил валидации, сохраняя каждую часть системы максимально изолированной и переиспользуемой.


Принцип единственной ответственности в форме

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

  • управление состоянием формы (значения полей, touched/dirty состояния);
  • описание правил валидации;
  • интеграционный слой, связывающий валидацию и форму.

Без такого разделения логика формы быстро становится монолитной: проверки перемешиваются с UI, обработчики событий дублируют правила валидации, а тестирование усложняется.


Роль Yup как декларативного слоя валидации

Yup выступает как самостоятельный слой описания схемы данных. Он не знает ничего о React, DOM или конкретной форме. Его задача — описать структуру и ограничения данных.

Пример схемы:

import * as Yup from "yup";

const userSchema = Yup.object({
  email: Yup.string()
    .email("Некорректный email")
    .required("Email обязателен"),
  password: Yup.string()
    .min(8, "Минимум 8 символов")
    .required("Пароль обязателен"),
});

Здесь вся логика валидации сосредоточена в одном месте. Она не зависит от UI и может быть использована в любом контексте: клиентском, серверном или тестовом.


React Hook Form как слой управления состоянием

React Hook Form отвечает за жизненный цикл формы:

  • регистрация полей;
  • отслеживание изменений;
  • управление состоянием ошибок;
  • обработка submit-событий.

При этом библиотека сознательно избегает жесткой привязки к конкретному способу валидации. Это позволяет подключать внешние схемы через механизм резолверов.

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

const { register, handleSubmit, formState } = useForm();

Без резолвера React Hook Form не знает, какие правила применять — он лишь предоставляет инфраструктуру.


YupResolver как интеграционный слой

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

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().required(),
});

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

В этом случае:

  • Yup отвечает за проверку данных;
  • YupResolver выполняет адаптацию результата;
  • React Hook Form управляет состоянием формы.

Каждый слой остается независимым.


Границы ответственности слоев

Четкое разделение становится заметным при анализе обязанностей:

Yup:

  • описание структуры данных;
  • проверка ограничений;
  • генерация ошибок валидации.

React Hook Form:

  • хранение состояния формы;
  • регистрация input-полей;
  • управление submit и reset;
  • оптимизация ререндеров.

YupResolver:

  • запуск валидации по схеме;
  • преобразование ошибок Yup в формат RHF;
  • возврат результата в форму.

Ни один из слоев не дублирует функциональность другого.


Почему смешивание ответственности приводит к проблемам

При отсутствии разделения часто возникает ситуация, когда:

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

Пример антипаттерна:

const validateEmail = (value) => {
  if (!value) return "Email обязателен";
  if (!value.includes("@")) return "Некорректный email";
  return true;
};

Такая проверка привязана к конкретному полю и месту использования, что делает её непереиспользуемой.


Централизация валидации через схему

Перенос логики в Yup устраняет дублирование и упрощает сопровождение:

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

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


Инверсия зависимостей в связке YupResolver

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

Это позволяет:

  • заменить Yup на Zod или Joi без переписывания формы;
  • тестировать форму без реального UI;
  • переиспользовать схему в разных слоях приложения.
const form = useForm({
  resolver: yupResolver(schema),
});

Форма знает только о resolver, но не о внутренней реализации.


Масштабирование сложных форм

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

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

const schema = Yup.object({
  user: Yup.object({
    name: Yup.string().required(),
    contacts: Yup.object({
      email: Yup.string().email().required(),
    }),
  }),
});

React Hook Form при этом остается неизменным, а YupResolver корректно обрабатывает структуру ошибок.


Переиспользование схем и независимость от UI

Одним из ключевых преимуществ разделения ответственности является возможность повторного использования схемы в разных контекстах:

  • валидация на клиенте;
  • проверка на сервере;
  • тестовые сценарии;
  • генерация документации по данным.

Схема перестает быть частью интерфейса и становится описанием данных как доменной модели.


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

Yup поддерживает композицию, что усиливает разделение ответственности на уровне домена:

const baseUser = Yup.object({
  email: Yup.string().email().required(),
});

const extendedUser = baseUser.shape({
  role: Yup.string().required(),
});

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


Ошибки как контракт между слоями

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

Структура ошибки:

  • имя поля;
  • сообщение;
  • тип ошибки.

Это позволяет UI оставаться максимально простым: он не принимает решений, а только отображает результат работы слоя валидации.


Изоляция логики как основа поддерживаемости

Разделение ответственности в связке Yup, YupResolver и React Hook Form приводит к архитектуре, в которой:

  • валидация независима от UI;
  • UI не содержит бизнес-правил;
  • интеграция реализована через тонкий адаптер;
  • изменения в одном слое не затрагивают другие.

Такое распределение обязанностей снижает связность кода и делает систему предсказуемой при росте сложности.