DEV Community

Cover image for The only custom hook you will ever need to fetch data in your React projects
VelizarMihaylov
VelizarMihaylov

Posted on • Originally published at functionalui.com

The only custom hook you will ever need to fetch data in your React projects

OK, I know what you think, this guy went a bit too far with the title of this article, but hey you did click on it. Seriously though, I do believe that the pattern I am about to show you is handy and will probably cover most of the cases in which you have to grab some data from external sources in your React applications.

Important Note

In this article, I am going to use React Hooks API, which was introduced officially in react 16.0.8. If you are running an earlier version of React, you won't be able to follow through. Technically you might be able to achieve similar results using higher-order class components, but it looks like hooks will be the future of React. On top of that, you should be keeping your dependencies up to date anyway. Don't forget to eat your greens too.

If you haven't used hooks before I strongly recommend checking out the official Hooks documentation on React website.

The Problem

If you have done any work on the client-side in the past few years, you've probably ended up having to fetch some data from a backend service or a third-party API. That is a very common use case in React applications. There are many ways to do that. You can use a library like axios or implement the standard fetch API, which is now supported in all major browsers. The usual place where that call was in made in React was inside the ComponentDidMount method and after React 16.8.0 inside the Effect Hook. In this article, we are going to try to generalise that process of getting external data.

The Set-Up

In this example, I am going to be using the standard Create React App setup. All you need to do is make sure you have create react app installed

npm install -g create react app

Or if you prefer yarn

yarn add -g create react app

And then generate a new react app

npx create-react-app fetch-hook

Like it or not it looks like Typescript is here to stay. A lot of people enjoy using this typed version of JavaScript so I decided to use the typescript template of Create React App for this article. If you don't use typescript in your projects feel free to remove the types. To install Create React App with Typescript just do:

npx create-react-app fetch-hook --typescript

I will also be using the NASA API. It's a free publicly available API this way you will be able to follow the steps in the tutorial. We are going to use only one of the endpoints that they provide "Picture of the day". Since this will change every single day, you will probably see a different picture than the one shown in this article, but the shape of the data should be the same.

It’s also nice to have PostMan installed on your machine so you can test the endpoint and know upfront in what form you will receive the data.

Getting the basics out of the way

OK first things first, we will need to create a simple component that will get our picture of the day from NASA and will display it in our app.

Before we go any further let see what information we want to display. I will assume you already have Postman installed but if you haven't go to their website and follow the installation instructions. It's very straightforward, and there is a version for any major OS. Once you have Postman up and running and add the NASA API URL in the search box. The URL you should use is:

https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY

We are using the DEMO_KEY api key which will do just fine for this example but if you would like to use the NASA API in production you should go to https://api.nasa.gov/ and sign up fo a developer key. Make sure you have the type of the request set to GET and hit Send. You should see something like this:

The response from our GET request

Great, so the API send us back a title, description and a URL for the image, as well as some additional metadata.

Let's create a simple, functional component that will present the data we are getting back. I like to keep things organised so we will create a components folder inside src and then add a new file PictureOfTheDay.js and in that file we will do:

import React from 'react'

const PictureOfTheDay: React.FC = (): React.ReactElement => (
  <>
    <img
      src="https://apod.nasa.gov/apod/image/1911/LighthouseMilkyWay_Salazar_960.jpg"
      style={{ width: '100%', maxWidth: 600 }}
      alt="Milky Way over Uruguayan Lighthouse"
    />
    <span>Date: 2019-11-19</span>
    <h1>Milky Way over Uruguayan Lighthouse</h1>
    <p>
      Can a lighthouse illuminate a galaxy? No, but in the featured image, gaps in light emanating from the Jose Ignacio Lighthouse in Uruguay appear to match up nicely, although only momentarily and coincidently, with dark dust lanes of our Milky Way Galaxy. The bright dot on the right is the planet Jupiter. The central band of the Milky Way Galaxy is actually the central spiral disk seen from within the disk. The Milky Way band is not easily visible through city lights but can be quite spectacular to see in dark skies. The featured picture is actually the addition of ten consecutive images taken by the same camera from the same location. The
      images were well planned to exclude direct light from the famous
      lighthouse.
    </p>
  </>
)

export default PictureOfTheDay

And this is how it looks in the browser.

NASA picture of the day

