25 окт. 2025 г.·6 мин чтения

Бизнес-логика в React-хуках: переносите правила в доменный код

Бизнес-логика в React-хуках часто начинается мелкой, а потом распространяется по экранам. Узнайте, что должно оставаться в хуках, что вынести в доменные модули и как безопасно рефакторить.

Бизнес-логика в React-хуках: переносите правила в доменный код

Почему хуки так быстро становятся громоздкими

Хук обычно не превращается в хаос с первого дня. Он начинается с одного простого правила: скрыть кнопку, заблокировать скидку или выбрать значение по умолчанию в зависимости от типа пользователя. Это кажется нормальным: экран уже имеет данные, а хук управляет взаимодействием.

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

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

Так доменные правила распространяются по React-хукам без какого-либо плана. Хук смешивает UI-стейт, сетевые вызовы, флаги загрузки, поведение форм и бизнес-решения в одном месте. Читать такой код тяжело, потому что при каждом изменении приходится отвечать на два вопроса одновременно: "Как ведет себя экран?" и "Каково правило?"

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

Это обычно видно рано. Одно продуктовое правило встречается в нескольких хуках, или одно небольшое изменение влияет на файлы оформления заказа, настроек аккаунта и админки. Это не проблема React — это знак, что общие правила оказались в месте, где пользователь кликает, а не там, где им место.

Что должен делать хук

React-хук лучше всего работает, когда остается близко к экрану. Он читает props, значения форм и локальный UI-стейт, а затем превращает это в простые значения и действия, которые может использовать остальная часть приложения.

Хороший хук управляет взаимодействием. Он отслеживает, что ввел пользователь, какой вариант выбран, выполняется ли запрос и что компонент должен показывать прямо сейчас. Это UI-вопросы. Им место рядом с компонентом, потому что они меняются вместе с экраном.

Само решение должно жить отдельно. Хук может передать простые данные в общую функцию, например calculateDiscount, canSubmitOrder или getUpgradeMessage. Эта функция возвращает результат, а хук превращает результат в то, что можно отобразить в компоненте.

Такое разделение делает код легче для чтения. Оно же упрощает тестирование. Вы тестируете общую доменную логику простыми входами и выходами, без рендера React или моков кликов. Затем тестируете хук на поведение UI — это уже гораздо меньшая задача.

Асинхронная работа по-прежнему естественно располагается в хуке. Хук может запускать запрос, держать isLoading, сохранять ошибку и выдавать действие повторной попытки. Всё это про тайминги и состояние экрана. Хук не должен знать, почему клиент получает бесплатную доставку или почему аккаунт должен быть заблокирован после неудачной проверки.

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

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

Что должно находиться в доменных модулях

Доменные модули должны содержать правила, которые должны оставаться верными независимо от того, где пользователь нажимает. Если клиент получает 10% скидки после того, как потратил $100, это правило должно быть в простом коде, а не в хуке. То же относится к порогам бесплатной доставки, критериям триала, лимитам апгрейда, окнам возврата и другим решениям, которые формируют поведение продукта.

Простой тест помогает: можно ли выполнить правило без React? Если да — вынесите. Хук собирает данные, реагирует на ввод и вызывает правило. Само правило живет в общем модуле, который может использовать любой экран, обработчик API или фоновая задача.

Сюда обычно относятся расчеты цен, валидация заказов и переходов состояния, проверки прав на основе роли или типа аккаунта и проверки по датам, например периоды продления или истечение триала.

Изменения статусов тоже там. Допустим, заказ может перейти со «pending» в «paid» только если платеж прошел и запас товара зарезервирован. Это не UI-вопрос. Экран может показывать кнопку, но правило решает, разрешен ли переход.

Чистые функции — самый надежный способ писать такой код. Они принимают вход, возвращают результат и не зависят от React-стейта, браузерных API или скрытых глобалов. Это облегчает тестирование и повторное использование. Функция вроде canApplyDiscount(cart, customer, today) понятнее, чем хук, смешивающий fetch-логику, состояние кнопки и правила скидок в одном файле.

Это важно по мере роста продукта. Страница оформления заказа, админ-панель и мобильное приложение могут все нуждаться в одинаковом ответе про ценообразование или доступ. Общая доменная логика сохраняет консистентность. Когда правило меняется, вы обновляете одну функцию вместо того, чтобы охотиться по хукам и надеяться, что всё согласовано.

