Why and When To Use Custom Hooks
The main reason to write a custom hook is for code reusability. For example, instead of writing the same code across multiple components that use the same common stateful logic (say a “setState” or localStorage logic), you can put that code inside a custom hook and reuse it.
Consider the example below:
I want to use the data that Strava provides for my run workouts in my third-party application. Through the Strava API I can get activity info like route map, distance, achievements, and any other info that Strava provides. Let’s say I have a component that displays my past 10 activities. Here’s what that component will look like:
import React, { useState, useEffect } from 'react';
import './App.css';
function App() {
const [isLoading, setIsLoading] = useState(true)
const [activities, setActivities] = useState([])
//Strava Credentials
let clientID = "71827";
let clientSecret = "[YOUR_CLIENT_SECRET]";
// refresh token and call address
const refreshToken = "a676424538bc556eec048a03042a72d8c709adda";
const refreshEndpoint = `https://www.strava.com/oauth/token?client_id=${clientID}&client_secret=${clientSecret}&refresh_token=${refreshToken}&grant_type=refresh_token`;
// endpoint for read-all activities. temporary token is added in getActivities()
const activitiesEndpoint = `https://www.strava.com/api/v3/athlete/activities?access_token=`
// Use refresh token to get current access token
useEffect(() => {
fetch(refreshEndpoint, {
method: 'POST'
})
.then(res => res.json())
.then(result => getActivities(result.access_token))
}, [refreshEndpoint])
function getActivities(accessToken){
console.log(activitiesEndpoint + accessToken)
fetch(activitiesEndpoint + accessToken)
.then(res => res.json())
.then(data => {setActivities(data); setIsLoading(prev => !prev)})
.catch(e => console.log(e))
}
function showActivities(){
if(isLoading) return <>LOADING</>
if(!isLoading) {
return activities.map((activity) => {
return <div key={activity.upload_id_str}> {activity.name} </div>
})
}
}
return (
<div className="App">
{showActivities()}
</div>
);
}
export default App;
How To Use Custom Hooks
Now let’s say in this app we have multiple components that use the activities data. These components will have the loading state and the activities state in common. Here’s where we want to extract the shared state and the shared useEffect fetch hook into our own custom hook so we don’t repeat it. This is what that custom hook will look like:
import React, { useState, useEffect } from 'react';
function useActivities() {
const [isLoading, setIsLoading] = useState(true)
const [activities, setActivities] = useState([])
//Strava Credentials
let clientID = "71827";
let clientSecret = "[YOUR_CLIENT_SECRET]";
// refresh token and call address
const refreshToken = "a676424538bc556eec048a03042a72d8c709adda";
const refreshEndpoint = `https://www.strava.com/oauth/token?client_id=${clientID}&client_secret=${clientSecret}&refresh_token=${refreshToken}&grant_type=refresh_token`;
// endpoint for read-all activities. temporary token is added in getActivities()
const activitiesEndpoint = `https://www.strava.com/api/v3/athlete/activities?access_token=`
// Use refresh token to get current access token
useEffect(() => {
fetch(refreshEndpoint, {
method: 'POST'
})
.then(res => res.json())
.then(result => getActivities(result.access_token))
}, [refreshEndpoint])
// use current access token to call all activities
function getActivities(accessToken){
console.log(activitiesEndpoint + accessToken)
fetch(activitiesEndpoint + accessToken)
.then(res => res.json())
.then(data => {setActivities(data); setIsLoading(prev => !prev)})
.catch(e => console.log(e))
}
return [isLoading, activities]
}
export default useActivities;
And then we can call the hook from our component like this:
import React from 'react';
import './App.css';
import useActivitiesData from './useActivitiesData';
function App() {
//GET REUSABLE STATE FROM CUSTOM HOOK
const [isLoading, activities] = useActivitiesData();
function showActivities(){
if(isLoading) return <>LOADING</>
if(!isLoading) {
return activities.map((activity) => {
return <div key={activity.upload_id_str}> {activity.name} </div>
})
}
}
return (
<div className="App">
{showActivities()}
</div>
);
}
export default App;
This way we can keep our components as simple as possible and isolate the testable logic in a custom hook function, as well as making shared logic between components reusable.
Something to note: there is a naming convention for custom hooks! React linting plugin requires that we use the naming convention – “use[something]” (in our example, useActivitiesData) in order to find bugs. By keeping things consistent, it becomes a lot easier to flag programming errors, stylistic errors, and error-prone constructs.
Finally, since the introduction of hooks, lots of custom hooks libraries have been added for common front-end scenarios such as using localstorage, memoization, or routing (useLocalStorage, useMemoCompare, and useRoutes are some examples), so we don’t have to worry about reinventing the wheel in most situations. Here’s a nice resource to browse some of those examples: https://usehooks.com/