#100DaysOfMERN - Day 45

#100DaysOfMERN - Day 45

·

7 min read

✏ React Custom Hooks

Custom hooks are a great way to extract (repetitive) logic from your components. They can be a bit puzzling at first, but like always in coding, you have to write them out a few times until they become familiar. In the end, they're just functions.

To demonstrate, I'll show an example for a custom useFetch hook, including a possibility to cancel the fetch, when it's no longer needed (for example because the user already navigated to a different view, in which case you'd be trying to update state of an unmounted component).


✏ A little introduction

As mentioned, a custom hook is just a function. It gets some input, and delivers some output. It's not special per sé, but if you use it as a hook, you're subscribing to the Rules of Hooks (and the linter will yell at you if you don't follow these rules).

They're quite simple though. Like React's own hooks useState and useEffect, you should always call your custom hook at the top level, and only from React functions. End of rules. If you're nice, you also follow the convention to give your hook a name that starts with "use".

Some differences between React hooks and your custom hooks:

You have total freedom concerning the input and output, React hooks all have a clearly defined signature. useState takes an initial value, and always returns an array with two items, a value and a function to update the value. useEffect takes a callback function and an array of dependencies, and returns undefined. With a custom hook, you can have as many parameters and return values as you like.

Also note that:

  • you can call a React hook within your custom hook

  • you can not call a custom hook within a React hook

  • you can call a custom hook within a custom hook

It's not as confusing as it sounds, let's see some code. A great way to get familiar with custom hooks is to implement one that is absolutely pointless and does absolutely nothing. Instead of logging something when it runs.


✏ Pointless Custom Hook Example

I'll call that hook useUseless. It'll have some pointless state as return value, and will also log that state when it executes:

import { useState, useEffect } from 'react';

const useUseless = () => {
    const [pointless] = useState(0);

    console.log(`pointless is still ${pointless}`);

    return pointless;
};

export default useUseless;

Within your App component, you can import that now and use it to get and display the pointless value:

import useUseless from '../hooks/useUseless.js';

function App() {

    const pointless = useUseless();

    return (
         <>
             <p>Pointless value is: {pointless}</p>
         </>
    );
}

export default App;

Before, the pointless value could've been some state maintained by App.js. If you extract that into a hook, App doesn't need state anymore, and gets its value from the hook instead.

How to make that hook run again when needed

Let's say we have a button in App that should trigger a "re-fetch" of the pointless value. Because of the Rules, you can't do something like this:

function App() {

    let pointless = useUseless();

    askAgain = () => {
        pointless = useUseless()
    }

    return (
         <>
             <p>Pointless value is: {pointless}</p>
             <button onClick={askAgain}>Ask again</button>
         </>
    );
}

export default App;

If you're very used to Vanilla JS, you'd probably do something like this, and try to manually let the hook run again to reassign the value. However, things are different in React. If something in App changes (state, or it received new props), it will automatically run its code again and also call the hook.

To trigger that, the button could do something (similarly) useless and increment a counter:

import { useState } from 'react';

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

    const pointless = useUseless();

    askAgain = () => setCount(prev => prev+1)

    return (
         <>
             <p>Pointless value is: {pointless}</p>
             <button onClick={askAgain}>Ask again</button>
         </>
    );
}

Changing the value of count will make useUseless execute again. But what if you want the opposite, because the hook does an unnecessary and expensive operation?

How to NOT make that hook run again when App rerenders

That's not really possible, unless I'm unaware of a way, but to avoid pointless calculations within the hook, a simple way would be to pass count as argument:

const useUseless = count => {
    const [pointless] = useState(0);

    if (count < 5) {
        console.log(`pointless is still ${pointless}`);
        // lots of other code
    }

    return pointless;
};

Another possibility is to wrap those parts with useEffect:

const useUseless = count => {
    const [pointless] = useState(0);

    console.log(`pointless is still ${pointless}`);

    useEffect(() => {
        console.log(`in effect, pointless is still ${pointless}`);
        // lots of other code
    }, [pointless]);

    return pointless;
};

Since the value of pointless doesn't change, the effect will run only once.

But enough of this. A useless hook is a great way to explore what you can and cannot do with custom hooks, and to test what runs when, but now for a more useful example.

(how often can you write "use" in a blog post before you feel awkward)


✏ Useful Custom Hook Example

One very typical use case for a custom hook is when you have multiple components that all fetch some data when they first mount. Each of those components has their own useEffect with their dependency array, and the same logic: making a GET request to an API.

To extract that into a custom hook, the code would have to be a bit more generalised, to make it work with different components. If one part of the app fetches a book list, and another part fetches details about a specific book, they would set their state accordingly like this - the repetitiveness is obvious:

// App.js
const [bookList, setBookList] = useState(initialValue);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
    // fetch to '/api/books'
}, [])



