Пользовательский хук это функция, имя которой начинается с use и которая вызывает другие хуки. Его пишут, когда двум компонентам нужна одна и та же логика с состоянием: перенесите вызовы useState и useEffect в функцию, верните то, что нужно компоненту, и вызывайте её из каждого компонента.
Оба спойлера вызывают useToggle, и открытие одного оставляет другой закрытым. Каждый вызов выполняет собственный useState, поэтому каждый компонент получает своё значение. Пользовательский хук делится логикой, а не состоянием.
Правила написания
- Имя начинается с
useи заглавной буквы:useToggle,useOnlineStatus. Правило линтера для хуков использует префикс, чтобы находить вызовы хуков и проверять их. - Он вызывает хотя бы один хук. Помощник, который не вызывает хуков, это обычная функция; назовите её
formatDate, а неuseFormatDate, чтобы её можно было вызывать где угодно, в том числе внутри условий. - Он подчиняется правилам хуков: вызывает хуки на своём верхнем уровне и вызывается только из компонентов или других пользовательских хуков.
- Он возвращает то, что полезно. Массив (
[on, toggle]) удобен, когда вызывающие переименовывают значения; объект ({ data, loading, error }) читается лучше для трёх и более.
useLocalStorage: состояние, которое переживает перезагрузку
useState забывает всё при перезагрузке. Этот хук читает начальное значение из localStorage и записывает туда каждое изменение.
Превью работает в изолированном фрейме, где браузер блокирует localStorage, поэтому здесь вы увидите предупреждение в Console, а хук перейдёт на обычное состояние. На обычной странице имя переживёт перезагрузку. try/catch нужен не только для этого превью: режимы приватного просмотра, заблокированные cookie и заполненная квота тоже заставляют хранилище выбрасывать ошибки в настоящих приложениях. Передача функции в useState означает, что хранилище читается один раз, при первом рендере.
useDebounce: подождать, пока пользователь перестанет печатать
Поле поиска не должно отправлять запрос при каждом нажатии клавиши. useDebounce возвращает копию значения, которая обновляется только после того, как оно перестало меняться на заданное время.
Быстро наберите слово: Typed следует за каждой клавишей, Debounced догоняет через полсекунды после остановки, а Console выводит один поиск. Каждое нажатие клавиши меняет value, поэтому React выполняет очистку (clearTimeout) перед запуском нового таймера. Замените 500 на 0, и каждое нажатие клавиши будет выводить собственный поиск.
useWindowWidth: подписка с очисткой
Хук, который слушает браузер, должен удалять обработчик, когда компонент уходит, иначе каждое монтирование оставляет его после себя.
Измените размер окна браузера, и число последует за ним. Нажмите кнопку, и Console покажет, что обработчик удаляется. Удалите return из эффекта, затем несколько раз смонтируйте и размонтируйте: каждый раз появляется строка added, а removed нет, поэтому старые обработчики накапливаются. Для значений браузера вроде этого React создал useSyncExternalStore, и его хорошо вызывать внутри пользовательского хука.
Логика общая, состояние нет
Два компонента, вызывающие один и тот же хук, получают две отдельные копии всего, что внутри него. Это удивляет тех, кто ожидает, что хук работает как глобальная переменная.
Нажмите любую кнопку, и сдвинется только этот счётчик. Если оба должны показывать одно и то же число, перенесите состояние в App и передайте его вниз или положите в контекст. Пользовательский хук может оборачивать это общее состояние для удобства (useCart(), читающий CartContext), но общим его делает контекст, а не хук.
Когда писать пользовательский хук
Выносите хук, когда одна и та же группа вызовов хуков появляется в двух компонентах или когда логика эффекта одного компонента настолько длинная, что имя объяснило бы её лучше (useOnlineStatus() читается лучше двадцати строк обработчиков). Не оборачивайте один вызов useState в хук только ради переименования и не пишите хуки, которые лишь вызывают useEffect, чтобы установить состояние из пропсов; вычисляйте такое значение во время рендера.
Часто задаваемые вопросы
Что такое пользовательский хук в React?
Функция JavaScript, имя которой начинается с use и которая вызывает один или несколько других хуков. Она упаковывает логику с состоянием (состояние, эффекты, рефы), чтобы любой компонент мог переиспользовать её, вызвав функцию.
Делятся ли пользовательские хуки состоянием между компонентами?
Нет. Каждый вызов пользовательского хука выполняет собственные вызовы useState и useEffect, поэтому каждый компонент получает независимое состояние. Чтобы делить одно и то же значение между компонентами, поднимите состояние или положите его в контекст или внешнее хранилище.
Почему имя пользовательского хука должно начинаться с use?
Имя говорит правилам линтера React и другим разработчикам, что функция вызывает хуки, поэтому к ней применяются правила хуков: вызывать на верхнем уровне, никогда внутри условия или цикла. Без префикса линтер не может проверить вызовы внутри неё.
Может ли пользовательский хук возвращать JSX?
Он может возвращать что угодно, но если он возвращает JSX, обычно лучше написать его как компонент. Пользовательские хуки обычно возвращают значения и функции, массивом ([value, toggle]) или объектом ({ data, error, loading }).
Можно ли вызывать пользовательский хук внутри useEffect или обработчика события?
Нет. Пользовательский хук всё равно остаётся хуком, поэтому подчиняется правилам хуков: вызывайте его на верхнем уровне компонента или другого пользовательского хука.