Five Must-Have Custom Hooks for React
Programming

Five Must-Have Custom Hooks for React

6 min read

React provides the programmer with a great basic set of hooks, and with each version their number and functionality increases.

It’s hard to imagine the code of a modern React application without functions like useState, useEffect, useRefand so on.

However, in everyday life we ​​often solve routine tasks, many of which can be automated.

Creating custom hooks is a great way to separate frequently reused code into separate entities.

This helps keep the core code of the component clean and prevents us from making small errors that might go unnoticed when you write the same code over and over again.

Below we will look at examples of some of them.  

1. useToggle

Have you ever created useStatea that only contained two values true​​and falseand was called something like isActive, isCheckedor isOpen?

If the answer is yes, then you’ve definitely come to the right place! The first hook we’ll look at encapsulates this logic, returning a value and methods to change its state.

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]
}

It can be easily extended with functions that will explicitly set the state value to trueor false.

Let’s look at an example of use:

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

Have you ever had a situation where  :hoverfor some reason css couldn’t be used and there was nothing left to do but simulate this behavior using mouseEnterand mouseLeave?

If the answer is yes again, then I am ready to introduce you to a second custom hook that will do it for you.

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]
}

The usage of this hook is a bit non-standard, let’s look at an example:

export function Component() {
  const [hoverRef, isHovered] = useHover<HTMLDivElement>()

  return (
    <div
      ref={hoverRef}
      style={{ backgroundColor: isHovered ? 'lightblue' : 'lightgray' }}
    >
      {isHovered ? 'hovered' : 'not hovered'}
    </div>
  )
}

3. useDerivedState

Sometimes, in a component we create a useState, the initial value of which is some value from the props.

If the props change, the corresponding changes will not affect our local state and it will continue to store the outdated value.

To avoid this we can use the following hook:

export function useDerivedState<T>(
  propValue: T,
): [T, Dispatch<SetStateAction<T>>] {
  const [state, setState] = useState(propValue)

  useEffect(() => {
    setState(propValue)
  }, [propValue])

  return [state, setState]
}

This can be useful in cases with user input where we want to change the value and only then save it or return the original value.

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

We are all used to using a hook useCallbackthat caches a function between re-renders.

However, if there are values ​​in the array of dependencies of this function that have changed, the function will be re-created.

From a performance optimization perspective, this may be overkill since your callback may never be called.

If you want to get a stable reference to a callback that does not change from render to render, but at the time of the call always contains the current values ​​of the variables on which it depends, then you can use the following hook:

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)
  }, [])
}

Most often, this hook is used for callbacks that are delayed in time and initiated by the user. A good example would be replacing regular callbacks for passing to 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

When the user interacts with the interface through events such as text input, changing the width of the browser window, scrolling, an excessively large number of calls to callback functions may occur.

Often we don’t need this and want to delay the call until the user has completed the action so that we can then execute useful code.

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
}

This hook can be extended with helper functions such as cancel, isPendingand flush.

Let’s look at an example of use:

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}
    />
  )
}

That’s it! The number and functionality of custom hooks can be very diverse, everything is limited only by your imagination and needs.

For more examples, you can refer to libraries like react-use or usehooks-ts , as well as many others.