Правило оформления заказа в неправильном месте

Когда useCheckout решает, применима ли скидка, пороги бесплатной доставки и налоговые правила, он перестает быть помощником UI. Он превращается в движок правил, обернутый в React.

Проблема проявляется, когда второй экран нуждается в том же ответе. Страница корзины может использовать useCheckout для показа итога, а админская страница делает собственную сводку для поддержки. Одна команда обновляет правило скидки, вторая забывает. Теперь клиент видит один итог, а админ — другой.

Такой дрейф обычен, потому что хуки удобны. В корзине уже есть стейт, поле купона, выбранный адрес и API-вызовы, поэтому добавить еще одно правило кажется безобидным. Через пару недель доставка считается в одном хуке, налог — в другом экране, и никто больше не верит в финальную цифру.

Лучшее разделение простое. Пусть хук собирает входы и реагирует на действия пользователя. Пусть чистая функция решает итог заказа.

const summary = calculateOrderSummary({
  items,
  coupon,
  shippingAddress,
  customerType,
})

calculateOrderSummary может вернуть промежуточную сумму, скидку, стоимость доставки, налог и финальный итог. И корзина, и админская страница вызывают одну и ту же функцию, поэтому они остаются согласованными при изменении правил.

Тестирование становится намного проще. Не нужно монтировать React, нажимать кнопки или ждать эффектов, чтобы проверить, работает ли купон с налогооблагаемым товаром. Передаете данные и проверяете результат.

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

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

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

Как вынести логику, не сломав экраны

Reduce Costly Rule Copies
Replace repeated conditions with domain modules your team can reuse across the product.

Когда хуки несут слишком много бизнес-логики, самое безопасное исправление — маленькое и скучное: вынести одно правило, проверить поведение, затем вынести следующее. Большинство сломанных экранов возникает от попытки изменить слишком много сразу.

Читая хук построчно, пометьте каждый if, ранний return и выводное значение. Затем переведите каждое в простую фразу на русском. "Показать спиннер пока данные загружаются" — это про экран. "Пользователи на приостановленном тарифе не могут создать проект" — это бизнес-правило. Это даст карту того, что остается, а что нужно вынести.

Далее сгруппируйте правила по бизнес-смыслу, а не по месту в хуке. Правила биллинга держите вместе. Правила доступа — вместе. Это помогает, когда одна и та же проверка появляется в двух-трех хуках с немного разными именами. Копированные проверки — первый признак необходимости общего доменного кода.

Вынесите одно правило в чистую функцию первым. Например, хук для страницы биллинга может решать, может ли клиент приостановить подписку. Вынесите это в canPauseSubscription(account, plan, invoiceStatus). Сделайте ее простой: вход → ответ.

После этого хук станет тоньше. Он должен в основном читать данные из стейта, props или запросов, вызывать доменную функцию, возвращать результат компоненту и управлять UI-стейтом вроде загрузки или локального ввода.

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

Добавьте тесты для нового модуля до того, как выносить следующее правило. Тестируйте бизнес-кейсы, а не поведение React. Если аккаунт на паузе с неоплаченным счетом не может снова приостановиться — опишите этот кейс в коде. Сохраняйте старые тесты экранов некоторое время. Они служат страховкой во время рефакторинга.

Если перенос кажется рискованным, вероятно хук все еще выполняет слишком много ролей. Делайте меньше. Проверяйте поведение. И двигайтесь дальше.

Частые ошибки во время рефакторинга

При выносе логики команды часто делают лишнее. Хук по-прежнему отвечает за тайминг UI: состояние загрузки, ретраи, чтение кеша и сетевые вызовы. Доменный код должен отвечать на вопросы вроде "может ли этот заказ отправиться сегодня?", а не "какая библиотека запросов должна это взять?"

Еще одна ошибка — передавать сеттеры React в функции, которые должны быть чистыми. Если хелпер может вызвать setError или setStep, он перестает быть переиспользуемым. Тогда тестирование усложняется. Лучше иметь чистые входы и выходы: передали данные заказа — получили решение.

Скрытые сайд-эффекты создают ещё большие проблемы. calculateDiscount не должна писать в localStorage, отправлять аналитику или триггерить refetch. Такие сюрпризы усложняют отладку, потому что функция говорит одно, а делает другое.

Код оформления заказа ясно показывает это. Если бесплатная доставка зависит от суммы корзины и типа клиента, держите правило в доменном модуле. Хук вызывает правило, а затем обновляет состояние экрана по результату. Не позволяйте хелперу сам подтягивать профиль пользователя или менять состояние формы за кадром.

Команды также создают один большой файл utils и кладут туда всё подряд. Сначала это выглядит аккуратно, но быстро превращается в ящик хлама. Группируйте код по домену: pricing, checkout, permissions, subscriptions. Если два правила про разные идеи — разделяйте их.

И ещё одна ошибка, которая отнимает много времени: переименования файлов, изменение импортов и переписывание логики в одном коммите. Когда что-то ломается, никто не поймет, откуда баг: от перемещения или от нового кода. Делайте переносы отдельно от изменения логики. Сначала переместите код без изменения поведения, запустите тесты и проверьте экраны. Переименовывайте и рефакторьте после того, как новое место стало стабильным.

Это медленнее, но обычно экономит время. Маленькие изменения легче ревьюить, тестировать и откатить, если экран начинает вести себя странно.

Быстрая проверка перед тем, как оставить логику в хуке

Make Rules Easy To Test
Turn messy hook conditions into plain functions your team can check fast.

Самый быстрый тест: спросите, останется ли правило важным, если React исчезнет. Если да — ему не место в хуке. Хуки должны заниматься состоянием, эффектами и взаимодействием. В тот момент, когда хук решает, что клиент может сделать, сколько он должен заплатить или какое состояние выигрывает, вы уже пишете доменный код в слое UI.

Переиспользование — ещё один сигнал. Представьте правило блокирующее купон, если в корзине есть подписка. Сегодня правило может быть нужно только на странице корзины, но завтра оно повлияет на оформление, админ-инструменты, предварительный просмотр заказа или мобильные экраны. Если другой экран может нуждаться в том же ответе, вынесите правило в общий модуль.

Тестирование тоже показатель. Если правило можно проверить простыми входами и выходами, React не добавляет ценности. Функцию canApplyCoupon(cart, coupon) легко покрыть примерами. Тест хука потребует рендеринга, сетапа и больше движущихся частей. Если браузер не нужен — держите правило вне браузерного кода.

Обращайте внимание на правила, связанные с деньгами, правами доступа и статусами. Эти области часто меняются, и ошибки там дорого обходятся. Налоговое правило, лимит возврата, проверка доступа или переход состояния заказа должны жить в коде, которому доверяет весь продукт. Спрятанные решения внутри одного хука легко пропустить и сложно проверить.

Есть и тест поддержки: если одно изменение политики заставит вас править два или три хука, правило уже разбросано. Баги обычно следуют той же схеме: один экран исправили, другой остался с старым поведением, и приходит поддержка.

Хук всё ещё может композировать результат. Он вызывает доменные модули, хранит возвращенные данные и подсоединяет их к UI. Это держит React-код коротким и оставляет правило в одном месте, где его легко читать, тестировать и менять.

Что меняется после разделения

Map Your Next Cleanup
Pick one busy hook and turn it into a smaller, easier file with expert guidance.

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

Команды обычно замечают это первыми, когда продукт просит изменить то, что кажется простым: "бесплатная доставка с $75 вместо $50". Сейчас это становится охотой по всему коду, когда правила разбросаны. После разделения меняется одна функция, и все экраны получают одинаковый ответ.

Веб и мобильные приложения тоже перестают расходиться. Если обе платформы вызывают одну доменную функцию, они следуют одним и тем же правилам для скидок, лимитов и eligibility. Кнопки могут выглядеть по-разному, но решение за ними остается одинаковым. Это важнее, чем многие думают.

QA получает более чистую цель. Вместо проверки каждого кейса кликая по нескольким сценариям, они могут сначала протестировать правило напрямую с четкими входами и ожидаемым результатом. Тестировщик может спросить: "Что произойдет, если в корзине подарочная карта, купон и заблокированный товар?" и получить прямой ответ из доменного кода до полного UI-теста.

Ежедневное влияние на код

Хуки становятся короче, и коротким хукам проще доверять. Новый разработчик может открыть хук и быстро понять, что он делает: читает стейт, вызывает API, управляет загрузкой и реагирует на клики. Ему не придется разбираться в налоговых правилах или лимитах подписки, смешанных в том же файле.