As you can see we just copy the data from Postman, of course, that's not ideal. What we really want is to fetch the data dynamically, so every time it changes, we can show the most up to date data to our users.

Let's deal with that by adding some hooks. There are three stages we want to handle in our component: loading, showing the data, and we also want to handle any errors if the fetch fails for whatever reason.

Fetch States

If you have already looked into hooks, you might already be familiar with the useState and useEffect hooks, but here we will use their less famous cousin the useReducer hook.

For anyone that has used Redux in the past, useReducer should sound familiar. But before we get to the hook, we need to create a reducer. Let's do that:

// First we define the Types for our Reducer
/**
 * @type {Payload}
 * Typing the data
 * we are expecting to
 * receive from the end point
 */

type Payload = {
  copyright: string
  date: string
  title: string
  explanation: string
  url: string
}

/**
 * @type {Action}
 * This type will represent the
 * action our reducer takes.
 * It has two params the action
 * `type` and the payload we expect
 *  to receive from the endpoint.
 * We are using a discriminated union type
 * for the action to make sure we are
 * covering all possible action types
 * our reducer will accept
 */

type Action =
  | { type: 'FETCH_INIT' }
  | {
      type: 'FETCH_SUCCESS'
      payload: Payload
    }
  | {
      type: 'FETCH_FAILURE'
    }

/**
 *
 * @type {State}
 * This type is the initial state
 * our reducer expects. It holds all
 * the possible states our app can be
 * in durning the fetch.
 */

type State = {
  loading: boolean
  data: null | Action['payload']
  error: boolean
}

/**
 * @function dataFetchReducer
 * Our fetch reducer
 */

const dataFetchReducer = (state: State, action: Action): State => {
  /**
   * The reducer will handle the three cases
   * based on the action type it receives
   */
  switch (action.type) {
    // The initial loading state
    case 'FETCH_INIT':
      return {
        ...state,
        loading: true
      }
    // We successfully received the data
    case 'FETCH_SUCCESS':
      return {
        ...state,
        loading: false,
        error: false,
        data: action.payload
      }
    // The fetch failed
    case 'FETCH_FAILURE':
      return {
        ...state,
        loading: false,
        error: true
      }
    /**
     * If we don't receive an expected action
     * we assume it's a developer mistake
     * and we will throw an error asking them
     * to fix that
     */
    default:
      throw new Error(
        `Unknown action type of '${action.type}' received for dataFetchReducer.
          Please make sure you are passing one of the following actions:
          * FETCH_INIT
          * FETCH_SUCCESS
          * FETCH_FAILURE
          :`
      )
  }
}

Now that we have our reducer we can pass it to the useReducer hook:

 /**
   * Here we are making use
   * of the useReducer hook
   * The hook accepts our reducer
   * we defined earlier and the
   * initial state of our app
   */
  const initialState = {
    loading: false,
    data: null,
    error: false
  }
  const [state, dispatch] = useReducer(dataFetchReducer, initialState)

OK so far so good but now what? This is where our old friend the useEffect hook comes in to play.

    /**
   * Since fetching data is a side effect
   * for our React app it is a good idea
   * to keep it in the useEffect hook
   */
  useEffect(() => {
    // Start fetching and fire up the loading state
    dispatch({ type: 'FETCH_INIT' })
    fetch(url)
      .then(response => response.json())
      .then(data => {
        // We got the data so lets add it to the state
        dispatch({ type: 'FETCH_SUCCESS', payload: data })
      })
      .catch(error => {
        // Something went wrong trigger the error state
        console.log(error)
        dispatch({ type: 'FETCH_FAILURE' })
      })
      /**
      * useEffect accepts a second argument an array
      * if the values of the array did not change the
      * effect will not re run. In our case we want to
      * re run the effect only if the fetch url changes
      */
  }, [url])

This is how the final version of the code will look like:

import React, { useReducer, useEffect } from 'react'
import PropTypes from 'prop-types'

/**
 * @type {Payload}
 * Typing the data
 * we are expecting to
 * receive from the end point
 */

type Payload = {
  copyright: string
  date: string
  title: string
  explanation: string
  url: string
}

/**
 * @type {Action}
 * This type will represent the
 * action our reducer takes.
 * It has two params the action
 * `type` and the payload we expect
 *  to receive from the endpoint.
 * We are using a discriminated union type
 * for the action to make sure we are
 * covering all possible action types
 * our reducer will accept
 */