// BookDetails.js
const [book, setBook] = useState(initialValue);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
    // fetch to '/api/books/id=5'
}, [])

To make the custom hook work with both of them, it would have to be agnostic to the URL it is fetching from, and to the type of data it is requesting. But overall, it takes only little adjustments.

The useFetch hook makes a GET request to the specified URL using axios, and returns the data (optionally some information about whether it's still loading):

import { useState, useEffect } from 'react';
import axios from 'axios';

const useFetch = url => {
    const [isLoading, setIsLoading] = useState(false);
    const [data, setData] = useState(null);

    useEffect(() => {
        setIsLoading(true);

        axios.get(url)
            .then(res => {
                setData(res.data);
                setIsLoading(false);
            })
            .catch(err => {
                console.log(err);
                setIsLoading(false);
            });

    }, [url]);

    return [isLoading, data];
};

export default useFetch;

Importing this in App, just like before:

import { useState } from 'react';
import useFetch from '../hooks/useFetch.js';

function App() {
    const [isLoading, bookList] = useFetch(url);

    return (
         <>
             {!bookList && !isLoading && <p>Something went wrong...</p>}

             <ul>
                {bookList && bookList.map(book => <li key={book.id}>{book.title}</li>)}
             </ul>
         </>
    );
}

export default App;

✏ Cancelling the request

If you're fetching a lot of data, or if the connection is slow, it can happen that the user navigates away from the view, while the fetch is still active. Upon (eventual) completion, the view that had requested the data has already unmounted, and you'd get a warning in the console, offering a solution:

useeffect-error-cleanup.jpg

If you use the Fetch API, you can abort a request using the AbortController. With axios, it works a little different: You can pass a cancel token as second parameter into the request, and call axios' .cancel method when the component unmounts:

import { useState, useEffect } from 'react';
import axios from 'axios';

const useFetch = url => {
    const [isLoading, setIsLoading] = useState(false);
    const [data, setData] = useState(null);

    useEffect(() => {
        setIsLoading(true);

        // cancel token
        const source = axios.CancelToken.source();

        axios.get(url, { cancelToken: source.token })
            .then(res => {
                setData(res.data);
                setIsLoading(false);
            })
            .catch(err => {
                console.log(err);
                setIsLoading(false);
            });

        // return a cleanup callback from useEffect
        return () => source.cancel();

    }, [url]);

    return [isLoading, data];
};

export default useFetch;

So much for custom hooks. If you're up for experiments, you can import the useUseless hook in useFetch and use it both there and in App. Playing a little with different hooks at different places for a while has definitely helped me to gain some confidence in how to use them.


✏ Resources

React.js Hooks Crash Course


✏ Recap

This post covered:

  • useless and useful custom hooks

  • how to cancel an axios request


✏ Thanks for reading!

I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subscribe to my newsletter. Until next time 👋


✏ Previous Posts

You can find an overview of all previous posts with tags and tag search here:

#100DaysOfMERN - The App