
React предоставляет программисту прекрасный базовый набор хуков и с каждой версией их количество и функционал увеличивается.
Трудно представить код современного React-приложения без таких функций как useState, useEffect, useRef и так далее.
Однако, в повседневной жизни мы часто решаем рутинные задачи, многие из которых могут быть автоматизированы.
Создание кастом-хуков это прекрасная возможность выделить часто переиспользуемый код в отдельные сущности.
Это помогает содержать основной код компонента в чистоте и избавляет нас от мелких ошибок, которые могут остаться незамеченными, когда вы пишете один и тот же код заново.
Ниже мы рассмотрим примеры некоторых из них.
1. useToggle
Приходилось ли вам когда-нибудь создавать useState, который содержал в себе только два значения true и false и назывался как-то вроде isActive, isChecked или isOpen?
Если ответ да - то вы определенно попали по адресу! Первый хук, который мы рассмотрим, инкапсулирует в себе эту логику, возвращая значение и методы для изменения его состояния.
import { useCallback, useState } from 'react'
import type { Dispatch, SetStateAction } from 'react'
export function useToggle(
defaultValue?: boolean,
): [boolean, () => void, Dispatch<SetStateAction<boolean>>] {
const [value, setValue] = useState(!!defaultValue)
const toggle = useCallback(() => {
setValue((x) => !x)
}, [])
return [value, toggle, setValue]
}
Его можно легко расширить функциями, которые будут явно устанавливать значение состояния в
trueилиfalse.
Рассмотрим пример использования:
export function Component() {
const [value, toggle, setValue] = useToggle()
return (
<>
<button onClick={toggle}>toggle</button>
<button onClick={() => setValue(false)}>hide</button>
{value && <div>Hello!</div>}
</>
)
}
2. useHover
Случались ли у вас такое, что css :hover по каким-либо причинам использовать было невозможно и ничего не оставалось, кроме как сымитировать это поведение с помощью mouseEnter и mouseLeave?
Если ответ снова положительный - то я готов вам представить второй кастом-хук, который сделает это за вас.
import { useRef, useState, useEffect } from 'react'
import type { RefObject } from 'react'
export function useHover<T extends HTMLElement = HTMLElement>(): [
RefObject<T>,
boolean,
] {
const ref = useRef<T>(null)
const [isHovered, setIsHovered] = useState(false)
useEffect(() => {
const element = ref.current
if (!element) return
const handleMouseEnter = () => setIsHovered(true)
const handleMouseLeave = () => setIsHovered(false)
element.addEventListener('mouseenter', handleMouseEnter)
element.addEventListener('mouseleave', handleMouseLeave)
return () => {
element.removeEventListener('mouseenter', handleMouseEnter)
element.removeEventListener('mouseleave', handleMouseLeave)
}
}, [])
return [ref, isHovered]
}
Использование этого хука несколько нестандартное, давайте рассмотрим на примере:
export function Component() {
const [hoverRef, isHovered] = useHover<HTMLDivElement>()
return (
<div
ref={hoverRef}
style={{ backgroundColor: isHovered ? 'lightblue' : 'lightgray' }}
>
{isHovered ? 'hovered' : 'not hovered'}
</div>
)
}
3. useDerivedState
Порой, в компоненте мы создаем useState, начальным значением которого является какое-либо значение из пропсов.
Если пропсы изменятся, то соответствующие изменения не затронут наше локальное состояние и оно продолжит хранить устаревшее значение.
Чтобы этого избежать мы можем воспользоваться следующим хуком:
export function useDerivedState<T>(
propValue: T,
): [T, Dispatch<SetStateAction<T>>] {
const [state, setState] = useState(propValue)
useEffect(() => {
setState(propValue)
}, [propValue])
return [state, setState]
}
Это может быть полезно в случаях с пользовательским вводом, когда мы хотим изменить значение и только затем его сохранить или вернуть изначальное значение.
export function Component({ initialName }: { initialName: string }) {
const [name, setName] = useDerivedState(initialName)
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
<div>Current name: {name}</div>
</>
)
}
4. useEventCallback
Все мы привыкли пользоваться хуком useCallback, который кеширует функцию между ре-рендерами.
Однако, если в массиве зависимостей этой функции будут значения, которые изменились - функция будет создана заново.
С точки зрения оптимизации производительности это может быть излишним, так как ваш коллбэк мог так ни разу и не быть вызванным.
Если вы хотите получить стабильную ссылку на коллбэк, который не меняется от рендера к рендеру, но при этом в момент вызова всегда содержит актуальные значения переменных, от которых он зависит, то вы можете воспользоваться следующим хуком:
export function useEventCallback<I extends unknown[], O>(
fn: (...args: I) => O,
): (...args: I) => O {
const ref = useRef<(...args: I) => O>()
useLayoutEffect(() => {
ref.current = fn
}, [fn])
return useCallback((...args) => {
const { current } = ref
if (current == null) {
throw new Error(
'callback created in useEventCallback can only be called from event handlers',
)
}
return current(...args)
}, [])
}
Чаще всего этот хук используется для коллбэков, вызов которых отложен во времени и инициируется пользователем. Удачным примером будет замена им обычных коллбэков для передачи в onClick:
export function Component() {
const [count, setCount] = useState(0)
const increment = useEventCallback(() => {
setCount((prev) => prev + 1)
})
return (
<div>
<p>{count}</p>
<button onClick={increment}>Add</button>
</div>
)
}
5. useDebouncedCallback
При взаимодействии пользователя с интерйфесом через такие события как: ввод текста, изменение ширины окна браузера, скролл - может возникать чрезмерно большое количество вызовов функций-коллбэков.
Зачастую нам это не нужно и мы хотим отложить вызов до момента, когда пользователь закончит действие, чтобы затем выполнить полезный код.
import { useEffect, useMemo, useRef } from 'react'
import debounce from 'lodash.debounce'
export function useDebouncedCallback<T extends (...args: any[]) => any>(
func: T,
delay = 500,
) {
const funcRef = useRef(func)
useEffect(() => {
funcRef.current = func
}, [func])
const debounced = useMemo(() => {
const debouncedFn = debounce(
(...args: Parameters<T>) => funcRef.current(...args),
delay,
)
return debouncedFn
}, [delay])
useEffect(() => {
return () => {
debounced.cancel()
}
}, [debounced])
return debounced
}
Этот хук можно расширить такими вспомогательными функциями как
cancel,isPendingиflush.
Рассмотрим пример использования:
export function Component() {
const [value, setValue] = useState('')
const debouncedSearch = useDebouncedCallback((query: string) => {
console.log('Search by:', query)
}, 500)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value
setValue(newValue)
debouncedSearch(newValue)
}
return (
<input
type="text"
placeholder="Search..."
value={value}
onChange={handleChange}
/>
)
}
Вот и все! Количество и функционал кастом-хуков может быть самым разнообразным, все ограничено лишь вашей фантазией и потребностями.
За большим количеством примеров вы можете обратиться в такие библиотеки как react-use или usehooks-ts, а также многие другие.
Комментарии (17)

