DEV Community

Dave M
Dave M

Posted on

Why You Should Be Writing React Custom Hooks

React Custom Hooks

You’re probably familiar with built-in React hooks like useEffect and useState. But have you explored writing custom hooks? Or thought about why you would want to?

“No, why would I?” You might ask. And since you’re playing along so kindly, I’ll tell you!

Custom hooks are a handy way to encapsulate hook-related logic that can be re-used across components when using component composition isn’t really something that will help, make sense, or just "look" semantically right.

Think of a custom hook as a super-powered helper function. According to the rules of hooks, you can't call a hook (like useEffect) in an ordinary helper function that is declared outside of a component. But you can call hooks inside custom hooks!

Additionally, if you have a component in which you have two or more separate pieces of useEffect logic going on, you might want to consider putting them into custom hooks to separate and name them, even if this isn’t logic that will be shared by any other component.

This is much like encapsulating logic into a well-named function for the sake of readability and code organization. After all, it’s a bit tough to read a string of useEffect routines and understand what’s going on. But if, on the other hand, you have one called something like useSyncCustomerRecordStore, then your consumer code is that much more readable.

Headless Components

Headless Components
It’s not quite a perfect comparison, but in a way, you can think of custom hooks as being a bit like headless components. Mostly because they can call hooks themselves, such as useEffect and useState. These built-in React hooks can work in custom hooks the same way they work in components.

The difference between a custom hook and a component is that a custom hook will return values, not React components or markup. In this way, they’re sort of like component helpers.

The Shape Of A Custom Hook

Shape Of A Hook
Custom hooks are really just:

  • Functions whose names begin with 'use...'
  • Functions which can call other hooks

A simple custom hook might look like this:

// Custom hook code
function useMyCustomHook(someDataKey) {

    const [someValue, setSomeValue] = useState(null);

    useEffect(() => {
        setSomeValue(useSomeOtherHook(someDataKey));
    }, [someDataKey]);

    return someNewValue;
}

// Consumer component code
function MyAwesomeComponent({someDataKey}) {

    const someValue = useMyCustomHook(someDataKey);

    return (<p>The new value is {someValue}</p>);
}
Enter fullscreen mode Exit fullscreen mode

Example: Page Data

I’m currently working on an enterprise application suite realized in the form of micro-service applications. To the user, it seems like one large application, but really, under the hood, it’s a collection of several independent React apps.

These apps need to refer to each others’ pages with links and common titles, and that data — called pageData — is set up in a context provider so that any component at any level in the apps can access it with a useContext hook.

Now, it is pretty simple to use this data without writing a custom hook. All a consumer component has to do is import the PageDataContext and then call useContext on it, like this:

// External Libraries
import React, { useContext } from 'react';

// App Modules
import PageDataContext from './PageDataContext';

function MyComponent() {

    const pageData = useContext(PageDataContext);

    return (<h1>{pageData.home.title}</h1>);
}
Enter fullscreen mode Exit fullscreen mode

Okay, So Why Use A Custom Hook For This?

Okay, so that's pretty simple, right? It's only three lines of code: two import statements, and a call to useContext. In that case, why am I still recommending a custom hook for a situation like this?

Here are a few reasons, from least to most important:

Eliminating Boilerplate Adds Up

If you just look at this one example, I'm only eliminating one line of boilerplate, because I will still have to import my custom hook, usePageData. I only really eliminate the line that imports useContext.

So what's the big deal? The thing is, just about every page in my enterprise app suite needs to use this pageData object, so we're talking hundreds of components. If we eliminate even one line of boilerplate from each one, we're talking hundreds of lines.

And believe me, just writing that extra line every time I create a new page feels that much more annoying, so there's a sort of pscychological/motivational benefit that adds up over time, too.

Well-Named Functions

If you've used hooks like useEffect much in your code, you've probably come across situations where there are two or three pieces of useEffect logic (either in separate calls to useEffect, or combined into one). This quickly gets hard to take in when you're reading the code.

If you're like me, you wind up putting comments about each piece of useEffect logic, such as:

    // Get the page data
    useEffect(() {
        // ...  stuff happens here
    });
Enter fullscreen mode Exit fullscreen mode

But one of the fundamental concepts of readable code is noticing where you're writing blocks of comments in big dumping ground "main" type functions, and instead separating those pieces of logic into their own, individual, well-named functions. Another developer reading your code is going to have a much easier time taking it all in when these details are abstracted away from the big picture. But when they're ready to drill into detail, they can go look at the function declaration.

The same is true of custom hooks. If I see this in the component code, I have a pretty good idea of what is going on:

   const pageData = useGetPageData();
Enter fullscreen mode Exit fullscreen mode

Encapsulation

I've saved the most important reason for last, and that's that it is good to encapsulate the logic in one place. Sure it's only two lines of code, but what if we decide to store pageData in a Redux or Mobx store instead of React Context?

If we're already using a custom hook, no problem! We just change the internal code in the hook and return the same pageData object back to the consumer code. What we don't have to do is go and update hundreds of components to import, say, useSelector, and then call it instead of useContext.

What useGetPageData Looks Like

It's dead simple! Just:

// External Libraries
import { useContext } from React;

// App Modules
import PageDataContext from './PageDataContext';

function useGetPageData() {
    return useContext(PageDataContext);
}
Enter fullscreen mode Exit fullscreen mode

Other Things You Can Do With Custom Hooks

Creative Hooking
The example I gave for page data is intentionally very basic, but there are many more useful things you can do with custom hooks, such as encapsulating shared logic for updating and reading Redux state. Just think of anything you want to do with hooks but for which you want to avoid a bunch of copy/paste boilerplate, and you're set to start getting creative with it.

Have fun!

Top comments (5)

Collapse
 
ganeshshetty195 profile image
Ganesh Shetty

You have mentioned, you can't call a hook (like useEffect) in an ordinary helper function that is declared outside of a component

But we can call hook in any Plain JS function rite

Collapse
 
ganeshshetty195 profile image
Ganesh Shetty

import { useState, useEffect } from "react";
const CustomUtil = (url) => {
    const [data, setData] = useState(null);
    useEffect(() => {
        fetch(url)
            .then((res) => res.json())
            .then((data) => setData(data));
    }, [url]);
    return [data];
};

export default CustomUtil;

`this is a plain function with hooks in it
Enter fullscreen mode Exit fullscreen mode
Collapse
 
codescript profile image
Cobbygraves

No you can't call hooks in a plain JavaScript function either outside the component function or inside

Collapse
 
gaurav5430 profile image
Gaurav Gupta

a custom hook can return React component or markup

Collapse
 
mr_devboy profile image
Bohdan Yatsenko

Short and clearly. Thank you.