Custom React Hooks and When to Use Them

Stephen Hanson

React hooks are functions that let you use and interact with state in React function components. React comes with some built-in hooks, the most commonly used ones being useState, useRef, and useEffect. The former two are used to store data across renders, while the latter is used to execute side effects when that data changes.

We can also build our own hooks using the built-in hooks as building blocks. These are often referred to as “custom hooks” to differentiate them from the built-in hooks. In my experience, custom hooks are the most underused React abstraction. Developers who are newer to React can struggle to understand how to build custom hooks or when to use them. This post will focus on answering those questions.

Built-in hooks

As a quick refresher on hooks, here’s an example of a React component that keeps track of a count and logs to the console when the count changes:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`Count changed to ${count}`);
  }, [count]);

  return (
    <div>
      The count is: {count}.
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

The component above stores count as state using the useState hook and also logs the count anytime it changes by using the useEffect hook.

What are custom hooks

The React documentation on building your own hooks defines custom hooks in a simple way:

A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.

That’s it! If you have code in a component that you feel would make sense to extract, either for reuse elsewhere or to keep the component simpler, you can pull that out into a function. If that function calls other hooks, like useEffect, useState, or maybe another custom hook, then your function is itself a hook, and, by convention, should be given a name that starts with “use” to make it clear that it is a hook.

If hooks are so similar to regular functions, you might wonder why we even have the “hook” concept. The reason we need this concept is because hooks are special. They are functions that also have state that is persisted under the hood by React across calls. Because of this, there are rules of hooks that must always be followed so React doesn’t get confused. The use... naming convention helps us identify which functions are hooks so we can be sure to follow the rules.

Following are some examples of when it might make sense to create a custom hook.

Reusable custom hooks

Most React developers are familiar with extracting reusable functionality into components or functions but are sometimes not as comfortable extracting code into hooks. If we have code we would like to extract from a component, a custom hook might be the proper extraction if the following conditions are met:

  • The extracted code has no JSX output (if it does, then you should create a component instead)
  • AND the extracted code includes calls to other hooks (if it doesn’t, then create a regular function instead)

Let’s take a simple example of a reusable hook. Say in our app, we want to change the document title every time we render a new page. We might have this code in many of our components:

function HomePage {
  useEffect(() => {
    document.title = 'Home'
  }, [])

  return <div>Home Page...</div>
}

We could simplify this a bit by extracting the title logic into a hook:

function useTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

Now, our home page looks like this:

function HomePage {
  useTitle('Home')

  return <div>Home Page...</div>
}

This was a simple example, and it likely would have been fine to not create the hook in this case, but as functionality gets more complex or is used in more places, it becomes more and more useful to extract the behavior.

Or, here’s another reusable custom hook that can be useful:

/**
 * Just like `useState`, but it keeps track of the previous value and returns it
 * in the array.
 * Usage:
 *   const [value, previousValue, setValue] = useStateWithPrevious('initialValue')
 */
function useStateWithPrevious(initialValue) => {
  const reducer = (state, value) => ({
    value,
    previousValue: state.value,
  });

  const [{ value, previousValue }, setValue] = useReducer(reducer, {
    value: initialValue,
  });

  return [value, previousValue, setValue];
};

function App() {
  const [name, previousName, setName] = useStateWithPrevious('')
  // now we always have access to the previous value
}

For some more custom hook inspiration, I recommend checking out react-use.

Non-reusable custom hooks to extract functionality

Just like with components, sometimes it can be useful to create a custom hook even if it isn’t reusable as a way to extract functionality to make the parent component easier to understand.

When assessing if it makes sense to extract a non-reusable hook, I use the same criteria I would use for any other extraction:

  • Is the parent code more understandable with the abstraction?
  • Is the abstraction isolated enough to make sense on its own?

If the answer to either of the above is no, then it does not seem that the hook is a clean abstraction, and the code is probably easier to understand with the hook inlined back into the component. Even if both answers are yes, still ask if the abstraction is worth the extra complexity created by this new layer.

Determining how much to extract

It’s not always clear where best to define your abstraction. This choice can affect if your new abstraction should be a hook vs. a component vs. a regular function.

Consider this contrived component where we access the user from a global store (like Redux or React Context) and then display a message like “Hello, Dr. Jane García”. Let’s also assume our app needs to display the same message in a few different places, so there’s a desire to extract some of this functionality for reuse.

Here’s the original component with no extraction:

function MyComponent({ displayTitle }) {
  const user = useSelector((state) => state.user);

  const formattedName = _.compact([
    displayTitle ? user.title : null,
    user.firstName,
    user.middleName,
    user.lastName,
  ]).join(" ");

  return (
    <>
      <h1>Hello, {formattedName)}</h1>
      <p>Some text</p>
    </>
  );
}

