Switch to 🌑 mode

How to Create a Reusable Custom Hook with React

August 09, 2022

Today I would like to speak about the advantages you will have when you start using hooks in your project. And particularly about the composition of hooks. Yes! You can create your own custom hooks to implement logic where you compose hooks together. Let’s take a look at how we can do this.

React hooks were introduced with React 16.8. Since then they have become very popular. In this short article, we will not go into the basics. If you are not familiar with hook’s concept take a look at the official docs.

We will look at the application which is using Redux as an example, but the core idea can be applied to any library exposing hooks or you can compose your custom hooks as well.

In our project, Redux is used as global storage. React-Redux library exposes to us hooks that we can use to manage our store. One of them is a useDispatch hook that basically helps us to dispatch actions.

import { useDispatch } from "react-redux"

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: "increment-counter" })}>
        Increment counter
      </button>
    </div>
  )
}

As official React-Redux docs suggests: “When passing a callback using dispatch to a child component, you may sometimes want to memoize it with useCallback”. dispatch function reference will be stable as long as the same store instance is being passed to the Provider. But there is no guarantee that the store will be stable so we need to add it as a dependency for useCallback.

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()
  const incrementCounter = useCallback(
    () => dispatch({ type: "increment-counter" }),
    [dispatch]
  )

  return (
    <div>
      <span>{value}</span>
      <button onClick={incrementCounter}>Increment counter</button>
    </div>
  )
}

Cool! And now let’s think about our application. Probably it will have many components where we would like to retrieve data stored in Redux global store. What we can do to simplify our life is to create a custom hook and move logic there. After, we can reuse it within our application.

// use-action.js

import { useCallback } from "react"
import { useDispatch } from "react-redux"

export const useAction = action => {
  const dispatch = useDispatch()
  return useCallback(() => dispatch(action), [dispatch, action])
}

Please note, that we added action argument to our array of dependencies. Because we need to be sure that the reference we are getting from useAction hook is updated only if the action param changes.

Now our component will look like this:

const incrementAction = { type: "increment-counter" }
export const CounterComponent = ({ value }) => {
  const incrementCounter = useAction(incrementAction)
  return (
    <div>
      <span>{value}</span>
      <button onClick={incrementCounter}>Increment counter</button>
    </div>
  )
}

We extracted incrementAction outside the component because otherwise on each rerender we pass a new object as an argument and therefore create a new function.

That already looks better! But let’s enhance our useAction hook even more. Pretty often you need to pass some payload with your action. To achieve that we need to pass an additional parameter to useAction hook.

Note, that we are using useMemo instead useCallback here because we can memoize the return value of the function.

// use-action.js

import { useMemo } from "react"
import { useDispatch } from "react-redux"

export const useAction = (action, value) => {
  const dispatch = useDispatch()
  return useMemo(() => dispatch(action(value)), [dispatch, action, value])
}

And our realistic structure will be:

// actions.js

export const incrementAction = ( data ) => ({
 type: 'increment-counter',
 payload: data
})

// counter-component.js

import { incrementAction } from 'actions'
import { useAction } from 'use-action'

export const CounterComponent = React.memo(({ value, incrementor }) => {
 const incrementCounter = useAction(incrementAction, incrementor)
 return (
   <div>
     <span>{value}</span>
     <button onClick={incrementCounter}>
       Increment counter
     </button>
   </div>
 ))

We used memo to memoize our CounterComponent so it will only rerender if our properties changes. Also, we add a useMemo hook to memoize the result of incrementCounter.

Our custom hook is flexible and can be used within different components across our application. That is how easily we can achieve a decent level of reusability of our dispatcher, using React hooks API.


Profile picture

Written by Pavel Ivanov in sunny Spain. Reach me on LinkedIn. Or just take a look at my CV.

* use of site content is strictly prohibited without prior written approval