type Action =
  | { type: 'FETCH_INIT' }
  | {
      type: 'FETCH_SUCCESS'
      payload: Payload
    }
  | {
      type: 'FETCH_FAILURE'
    }

/**
 *
 * @type {State}
 * This type is the initial
 * state our reducer expects.
 * It hold all the possible
 * states our app can be in
 * durning the fetch.
 */

type State = {
  loading: boolean
  data: null | Payload
  error: boolean
}

/**
 * @function dataFetchReducer
 * Our fetch reducer
 */

const dataFetchReducer = (state: State, action: Action): State => {
  /**
   * The reducer will handle
   * the three cases based on
   * the action type it receives
   */
  switch (action.type) {
    // The initial loading state
    case 'FETCH_INIT':
      return {
        ...state,
        loading: true
      }
    // We successfully received the data
    case 'FETCH_SUCCESS':
      return {
        ...state,
        loading: false,
        error: false,
        data: action.payload
      }
    // The fetch failed
    case 'FETCH_FAILURE':
      return {
        ...state,
        loading: false,
        error: true
      }
    /**
     * If we don't receive an expected action
     * we assume it's a developer mistake
     * and we will throw an error asking them
     * to fix that
     */
    default:
      throw new Error(
        `Unknown action type received 
        for dataFetchReducer.
        Please make sure you are passing
        one of the following actions:
          * FETCH_INIT
          * FETCH_SUCCESS
          * FETCH_FAILURE
          :`
      )
  }
}

// Adding a type for PictureOfTheDay props
type PictureOfTheDayProps = {
  url: string
}

const PictureOfTheDay: React.FC<PictureOfTheDayProps> = ({
  url
}): React.ReactElement => {
  /**
   * Here we are making use
   * of the useReducer hook
   * The hook accepts our reducer
   * we defined earlier and the
   * initial state of our app
   */
  const initialState = {
    loading: false,
    data: null,
    error: false
  }
  const [{ loading, data }, dispatch] = useReducer(
    dataFetchReducer,
    initialState
  )

  /**
   * Since fetching data is a side effect
   * for our React app it is a good idea
   * to keep it in the useEffect hook
   */
  useEffect(() => {
    // Start fetching and fire up the loading state
    dispatch({ type: 'FETCH_INIT' })
    fetch(url)
      .then(response => response.json())
      .then(data => {
        // We got the data so lets add it to the state
        dispatch({ type: 'FETCH_SUCCESS', payload: data })
      })
      .catch(error => {
        // Something went wrong trigger the error state
        console.log(error)
        dispatch({ type: 'FETCH_FAILURE' })
      })
      /**
      * useEffect accepts a second argument an array
      * if the values of the array did not change the
      * effect will not re run. In our case we want to
      * re run the effect only if the fetch url changes
      */
  }, [url])

  if (loading) {
    return <h1>...Loading</h1>
  }

  if (data) {
    const { title, date, explanation, url } = data
    return (
      <>
        <img src={url} style={{ width: '100%', maxWidth: 600 }} alt={title} />
        <p>{title}</p>
        <p>{date}</p>
        <p>{explanation}</p>
      </>
    )
  }

  // If not loading or received data show error message
  return <h1>Oops something went wrong!</h1>
}

//Making sure that the url prop will be set
PictureOfTheDay.propTypes = {
  url: PropTypes.string.isRequired
}

export default PictureOfTheDay

This is quite a lot of code to process so take your time to read trough it and understand how everything works.

Great, you might be thinking that we are done here, and as far as our Nasa Image of the day component is concerned, we are. But there is a chance we might need to fetch some other data somewhere else in our App. We might copy-paste the code from this component, but hey that's not what good developers do. What we can do instead is abstracting the fetch logic in our custom hook.

Let's create a new folder in the src folder and call it effects. Inside this folder we are gonna create a file named useFetch and in that file we are gonna copy all the fetch related code from our Component and will tweak it a little bit so we can then reuse it:

import { useReducer, useEffect, Reducer } from 'react'

/**
 * @type {Action}
 * This type will represent the
 * action our reducer takes.
 * It has two params the action
 * `type` and the payload we expect
 *  to receive from the endpoint.
 * We are using a discriminated union type
 * for the action to make sure we are
 * covering all possible action types
 * our reducer will accept
 */

