We’ll be at Google Cloud Next ’24 in Las Vegas, April 9-11

Back to top
Design - Detroit Labs
Pegah Eizadkhah

Pegah Eizadkhah

Pegah is a full stack developer at Detroit Labs with over three years of experience building scalable and maintainable applications. When she is not honing her coding skills, she is probably running. She enjoys racing, traveling, or racing while traveling.

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/