open atlas
↑ К треку
Паттерны React RXP · 04 · 03

Когда не выносить хук

У выноса кастомного хука есть стоимость владения; не плати её ради единственного потребителя, однострочной обёртки или общего файла для несвязанного состояния — инлайн часто senior-выбор, а кодовая база одноразовых хуков ненавигируема.

RXP Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Прошлые два урока научили тебя выносить кастомные хуки — и они работают, поэтому рефлекс — выносить везде. У компонента три useState и эффект, и инстинкт говорит: «вынеси в useThing». Ты выносишь, и дифф выглядит аккуратнее. Полгода спустя в репозитории сорок хуков, каждый использован ровно один раз, и найти код, запускающий экран, означает открыть пять файлов, чтобы собрать заново то, что раньше было одной читаемой функцией.

Вынос не бесплатен. Каждый хук, который ты создаёшь, — это новое имя для запоминания, новый файл для открытия, слой косвенности между читателем и поведением. Хук с одним потребителем обычно эту цену не оправдал. Этот урок — встречное давление: senior-навык не в том, чтобы выносить хуки, а в том, чтобы знать, какие выносы окупаются, а какие — тот же рефлекс переабстрагирования, от которого предостерегает весь этот курс, только теперь в одежде с префиксом use.

Цель

После этого урока ты можешь сформулировать, что у абстракции есть стоимость владения и что кастомный хук обязан её оправдать; распознать три сигнала преждевременного выноса — единственный потребитель, тонкая однострочная обёртка над встроенным хуком и сваливание несвязанного состояния вместе лишь ради «общего файла»; объяснить, почему инлайн-код часто более ясный и сопровождаемый выбор; и назвать режим отказа — кодовую базу из десятков одноразовых хуков, по которой никто не может ориентироваться, — чтобы тянуться к выносу только тогда, когда переиспользование или подлинная сложность реальны, а не умозрительны.

1

У кастомного хука есть стоимость владения: имя, файл и слой косвенности — и эта стоимость реальна вне зависимости от того, переиспользуется хук или нет. Читая инлайн-код, ты видишь поведение прямо там, где оно выполняется. Читая useUserForm(), ты видишь имя и должен прыгнуть к его определению, чтобы узнать, что он на самом деле делает, а затем прыгнуть обратно. Этот прыжок дёшев один раз; умноженный на экран, собранный из шести одноразовых хуков, он превращает чтение в сборку пазла. Вынос меняет локальность (всё в одном месте) на имя (ярлык, который можно переиспользовать). Сделка выигрывает только тогда, когда имя переиспользуется или инлайн-версия по-настоящему слишком велика для чтения.

// инлайн: поведение видно ровно там, где оно выполняется — нулевая косвенность
function ProfileForm() {
  const [name, setName] = useState("");
  const [bio, setBio] = useState("");
  // читатель видит всё состояние формы прямо здесь
}

По умолчанию вопрос не «выносить или нет» — а «вынос обязан выкупить больше, чем стоит косвенность, которую он добавляет».

2

Сигнал первый: единственный потребитель. Хук, используемый ровно в одном месте, почти всегда преждевременен. Весь смысл кастомного хука — делить логику с состоянием между компонентами. С одним потребителем делить нечего — ты перенёс код в другой файл и получил имя, но заплатил косвенностью и не купил никакого переиспользования. Честная версия держит код инлайн, пока реально не появится второй потребитель, и тогда вынос мотивирован настоящим дублированием, а не догадкой.

// преждевременно: у useProfileForm один потребитель — это просто тело ProfileForm в другом файле
function useProfileForm() {
  const [name, setName] = useState("");
  const [bio, setBio] = useState("");
  return { name, setName, bio, setBio };
}
function ProfileForm() {
  const { name, setName, bio, setBio } = useProfileForm(); // почему это не просто здесь?
}

«Может, переиспользую потом» — это YAGNI в форме хука. Выноси на втором использовании, а не на первой догадке — рефакторинг занимает две минуты, когда второй потребитель реален, и не стоит ничего до тех пор.

3

Сигнал второй: тонкая однострочная обёртка. Оборачивание единственного встроенного хука в кастомный добавляет имя, не добавляя смысла. useState(false) — уже полное, идиоматичное выражение, которое каждый React-разработчик читает мгновенно. Переименование его в useBoolean не покупает ничего — читателю теперь приходится выучить твоё имя для вещи, которую платформа уже назвала, и проверить, добавила ли твоя обёртка поведение или просто пробросила. Кастомный хук оправдывает своё имя, когда инкапсулирует нетривиальную логику: эффект с очисткой, подписку, производное вычисление, многошаговый автомат состояний. Сквозной проброс — чистый налог.

// тонкая обёртка: добавляет имя и файл, не инкапсулирует ничего
function useBoolean(initial = false) {
  const [v, setV] = useState(initial);
  return [v, () => setV(true), () => setV(false)] as const; // едва больше, чем useState
}

// в месте вызова это читается не лучше встроенного, а для нового читателя — хуже
const [open, openIt, closeIt] = useBoolean(); // против: const [open, setOpen] = useState(false)

Если тело хука короче его собственной сигнатуры плюс места вызова, ты не абстрагировал ничего.

4

Сигнал третий: сваливание несвязанного состояния ради «общего файла». Запихивание независимых забот в один хук связывает вещи, у которых нет причины меняться вместе. Хук, возвращающий { user, theme, cartCount }, потому что эти значения случайно оказались в одном компоненте, создаёт ложную зависимость: изменение логики корзины теперь трогает хук, который импортирует и код темы, а любой потребитель одного поля тянет за собой все. Когезия, а не число файлов, — вот метрика. Несвязанное состояние должно оставаться раздельным — три маленьких сфокусированных хука (или три инлайн-useState) бьют один сборный хук, единственный организующий принцип которого — «они существовали рядом друг с другом».

