
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
В большинстве случаев ок. Но не во всех: