React: Creating Custom Hooks

Intro

Now that we are familiar with the power of hooks like setState and useEffect we can learn to create our own hooks to harness the power of re-usable functionality. Here are some basic notes on custom hooks.

  • Best way to create reusable code in a React project (besides components)
  • Created by extracting hook-related code out of a function component
  • Custom hooks always make use of at least one primitive hook internally
  • Each custom hook should have one purpose
  • Sort of an art form
  • Data-fetching is a good use case

Custom components are made for re-usable portions of JSX, custom hooks are really useful for the logic that governs those components, AKA, the top part of the Component.

The basic process for creating a custom hook looks something like this.

  • Identify each line of code related to some single purpose
  • Identify the inputs to that code
  • Identify the outputs to that code
  • Extract all of the code into a separate function, receiving the inputs as arguments, and returning the outputs

Sample

Let us take the following sample piece of code from an app that fetches a list of videos from Youtubes API, displays the videos in a list and has a display port that shows one of the videos. Like a very simple clone of Youtube.

App.js
const App = () => {
  const [videos, setVideos] = useState([])
  const [selectedVideo, setSelectedVideo] = useState(null)

  useEffect(() => {
    onTermSubmit("buildings")
  }, [])

  const onTermSubmit = async term => {
    const response = await youtube.get("/search", {
      params: {
        q: term,
      },
    })

    setVideos(response.data.items)
    setSelectedVideo(response.data.items[0])
  }
}

First we can look at all of these items and sort out which are pertaining to the LIST of videos, and which are pertaining to the SELECTED video.

Looking at our rules for hooks, we know that each hook is supposed to do one thing. So we would create a custom hook for creating the list of videos, and another hook for the selected video (if necessary). The point is, we don’t do both of these things in one hook.

Therefore let us focus on the hook for creating the list of videos.

Then we can look for the inputs. In this case there is just one input, which is the term that the user is entering into the search bar. Which in this case is buildings.

Next we can identify the outputs. The first obvious output would be the list of videos itself, which is represented by the variable videos. The second output would be the callback function onTermSubmit. This one is less obvious, however it helps to think of it like this. We are looking for all the variables that we will make use of in our component. And the callback function which makes the API call is one of those variables.

App.js
const App = () => {
  const [videos, setVideos] = useState([])
  useEffect(() => {
    onTermSubmit("buildings")
  }, [])

  const onTermSubmit = async term => {    const response = await youtube.get("/search", {
      params: {
        q: term,
      },
    })

    setVideos(response.data.items)
  }
}

We are looking for the output of the hook, not the output of the component.

Create Custom Hook

Hooks Directory

A good way to organize your hooks is to give them their own directory and then name them with the same convention as the primitive React hooks like useEffect. Therefore let’s create a hook called useVideos in our new directory.

Scaffolding The Hook

First let us scaffold the basic structure of our hook.

hooks/useVideos.js
import { useState, useEffect } from 'react;
// axios configuration file
import youtube from '../apis/youtube';

const useVideos = () => {
// functionality goes here
};

export default useVideos;

This hook will require an Axios configuration file, but that is just for this particular example, not standard boilerplate.

Next we can start filling in our videos functionality by copy/pasting it from our App file.

hooks/useVideos.js
import { useState, useEffect } from 'react;
// axios configuration file
import youtube from '../apis/youtube';

const useVideos = () => {
  const [videos, setVideos] = useState([]);  useEffect(() => {    onTermSubmit("buildings")  }, []);  const onTermSubmit = async term => {    const response = await youtube.get("/search", {      params: {        q: term,      },    })    setVideos(response.data.items)  }};

export default useVideos;

We have now moved all the code related to managing this one resource into our custom hook.

Wiring Inputs & Outputs

Inputs

We previously identified the term as our only input. Now that we turning this functionality into a re-usable hook, we want to replace the hard coded default search term “buildings” with a variable.

hooks/useVideos.js
import { useState, useEffect } from 'react;
// axios configuration file
import youtube from '../apis/youtube';

const useVideos = (defaultSearchTerm) => {  const [videos, setVideos] = useState([]);

  useEffect(() => {
    onTermSubmit(defaultSearchTerm);  }, []);

  const onTermSubmit = async term => {
    const response = await youtube.get("/search", {
      params: {
        q: term,
      },
    })

    setVideos(response.data.items)
  }
};

