Контракты хуков, Rules of Hooks, инкапсуляция жизненного цикла, стабильность API и проверяемые адаптеры.
Custom Hook переиспользует реактивную логику и lifecycle, но не делит state между вызовами автоматически.
Вы выделите проверяемые Hooks с узкими входами, явными результатами и корректной отменой, не превращая их в скрытый сервис-локатор.
До начала достаточно Effects, refs, reducers, callbacks и TypeScript generics. В конце должен появиться не конспект, а набор useOnlineStatus, useDebouncedValue и useResource hooks с адаптерами и contract tests.
Hooks вызывают только на верхнем уровне function component или custom Hook. Нельзя вызывать их в условии, цикле, handler, обычной функции или после conditional return. React сопоставляет state cells по порядку вызовов, поэтому порядок обязан быть одинаковым для identity компонента.
Рабочая проверка. Перепишите условный Hook так, чтобы условие находилось внутри Effect или в отдельном компоненте.
Типичная поломка. if (enabled) useEffect(...).
Функция с use считается Hook и может вызывать другие Hooks; обычной утилите префикс не нужен. Название описывает возможность (useOnlineStatus), а сигнатура — reactive inputs и outputs. Не скрывайте обязательную зависимость чтением случайного singleton, если её можно передать параметром или Provider.
Рабочая проверка. Разделите pure parseQuery и Hook useQueryState.
Типичная поломка. Назвать обычный formatter useFormatDate, вводя lint и читателя в заблуждение.
Custom Hook переиспользует код, а не state. Два вызова useOnlineStatus создают две subscriptions, если Hook так написан; для одного общего external source лучше useSyncExternalStore, который управляет подпиской store. Если consumers должны делить draft, state поднимают.
Рабочая проверка. Создайте два useCounter и докажите независимость значений.
Типичная поломка. Ожидать, что custom Hook является singleton store.
Hook возвращает доменную модель и commands. Сырые setters раскрывают внутреннюю структуру и позволяют illegal transitions. Object result читаем, но новая object identity может влиять на потребителей; не мемоизируйте ритуально, сначала проектируйте контракт. Tuple подходит устойчивой малой паре вроде [value, setValue].
Рабочая проверка. Замените setState на commands retry/cancel/select.
Типичная поломка. Вернуть десять setters и заставить каждый consumer повторять workflow.
Hook можно связать с API через узкий interface или функцию. Это делает browser/network boundary заменяемой в тесте и не требует module mocking всего приложения. Стабильность adapter identity важна для Effect deps; production adapter обычно экспортируется модулем или context.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
type SearchClient = {
search(query: string, signal: AbortSignal): Promise<readonly Product[]>;
};
function useProductSearch(client: SearchClient, query: string) { /* lifecycle */ }Интерфейс описывает ровно нужную capability и позволяет deterministic fake с управляемыми Promise.
Рабочая проверка. Внедрите fake, который завершает два запроса в обратном порядке.
Типичная поломка. Импортировать огромный global API singleton прямо в generic Hook.
Debounce Hook хранит отложенное значение и очищает timer при смене input. Search Hook отменяет request отдельно. Не смешивайте задержку ввода, data cache и UI state в один необозримый Hook. Fake timers проверяют debounce, controlled promises — race; это разные контракты.
Рабочая проверка. Проверьте, что пять быстрых вводов дают одно обновление debounced value.
Типичная поломка. Создавать timeout во время render или не очищать его на unmount.
Pure helper тестируется напрямую. Reactive Hook проверяется через маленький consumer или специализированный renderHook: входы меняются через rerender, обновления заворачиваются в act, cleanup наблюдается у adapter. Не проверяйте private state или число вызовов Effects, если контракт этого не обещает.
Рабочая проверка. Проверьте status transitions и abort при смене query, не читая внутренние refs.
Типичная поломка. Утверждать конкретное число render в StrictMode как продуктовый контракт.
Hooks хорошо компонуются, если каждый имеет ясную ответственность и error/cancellation policy. God hook, возвращающий десятки значений и скрывающий routing, auth, cache и UI, становится неявным framework. Оставляйте orchestration в feature Hook, а низкоуровневые Hooks — маленькими adapters.
Рабочая проверка. Разделите feature search на query parsing, debounce и resource lifecycle.
Типичная поломка. useApp() возвращает всё состояние и все сервисы приложения каждому компоненту.
Создайте useSearchProducts: debounce, abort предыдущего запроса, status union, retry и внедряемый client. Hook не читает глобальный URL и не возвращает сырые setters. Проверьте смену query и unmount.
Критерий завершения: rules-of-hooks lint проходит, каждый вызов независим, внешние зависимости внедрены, а race policy доказана тестом. Сначала зафиксируйте наблюдаемое поведение тестом или измерением, затем меняйте реализацию.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Асинхронный интерфейс и Suspense