Zukomux
07.02.2025 22:58Если useDerivedState нужен для первоначального состояния, то может использовать нормальные инструменты форм? Например Final Form, потому как в вашем примере работа идёт именно с полями ввода данных

AndreyVolfman Автор
07.02.2025 22:58Согласен, для этого примера лучше подойдут инструменты форм. Хотел показать хуки без дополнительных зависимостей

Gary_Ihar
07.02.2025 22:58Что будет с useHover, если элемент который к нему подключен рендерится по условию и вдруг исчез, а потом появился?
В 4 хуке вы упомянули про производительность, но в хук воткнули useLayoutEffect. Зачем он там? То же самое и в 5

BerkutEagle
07.02.2025 22:58useDerivedState в такой реализации может добавить головной боли. Сначала компонент перерисуется с обновлёнными пропсами, но старым стейтом. Затем, после setState внутри useEffect, перерисуется с новыми пропсами и новым стейтом.

Alexandroppolus
07.02.2025 22:58согласен, лучше сделать как-то так:
export function useDerivedState<T>( propValue: T, ): [T, Dispatch<SetStateAction<T>>] { const [state, setState] = useState(propValue); const ref = useRef(propValue); useEffect(() => { ref.currrent = propValue; setState(propValue); }, [propValue]); return [propValue === ref.current ? state : propValue, setState]; }Правда, от лишнего рендера тут не уйти, но хотя бы с актуальным состоянием.