export default useVideos;

Outputs

We previously determined that the outputs are the list of videos, and the callback function onTermSubmit that queries the api. We can return these outputs in one of two ways.

The first way is to follow the convention of primitive hooks like useState, which returns an array with the state and a function to set state like this.

const [state, setState] = useState(default)

In this case we have a perfect example of this. We have a state, which is the list of videos, and we have a function that updates the list of videos onTermSubmit, which literally contains the setVideos function.

Therefore we would return our outputs like so.

hooks/useVideos.js
import { useState, useEffect } from 'react;
// axios configuration file
import youtube from '../apis/youtube';

const useVideos = (defaultSearchTerm) => {
  const [videos, setVideos] = useState([]);

  useEffect(() => {
    onTermSubmit(defaultSearchTerm);
  }, []);

  const onTermSubmit = async term => {
    const response = await youtube.get("/search", {
      params: {
        q: term,
      },
    })

    setVideos(response.data.items)
  };

  return [videos, onTermSubmit];};

export default useVideos;

A slightly more common method is to return an object with two properties instead of an array. Which would simply look like this.

hooks/useVideos.js
import { useState, useEffect } from 'react;
// axios configuration file
import youtube from '../apis/youtube';

const useVideos = (defaultSearchTerm) => {
  const [videos, setVideos] = useState([]);

  useEffect(() => {
    onTermSubmit(defaultSearchTerm);
  }, []);

  const onTermSubmit = async term => {
    const response = await youtube.get("/search", {
      params: {
        q: term,
      },
    })

    setVideos(response.data.items)
  };

  return { videos, onTermSubmit };};

export default useVideos;

The last change we could make here is that the callback onTermSubmit is not a very good descriptor of what is happening here, so let us change that to simply search.

hooks/useVideos.js
import { useState, useEffect } from 'react;
// axios configuration file
import youtube from '../apis/youtube';

const useVideos = (defaultSearchTerm) => {
  const [videos, setVideos] = useState([]);

  useEffect(() => {
    search(defaultSearchTerm);  }, []);

  const search = async term => {    const response = await youtube.get("/search", {
      params: {
        q: term,
      },
    })

    setVideos(response.data.items)
  };

  return [videos, search];};

export default useVideos;

We have now created a custom hook that can be used in any component, and as long as it is provided with a default search term, it will search for a list of videos and provide that list, and a function to update the list.

Using the Hook

Now it’s time to use the hook. Back in the App import the hook.

App.js
import useVideos from '../hooks/useVideos';
const App = () => {
  const [selectedVideo, setSelectedVideo] = useState(null)

  
    setSelectedVideo(response.data.items[0])
  }
}

Then we need to call our hook and feed it the input it requires, which is the defaultSearchTerm

App.js
import useVideos from '../hooks/useVideos';

const App = () => {
  const [selectedVideo, setSelectedVideo] = useState(null)
  useVideos('capybaras')  
    setSelectedVideo(response.data.items[0])
  }
}

and we know that function will return an array with a list of videos and callback to update that list, so we can destructure those items just like we do with the primitive hooks.

App.js
import useVideos from '../hooks/useVideos';

const App = () => {
  const [selectedVideo, setSelectedVideo] = useState(null)
  const [videos, search] = useVideos('capybaras')  
    setSelectedVideo(response.data.items[0])
  }
}

We will need to come up with a new way to handle our selected video. We can use a primitive hook for that.

App.js
import useVideos from '../hooks/useVideos';

const App = () => {
  const [selectedVideo, setSelectedVideo] = useState(null)
  const [videos, search] = useVideos('capybaras')
  
  // run when videos list is updated
  useEffect(() => {      setSelectedVideo(videos[0]);  }, [videos])}

And then the props in our JSX below will need to be updated.

App.js
import useVideos from '../hooks/useVideos';

const App = () => {
  const [selectedVideo, setSelectedVideo] = useState(null)
  const [videos, search] = useVideos('capybaras')
  
  useEffect(() => {
      setSelectedVideo(videos[0]);
  }, [videos])
}

return (
    <SearchBar onFormSubmit={search}>    <VideoDetail video={selectedVideo}>
)

That’s it. We have unlocked the magical world of custom hooks!