Код-ревью тоже улучшаются. Можно обсуждать правило в одном месте и поведение UI в другом. Если появляется баг, команда может задать более простой вопрос: правило неверно или экран его неправильно использует?

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

Что делать дальше в вашем коде

Начните с одного хука, которого боятся трогать. Вы его узнаете: он делает fetch, держит UI-стейт, проверяет права доступа, применяет правила ценообразования и тихо решает, что экран должен разрешать. Обычно там и начинается проблема.

Прочитайте хук сверху донизу и пометьте каждое бизнес-решение. Задавайте простые вопросы: может ли пользователь сделать это? Нужно ли скрыть этот элемент? Квалифицируется ли этот заказ? Если ответ зависит от правил продукта больше, чем от кликов или состояния загрузки, вынесите это из хука.

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

Не ждите полного переписывания. Даже один вынесенный правило меняет форму кода. Следующий разработчик быстрее найдет, протестирует и переиспользует его в другом экране без копирования условий.

Оформление заказа часто — хороший первый кандидат. Если хук решает, применяется ли скидка, можно ли использовать денежные средства со счета или показывать ли экспресс-доставку, эти проверки лучше держать в общем доменном коде. Хук может вызывать canUseStoreCredit(order, customer) или getAvailableShippingOptions(order). Это гораздо читаемее, чем пять вложенных условий, смешанных с useEffect и локальным стейтом.

Тесты важны прежде, чем идти дальше. Добавьте их для вынесенного правила, даже если тесты небольшие. Это даст уверенность при чистке следующего хука.

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

Если хотите второе мнение, Oleg Sotnikov at oleg.is работает со стартапами и малыми командами по архитектуре продукта и консультированию в роли Fractional CTO. Такая работа по границе фронтенда часто становится проще, когда кто-то просматривает кодовую базу и задает четкие правила, где кончается UI-логика и начинается доменная.

Часто задаваемые вопросы

How can I tell if logic belongs in a hook or in domain code?

Задайте один простой вопрос: имело бы значение это правило, если бы React исчез? Если да — вынесите его в доменный код. Хук оставьте для состояния экрана, ввода пользователя, загрузки, ошибок и действий.

What should stay inside a React hook?

Хук должен управлять взаимодействием: читать props, локальный стейт, значения форм, результаты запросов и возвращать флаги вроде isLoading, disabled, а также действия submit или retry. Решения про ценообразование, права доступа и статус заказа лучше не держать в хуках.

What kinds of logic belong in domain modules?

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

Why does business logic inside hooks cause so many problems later?

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

What is the safest way to refactor a hook that has too much business logic?

Начинайте с малого: вынесите одно правило в чистую функцию, пусть хук вызывает её, проверьте, что экран ведет себя одинаково. Затем заменяйте скопированные версии в других хуках и добавляйте тесты для доменной функции.

Should async requests stay in hooks?

Асинхронные запросы и управление состоянием загрузки остаются в хуке: хук должен делать запрос, держать isLoading, сохранять ошибку и предоставлять возможность повтора. Доменный код отвечает на бизнес-вопрос после того, как данные получены.

How should I test things after I split the logic?

Сначала тестируйте доменные правила простыми входными данными и ожидаемым результатом. Функцию вроде canApplyCoupon(cart, coupon) можно проверить без рендера компонентов. Хуки тестируйте уже на уровне UI: загрузки, отображения ошибок и привязки действий.

Where should pricing, permission, and status rules live?

Правила про деньги, доступ и переходы состояний стоит вынести в доменный код — это единый источник правды, к которому могут обратиться все экраны. Хук использует результат, чтобы показать или скрыть кнопки и сообщения.

What mistakes do teams make when they extract logic from hooks?

Частые ошибки — выносить слишком много в один шаг или позволять новым хелперам вызывать сеттеры React и сайд-эффекты. Доменные функции должны быть простыми: вход → выход. Они не должны вызывать setState, писать в localStorage, слать аналитику или делать дополнительные запросы за кадром.

Which hook should I clean up first?

Начните с хука, который люди боятся менять. Чаще всего это экраны оплаты, биллинга или аккаунта: они смешивают локальный стейт и продуктовые правила и дают быстрый выигрыш при чистке.