BerkutEagle
07.02.2025 22:58Можно заменить useEffect на useLayoutEffect и мутировать стейт там. Минус производительность (одно дополнительное вычисление VDOM из-за мутации стейта), но зато попадаем в одну отрисовку DOM'а с актуальным состоянием

BerkutEagle
07.02.2025 22:58На рефах у меня получилось что-то такое
export function useDerivedState(propValue) { const [state, setState] = useState(propValue); const stateChanged = useRef(false); const lastValue = useRef(propValue); if (lastValue.current !== propValue) { stateChanged.current = false; lastValue.current = propValue; } return [ stateChanged.current ? state : propValue, useCallback((stateValue) => { if (typeof stateValue === "function") { setState((prevValue) => { if (stateChanged.current) { return stateValue(prevValue); } else { stateChanged.current = true; return stateValue(lastValue.current); } }); } else { stateChanged.current = true; setState(stateValue); } }, []), ]; }

BerkutEagle
07.02.2025 22:58Вариант на useSyncExternalStore
function getStore(propValue) { let listener = null; let val = propValue; let propVal = propValue; let stateVal = propValue; return { clean() { listener = null; }, subscribe(l) { listener = l; }, getValue() { return val; }, setPropValue(nextVal) { if (propVal !== nextVal) { val = propVal = nextVal; if (listener) listener(); } }, setStateValue(nextVal) { const nextStateValue = typeof nextVal === "function" ? nextVal(val) : nextVal; if (nextStateValue !== stateVal) { val = stateVal = nextStateValue; if (listener) listener(); } }, }; } export function useDerivedState(propValue) { const store = useMemo(() => getStore(propValue), []); store.setPropValue(propValue); return [ useSyncExternalStore(store.subscribe, store.getValue), store.setStateValue, ]; }

Alexandroppolus
07.02.2025 22:58В реализации useHover есть довольно типичная ошибка (упомянутая в комментарии выше), о ней писал здесь. Альтернативный вариант - использовать функциональный реф вместо useRef.
Несколько мелких поинтов о useEventCallback:
1) Если вызвать созданную им функцию внутри layoutEffect дочернего компонента, то реф ещё не будет обновлен, потому что сначала вызываются эффекты в чилдах. Для обновления рефа больше подходит useInsertionEffect.
2) throw там может вызваться только на самом первом рендере, а на последующих в рефе будет сидеть устаревшая функция и проверка не сработает. По нормальному это не проверить, потому предлагаю просто не заморачиваться.
3) Типизация не поддерживает перегрузки функций
4) Функция, передаваемая в хук, может быть необязательной, например это опциональный проп. Это хорошо бы поддержать, чтобы потом не городить костыли снаружи.Итого, со всеми поправками
function useEventCallback<T extends undefined | null | ((...args: never[]) => unknown)>( func: T, ): T { const refCallee = useRef(func); useInsertionEffect(() => { refCallee.current = func; }); const callback = useCallback((...args: never[]) => refCallee.current?.(...args), []) as T; return func && callback; }

shsv382
07.02.2025 22:58Ну кстати прикольная подборка, еще useFetch добавить, и можно в библиотеку компоновать! Спасибо!

DDeenis
07.02.2025 22:58В приведенном коде useDerivedState использование useEffect нежелательно, так как может привести к лишним перерисовкам и другим неприятным багам. Заменить useEffect можно так:
function useDerivedState<T>(propValue: T) { const [state, setState] = useState(propValue); const [prevState, setPrevState] = useState(propValue); if (prevState !== propValue) { setState(propValue); setPrevState(propValue); } return [state, setState] as const; }
adminNiochen
В useEventCallback дичь какая-то, зачем там layouteffect? Если хочешь юзколбек которому не нужен массив зависимостей и ссылка всегда стабильная, можно через рефы сделать
Finesse
Да, вместо использования
useLayoutEffectможно записывать новую функцию вrefпрям во время вызова хука. Но это сомнительный паттерн использования Реакта.Вообще, на месте создателей Реакта, я бы сделал так, чтобы
useCallbackвсегда возвращал одну и ту же ссылку на функцию, потому что случаи, когда нужно менять ссылку, намного более редкие.adminNiochen
Это нормальный паттерн использования реакт, когда реф не используется для дом элемента (если у тебя в return компонента какому-то диву реф присваивается, этот код выполнится тупо позже чем любые синхронные действия с рефом до return и всё перетрёт)
Finesse
В большинстве случаев ок. Но не во всех: