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.
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.
const App = () => {
const [videos, setVideos] = useState([]) // highlight-line
useEffect(() => {
onTermSubmit("buildings")
}, [])
const onTermSubmit = async term => { // highlight-line
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.
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.
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.
import { useState, useEffect } from 'react;
// axios configuration file
import youtube from '../apis/youtube';
const useVideos = (defaultSearchTerm) => { // highlight-line
const [videos, setVideos] = useState([]);
useEffect(() => {
onTermSubmit(defaultSearchTerm); // highlight-line
}, []);
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.
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]; // highlight-line
};
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.
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 }; // highlight-line
};
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
.
import { useState, useEffect } from 'react;
// axios configuration file
import youtube from '../apis/youtube';
const useVideos = (defaultSearchTerm) => {
const [videos, setVideos] = useState([]);
useEffect(() => {
search(defaultSearchTerm); // highlight-line
}, []);
const search = async term => { // highlight-line
const response = await youtube.get("/search", {
params: {
q: term,
},
})
setVideos(response.data.items)
};
return [videos, search]; // highlight-line
};
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.
import useVideos from '../hooks/useVideos'; // highlight-line
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
import useVideos from '../hooks/useVideos';
const App = () => {
const [selectedVideo, setSelectedVideo] = useState(null)
useVideos('capybaras') // highlight-line
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.
import useVideos from '../hooks/useVideos';
const App = () => {
const [selectedVideo, setSelectedVideo] = useState(null)
const [videos, search] = useVideos('capybaras') // highlight-line
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.
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.
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}> // highlight-line
<VideoDetail video={selectedVideo}>
)
That's it. We have unlocked the magical world of custom hooks!
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.