The simplest extraction we could make is to pull out the logic for formatting the user’s name into a separate function:

function formatUserName(user, { displayTitle }) {
  return _.compact([
    displayTitle ? user.title : null,
    user.firstName,
    user.middleName,
    user.lastName,
  ]).join(" ");
}

function MyComponent({ displayTitle }) {
  const user = useSelector((state) => state.user);

  // extracted logic to a function
  const formattedName = formatUserName(user, { displayTitle });

  return (
    <>
      <h1>Hello, {formattedName}</h1>
      <p>Some text</p>
    </>
  );
}

Another option would be to extract the useSelector call as well, which means we are now creating a hook instead of a regular function (remember: if your function calls a hook, then your function is a hook):

// extracted everything, including the global store access, so this is a hook
function useFormattedUserName({ displayTitle }) {
  const user = useSelector((state) => state.user);
  const formattedName = _.compact([
    displayTitle ? user.title : null,
    user.firstName,
    user.middleName,
    user.lastName,
  ]).join(" ");

  return formattedName;
}

function MyComponent({ displayTitle }) {
  const formattedName = useFormattedUserName({ displayTitle });

  return (
    <>
      <h1>Hello, {formattedName}</h1>
      <p>Some text</p>
    </>
  );
}

A third option, that is even heavier-handed would be to extract this into a component:

function UserGreeting({ displayTitle }) {
  const user = useSelector((state) => state.user);
  const formattedName = _.compact([
    displayTitle ? user.title : null,
    user.firstName,
    user.middleName,
    user.lastName,
  ]).join(" ");

  return <h1>Hello, {formattedName}</h1>;
}

function MyComponent({ displayTitle }) {
  return (
    <>
      <UserGreeting displayTitle={displayTitle} />
      <p>Some text</p>
    </>
  );
}

The examples above are fairly contrived, but they demonstrate some of the nuance around choosing the right abstraction point. There are always multiple ways to build abstractions.

As we’ll discuss in the next section, it’s usually best to create smaller abstractions, even at the risk of a little duplicated code. So in this case, it’s probably best to just create the formatUserName function and allow a bit of duplication with the useSelector call and JSX output in the various places we display a greeting to the user.

If we had instead created the <UserGreeting /> component, a larger abstraction that includes displaying/styling, then we would be forced to modify that abstraction anytime we have a need to display the user’s name with a different style, salutation, or HTML tag. This would introduce an undesirable coupling between different parts of the codebase as that component changes.

The cost of abstractions

As we discussed in the last section, abstractions don’t come for free. They have downsides such as creating coupling and introducing extra layers and indirection. Sometimes the best answer is to forego the abstraction and have some duplication.

Over time, requirements change. When we update an abstraction, we have to consider how an update affects all of the call sites. This slows us down and also makes it easy to introduce bugs.

In addition to creating coupling, abstractions also add extra layers and indirection in an app that can make the code harder to understand. Our brains can only hold onto a few layers at a time in memory, so as complexity increases, our understanding of the functionality decreases.

The React documentation advises the following, regarding abstractions:

Try to resist adding abstraction too early. Now that function components can do more, it’s likely that the average function component in your codebase will become longer. This is normal — don’t feel like you have to immediately split it into Hooks. But we also encourage you to start spotting cases where a custom Hook could hide complex logic behind a simple interface, or help untangle a messy component.

If you aren’t sure if an abstraction needs to be added, it probably does not! If you are unsure how best to shape an abstraction, consider holding off until the needs are clear. As Sandi Metz says, “duplication is far cheaper than the wrong abstraction”.

For further information on downsides of using too many (or wrong) abstractions, I recommend watching Dan Abramov’s talk, The Wet Codebase.

Conclusion

Custom hooks are a useful abstraction. Understanding them and when to use them is key to building a maintainable React application. And while components, hooks, functions, and other patterns each have their place in the developer’s toolkit, we should also be judicious when creating abstractions, understanding that they create coupling and additional layers and indirection.