// связанная солянка: тема, авторизация и корзина не имеют друг к другу никакого отношения
function usePageState() {
  const [user, setUser] = useState<User | null>(null);
  const [theme, setTheme] = useState<"light" | "dark">("light");
  const [cartCount, setCartCount] = useState(0);
  return { user, setUser, theme, setTheme, cartCount, setCartCount };
}
// теперь переключатель темы и бейдж корзины связаны по изменениям через один хук без причины

У хука должна быть одна связная задача. «Живёт в том же компоненте» — это не задача.

Разбор примера

«Полезный» вынос и инлайн-версия, которая читается лучше. Коллега выкатывает модалку и выносит её состояние open/close в переиспользуемый хук, «чтобы будущие модалки могли его использовать»:

// вынесенный хук — выглядит переиспользуемым, используется ровно в одном компоненте
function useModal() {
  const [isOpen, setIsOpen] = useState(false);
  const open = useCallback(() => setIsOpen(true), []);
  const close = useCallback(() => setIsOpen(false), []);
  return { isOpen, open, close };
}

function SettingsPanel() {
  const { isOpen, open, close } = useModal();
  return (
    <>
      <button onClick={open}>Settings</button>
      {isOpen && <Dialog onClose={close}>{/* … */}</Dialog>}
    </>
  );
}

Посчитай, что useModal здесь реально купил. Потребитель один, значит, переиспользования нет. Это тонкая обёрткаisOpen/open/close — это useState(false) с двумя тривиальными сеттерами под новыми именами. Чтобы понять SettingsPanel, читателю теперь нужно открыть второй файл, чтобы убедиться, что open просто ставит true. Хук добавил имя, файл и церемонию useCallback, а ничего нетривиального не инкапсулировал. Инлайн-версия и короче, и яснее:

function SettingsPanel() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>Settings</button>
      {isOpen && <Dialog onClose={() => setIsOpen(false)}>{/* … */}</Dialog>}
    </>
  );
}

Всё, что делает компонент, на экране; искать нечего. Senior-ход — оставить инлайн, — а если позже третья модалка захочет то же самое поведение плюс, скажем, ловушку фокуса и закрытие по Escape (реальная, нетривиальная логика), тогда вынеси useModal, мотивированный подлинным переиспользованием и сложностью, а не переименованием useState. Выносить сейчас — это рефлекс переабстрагирования: он делает сегодняшний код хуже, чтобы облегчить гипотетическое будущее, а это будущее может никогда не наступить.

Почему это работает

Почему «переиспользую потом» так убедительно и так часто неверно? Потому что стоимость невидима в момент выноса — дифф выглядит чище, ты чувствуешь себя продуктивным, а будущее переиспользование кажется вероятным. Но стоимость платится непрерывно и другими людьми: каждый читатель единственного потребителя теперь переключает контекст через твой файл, а каждый одноразовый хук делает следующее решение «выносить ли?» более привычным, так что они накапливаются. Асимметрия — вот ключ: вынести позже, когда появится реальный второй потребитель, стоит две минуты и движимо доказательством. Вынести сейчас, умозрительно, стоит каждого будущего читателя и движимо догадкой. Дёшево отложить плюс движимо догадкой означает, что отложить — это вариант по умолчанию.

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

Рефлекторная ошибка — считать «вынеси хук» безусловно хорошим: тот же инстинкт переабстрагирования, который этот курс не устаёт помечать, просто в react-обличье. Симптомы: useBoolean, useToggle, useString, useCounter, оборачивающие одиночные встроенные хуки; директория хуков, каждый из которых импортирован ровно одним компонентом; usePageState, возвращающий десять несвязанных полей. Лекарство не «никогда не выноси» — вынос правильный инструмент для реальной общей, нетривиальной логики. Лекарство — требовать причину прежде, чем платить стоимость: второй потребитель, который существует, или логику, слишком сложную для чтения инлайн. Если ни того ни другого нет, инлайн — senior-ответ, а более чистый на вид вынос — это регрессия, за которую ты будешь платить при каждом будущем чтении.

Проверь себя
Викторина

Коллега выносит единственный кусок состояния open/close компонента в хук useModal(), используемый ровно в одном месте и возвращающий isOpen/open/close, которые просто оборачивают useState(false). По логике этого урока, это хороший вынос?

Итог

Кастомные хуки — реальный инструмент, но у выноса есть стоимость владения — имя для запоминания, файл для открытия, слой косвенности между читателем и поведением, — и эта стоимость платится вне зависимости от того, переиспользуется хук или нет. Три сигнала помечают вынос как преждевременный: единственный потребитель (делить нечего, так что это просто тело компонента в другом файле — YAGNI), тонкая однострочная обёртка над встроенным хуком вроде useState (переименование, которое ничего не инкапсулирует) и сваливание несвязанного состояния в один хук-солянку лишь ради общего файла (ложное связывание; когезия, а не число файлов, — вот метрика). Во всех трёх инлайн-код яснее, потому что поведение остаётся там, где выполняется. Режим отказа — считать «вынеси хук» безусловно хорошим и прийти к кодовой базе из десятков одноразовых хуков, по которой никто не может ориентироваться, — тот же рефлекс переабстрагирования в react-форме. Senior-правило: выноси на реальном втором потребителе или по-настоящему сложной логике, а не на догадке. Рефакторинг дёшево отложить, и он движим доказательством, когда реален; умозрительный вынос дорог навсегда.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.