Search
Write a publication
Pull to refresh

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

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

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.