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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Фото Tatiana Rodriguez
Фото 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/


Интересные статьи

Интересные статьи

Один из крупнейших российских дистрибьюторов электротехники «Русский свет» выпустил под своим брендом лампочки, прожекторы и разнообразные светильники. У всех ламп заявлены честные характеристики,...
И рассказываем, чтобы сэкономить Ваше времяЧтобы перелезть через этот высоченный забор понимания, как же все-таки работает сейчас КЭДО в Битрикс24 мы...• Просмотрели 384 минуты видеоуроков и рол...
Был такой функционал в Битрикс24 - Face-трекер. В 2022 году его отключили, о чем вышло официальное уведомлениеПричина для отключения - "Провайдер, с которым мы сотрудничаем, сейчас не может предостави...
Во второй части статьи про bitrix кластер мы будет рассматривать настройку percona xtradb cluster mysql 5.7, настройка арбитра, настройка удобного dashboard.Если вы пропустили предыдущие части: Первая...
Недавно я рассказывал про ноутбук Compaq LTE Elite на базе 486-го процессора. Это был один из самых крутых ноутбуков 1994 года выпуска. Сегодня я расскажу о ноутбуке 1996 года. Из настоящего кажется, ...