type Action<P> =
  | { type: 'FETCH_INIT' }
  | {
      type: 'FETCH_SUCCESS'
      payload: P
    }
  | {
      type: 'FETCH_FAILURE'
    }

/**
 *
 * @type {State}
 * This type is the initial
 * state our reducer expects.
 * It hold all the possible
 * states our app can be in
 * durning the fetch.
 */

type State<P> = {
  loading: boolean
  data: null | P
  error: boolean
}

/**
 * @function dataFetchReducer
 * Our fetch reducer
 */
const dataFetchReducer = <P>(state: State<P>, action: Action<P>): State<P> => {
  /**
   * The reducer will handle
   * the three cases based on
   * the action type it receives
   */
  switch (action.type) {
    // The initial loading state
    case 'FETCH_INIT':
      return {
        ...state,
        loading: true
      }
    // We successfully received the data
    case 'FETCH_SUCCESS':
      return {
        ...state,
        loading: false,
        error: false,
        data: action.payload
      }
    // The fetch failed
    case 'FETCH_FAILURE':
      return {
        ...state,
        loading: false,
        error: true
      }
    /**
     * If we don't receive an expected action
     * we assume it's a developer mistake
     * and we will throw an error asking them
     * to fix that
     */
    default:
      throw new Error(
        `Unknown action type received 
        for dataFetchReducer.
        Please make sure you are passing
        one of the following actions:
          * FETCH_INIT
          * FETCH_SUCCESS
          * FETCH_FAILURE
          :`
      )
  }
}

// The useFetch hook that we will reuse across our App
const useFetch = <P>(url: string): State<P> => {
  /**
   * Here we are making use
   * of the useReducer hook
   * The hook accepts our reducer
   * we defined earlier and the
   * initial state of our app
   */
  const initialState = {
    loading: false,
    data: null,
    error: false
  }
  const [state, dispatch] = useReducer<Reducer<State<P>, Action<P>>>(
    dataFetchReducer,
    initialState
  )
  /**
   * Since fetching data is a side effect
   * for our React app it is a good idea
   * to keep it in the useEffect hook
   */
  useEffect(() => {
    // Start fetching and fire up the loading state
    dispatch({ type: 'FETCH_INIT' })
    fetch(url)
      .then(response => response.json())
      .then((data: P) => {
        // We got the data so lets add it to the state
        dispatch({ type: 'FETCH_SUCCESS', payload: data })
      })
      .catch(error => {
        // Something went wrong trigger the error state
        console.log(error)
        dispatch({ type: 'FETCH_FAILURE' })
      })
  }, [url])
  return state
}

export default useFetch

Everything is more or less still the same but now we are abstracting all of the fetch logic in the useFetch hook. We are also making our Payload a generic type so we can pass different values depending on what we get from the endpoint we are calling. Now inside our component, we can use our shiny new custom hook:

import React from 'react'
import PropTypes from 'prop-types'

import useFetch from '../effects/useFetch'
/**
 * @type {Payload}
 * Typing the data
 * we are expecting to
 * receive from the end point
 */
type Payload = {
  date: string
  title: string
  explanation: string
  url: string
}

// Adding a type for PictureOfTheDay props
type PictureOfTheDayProps = {
  url: string
}

const PictureOfTheDay: React.FC<PictureOfTheDayProps> = ({
  url
}): React.ReactElement => {
  // All we need to do is call useFetch with the url to get the data 
  const { loading, data } = useFetch<Payload>(url)
  if (loading) {
    return <h1>...Loading</h1>
  }

  if (data) {
    const { title, date, explanation, url } = data
    return (
      <>
        <img src={url} style={{ width: '100%', maxWidth: 600 }} alt={title} />
        <p>{title}</p>
        <p>{date}</p>
        <p>{explanation}</p>
      </>
    )
  }

  // If not loading or received data show error message
  return <h1>Oops something went wrong!</h1>
}

//Making sure that the url prop will be set
PictureOfTheDay.propTypes = {
  url: PropTypes.string.isRequired
}

export default PictureOfTheDay

That's it, we now have a custom made fetchHook that we can use to fetch data from pretty much any URL.

Extend the fetch hook

Check out the full article on my personal blog to learn how you can extend the fetch hook to be able to:

  • reload the content
  • lazy fetch after the user performs an action
  • cancel the fetch

Top comments (0)