Пять нужных кастом-хуков для React

от автора

Фото Tatiana Rodriguez

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, а также многие другие.


ссылка на оригинал статьи https://habr.com/ru/articles/880570/