Пять нужных кастом-хуков для React
- воскресенье, 9 февраля 2025 г. в 00:00:05
React предоставляет программисту прекрасный базовый набор хуков и с каждой версией их количество и функционал увеличивается.
Трудно представить код современного React-приложения без таких функций как useState
, useEffect
, useRef
и так далее.
Однако, в повседневной жизни мы часто решаем рутинные задачи, многие из которых могут быть автоматизированы.
Создание кастом-хуков это прекрасная возможность выделить часто переиспользуемый код в отдельные сущности.
Это помогает содержать основной код компонента в чистоте и избавляет нас от мелких ошибок, которые могут остаться незамеченными, когда вы пишете один и тот же код заново.
Ниже мы рассмотрим примеры некоторых из них.
Приходилось ли вам когда-нибудь создавать 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>}
</>
)
}
Случались ли у вас такое, что 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>
)
}
Порой, в компоненте мы создаем 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>
</>
)
}
Все мы привыкли пользоваться хуком 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>
)
}
При взаимодействии пользователя с интерйфесом через такие события как: ввод текста, изменение ширины окна браузера, скролл - может возникать чрезмерно большое количество вызовов функций-коллбэков.
Зачастую нам это не нужно и мы хотим отложить вызов до момента, когда пользователь закончит действие, чтобы затем выполнить полезный код.
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, а также многие другие.