Fetching data and creating a custom hook. ๐Ÿช

franklin030601

Franklin Martinez

Posted on June 18, 2022

Fetching data and creating a custom hook. ๐Ÿช

The purpose of this post is to teach a way how to make HTTP GET type requests by using React and a custom hook.

๐Ÿšจ Note: This post requires you to know the basics of React (basic hooks and fetch requests).

Any kind of feedback is welcome, thank you and I hope you enjoy the article.๐Ÿค—

ย 

Table of Contents.

๐Ÿ“Œ Technologies to use

๐Ÿ“Œ Creating the project

๐Ÿ“Œ First steps

๐Ÿ“Œ Making our first fetch

๐Ÿ“Œ Displaying API data on screen

๐Ÿ“Œ Creating a custom hook

๐Ÿ“Œ Improving the useFetch hook

๐Ÿ“Œ Adding more components and refactoring

๐Ÿ“ Header.tsx

๐Ÿ“ Loading.tsx

๐Ÿ“ ErrorMessage.tsx

๐Ÿ“ Card.tsx

๐Ÿ“ LayoutCards.tsx


๐Ÿšจ Technologies to use.

โ–ถ๏ธ React JS (version 18)

โ–ถ๏ธ Vite JS

โ–ถ๏ธ TypeScript

โ–ถ๏ธ Rick and Morty API

โ–ถ๏ธ vanilla CSS (Styles can be found in the repository at the end of this post)

ย 

ใ€ฝ๏ธ Creating the project.

npm init vite@latest
Enter fullscreen mode Exit fullscreen mode

In this case we will name it: fetching-data-custom-hook (optional).

We will select React and then TypeScript.

Then we execute the following command to navigate to the directory just created.

cd fetching-data-custom-hook
Enter fullscreen mode Exit fullscreen mode

Then we install the dependencies:

npm install
Enter fullscreen mode Exit fullscreen mode

Then we open the project in a code editor (in my case VS code)

code .
Enter fullscreen mode Exit fullscreen mode

ย 

ใ€ฝ๏ธ First steps.

Inside the src/App.tsx folder we delete all the contents of the file and place a functional component that displays a title and a subtitle.

const App = () => {
  return (
            <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
  )
}
export default App;

Enter fullscreen mode Exit fullscreen mode

Show title and sutitle

First of all, we will create a couple of interfaces that will help us to auto-complete the properties that come in the JSON response provided by the API.

  • The first interface Response contains the results property which is an array of Results.
  • The second interface Result, only contains 3 properties (although there are more, you can check the documentation of the API), select an ID, the name and the image of the character.
interface Response {
  results: Result[]
}

interface Result {
  id: number;
  name: string;
  image: string;
}

Enter fullscreen mode Exit fullscreen mode

ย 

ใ€ฝ๏ธ Making our first Fetch.

  1. First we add a state that is of type Result[] and the default value will be an empty array since we are not making the API call yet. This will serve us to store the API data and to be able to show them.
const App = () => {

  const [data, setData] = useState<Result[]>([]);

  return (
        <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
  )
}
export default App;

Enter fullscreen mode Exit fullscreen mode
  1. In order to perform a data fetch, we have to do it in a useEffect, since we need to execute the fetch when our component is rendered for the first time.

Since we need it to be executed only once, we place an empty array (i.e., without any dependencies).

const App = () => {
  const [data, setData] = useState<Result[]>([]);

    useEffect(()=> {

    },[]) // arreglo vaciรณ

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
    </div>
  )
}
export default App;

Enter fullscreen mode Exit fullscreen mode
  1. Inside the body of the useEffect function, the API call will be made, and as the useEffect does not allow us to use asynchronous code directly, we will make the call through promises in the meanwhile.
const [data, setData] = useState<Result[]>([]);

useEffect(()=> {
   fetch('<https://rickandmortyapi.com/api/character/?page=8>')
   .then( res => res.json())
   .then( (res: Response) => {})
   .catch(console.log)
},[])

Enter fullscreen mode Exit fullscreen mode
  1. Once the promises are solved, we will obtain the data corresponding to the API, which we will place it to the state by means of the setData function.

With this we could already display the data on the screen. ๐Ÿ˜Œ

๐Ÿšจ If something goes wrong with the API, the catch will catch the error and show it by console and the value of the state "data" remains as empty array (and at the end nothing will be shown but the title and subtitle of the app).

const [data, setData] = useState<Result[]>([]);

useEffect(()=> {
   fetch('<https://rickandmortyapi.com/api/character/?page=8>')
   .then( res => res.json())
   .then( (res: Response) =>  {
      setData(res.results);
   })
   .catch(console.log)
},[])

Enter fullscreen mode Exit fullscreen mode

ย 

ใ€ฝ๏ธ Displaying the API data on screen.

Before displaying the API data, we need to do an evaluation. ๐Ÿค”

๐Ÿ”ต Only if the length of the "data" status value is greater than 0, we display the API data on screen.

๐Ÿ”ต If the length of the "data" status value is less than or equal to 0, no data will be displayed on the screen, only the title and subtitle.

const App = () => {
    const [data, setData] = useState<Result[]>([]);

    useEffect(()=> {
       fetch('<https://rickandmortyapi.com/api/character/?page=8>')
       .then( res => res.json())
       .then( (res: Response) =>  {
          setData(res.results);
       })
       .catch(console.log)
    },[])

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
      {
        (data.length > 0) && <p>data</p>
      }
    </div>
  )
}
export default App;

Enter fullscreen mode Exit fullscreen mode

Now, once we have confirmed that we do have data in the "data" state value, we will proceed to display and shape the data.

Using the map function used in arrays. We will traverse the array of the value of the state "data" and we will return a new JSX component that in this case will only be an image and a text.

๐Ÿ”ด NOTE: the key property inside the div, is an identifier that React uses in the lists, to render the components in a more efficient way. It is important to set it.

const App = () => {
    const [data, setData] = useState<Result[]>([]);

    useEffect(()=> {
       fetch('<https://rickandmortyapi.com/api/character/?page=8>')
       .then( res => res.json())
       .then( (res: Response) =>  {
          setData(res.results);
       })
       .catch(console.log)
    },[])

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
      {
        (data.length > 0) && data.map( ({ id, image, name }) => (
          <div key={id}>
            <img src={image} alt={image} />
            <p>{name}</p>
          </div>
        ))
      }
    </div>
  )
}
export default App;

Enter fullscreen mode Exit fullscreen mode

In this way we have finished fetching data and displaying it correctly on the screen. But we can still improve it. ๐Ÿ˜Ž

Show cards
ย 

ใ€ฝ๏ธ Creating a custom hook.

Inside the folder src/hook we create a file named useFetch.

We create the function and cut the logic of the App.tsx component.

const App = () => {

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
      {
        (data.length > 0) && data.map( ({ id, image, name }) => (
          <div key={id}>
            <img src={image} alt={image} />
            <p>{name}</p>
          </div>
        ))
      }
    </div>
  )
}
export default App;

Enter fullscreen mode Exit fullscreen mode

We paste the logic inside this function, and at the end we return the value of the state "data."

export const useFetch = () => {
  const [data, setData] = useState<Result[]>([]);

  useEffect(()=> {
     fetch('<https://rickandmortyapi.com/api/character/?page=8>')
     .then( res => res.json())
     .then( (res: Response) =>  {
        setData(res.results);
     })
     .catch(console.log)
  },[]);

  return {
    data
  }
}

Enter fullscreen mode Exit fullscreen mode

Finally, we make the call to the useFetch hook extracting the data.

And ready, our component is even cleaner and easier to read. ๐Ÿค“

const App = () => {

  const { data } = useFetch();

  return (
    <div>
      <h1 className="title">Fetching data and create custom Hook</h1>
      <span className="subtitle">using Rick and Morty API</span>
      {
        (data.length > 0) && data.map( ({ id, image, name }) => (
          <div key={id}>
            <img src={image} alt={image} />
            <p>{name}</p>
          </div>
        ))
      }
    </div>
  )
}
export default App;

Enter fullscreen mode Exit fullscreen mode

But wait, we can still improve this hook. ๐Ÿคฏ
ย 

ใ€ฝ๏ธ Improving the useFetch hook.

Now what we will do is to improve the hook, adding more properties.

To the existing state we will add other properties and this new state will be of type DataState.

interface DataState {
    loading: boolean;
    data: Result[];
    error: string | null;
}

Enter fullscreen mode Exit fullscreen mode

loading, boolean value, will let us know when the API call is being made. By default the value will be set to true.

error, string or null value, it will show us an error message. By default the value will be null.

data, value of type Result[] , will show us the API data. By default the value will be an empty array.

๐Ÿ”ด NOTE: the properties of the estate have just been renamed.

๐Ÿ”ต data โžก๏ธ dataState

๐Ÿ”ต setData โžก๏ธ setDataState

export const useFetch = () => {
    const [dataState, setDataState] = useState<DataState>({
      data: [],
      loading: true,
      error: null
  });

  useEffect(()=> {
     fetch('<https://rickandmortyapi.com/api/character/?page=8>')
     .then( res => res.json())
     .then( (res: Response) =>  {
        setData(res.results);
     })
     .catch(console.log)
  },[]);

  return {
    data
  }
}

Enter fullscreen mode Exit fullscreen mode

Now we will take out the useEffect logic in a separate function. This function will have the name handleFetch.

We will use useCallback, to store this function and prevent it from being recreated when the state changes.

The useCallback also receives an array of dependencies, in this case we will leave it empty since we only want it to be generated once.

const handleFetch = useCallback(
    () => {},
    [],
)

Enter fullscreen mode Exit fullscreen mode

The function that receives in useCallback, can be asynchronous so we can use async/await..

  1. First we place a try/catch to handle errors.
  2. Then we create a constant with the URL value to make the API call.
  3. We make the API call using fetch and send the URL (the await will allow us to wait for a response either correct or wrong, in case of error, it would go directly to the catch function).
const handleFetch = useCallback(
      async () => {
          try {
                            const url = '<https://rickandmortyapi.com/api/character/?page=18>';
              const response = await fetch(url);
          } catch (error) {}
        },
        [],
    )

Enter fullscreen mode Exit fullscreen mode
  1. Then we evaluate the response, if there is an error then we activate the catch and send the error that the API gives us.
  2. In the catch, we are going to set the state. We call the setDataState, we pass a function to obtain the previous values (prev). We return the following.
    1. We spread the previous properties (...prev), which in this case will only be the value of the data property, which will end up being an empty array.
    2. loading, we set it to false.
    3. error, we caste the value of the parameter error that receives the catch to be able to obtain the message and to place it in this property.
const handleFetch = useCallback(
      async () => {
          try {
                            const url = '<https://rickandmortyapi.com/api/character/?page=18>';
              const response = await fetch(url);

              if(!response.ok) throw new Error(response.statusText);

          } catch (error) {

              setDataState( prev => ({
                  ...prev,
                  loading: false,
                  error: (error as Error).message
              }));
          }
        },
        [],
    )

Enter fullscreen mode Exit fullscreen mode
  1. If there is no error from the API, we get the information, and set the status in a similar way as we did in the catch.
  2. We call the setDataState, pass it a function to get the previous values (prev). We return the following.
    1. We spread the previous properties (...prev), which in this case will only be the value of the error property that will end up being a null.
    2. loading, we set it to false.
    3. data, will be the value of the dataApi counter by accessing its results property.
const handleFetch = useCallback(
      async () => {
          try {
                            const url = '<https://rickandmortyapi.com/api/character/?page=18>';
              const response = await fetch(url);

              if(!response.ok) throw new Error(response.statusText);

              const dataApi: Response = await response.json();

              setDataState( prev => ({
                  ...prev,
                  loading: false,
                  data: dataApi.results
              }));

          } catch (error) {

              setDataState( prev => ({
                  ...prev,
                  loading: false,
                  error: (error as Error).message
              }));
          }
        },
        [],
    )

Enter fullscreen mode Exit fullscreen mode

After creating the handleFetch function, we return to the useEffect to which we remove the logic and add the following.

We evaluate if the value of the state "dataState" by accessing the data property, contains a length equal to 0, then we want the function to be executed. This is to avoid calling the function more than once.

useEffect(() => {
    if (dataState.data.length === 0) handleFetch();
}, []);

Enter fullscreen mode Exit fullscreen mode

And the hook would look like this:

๐Ÿ”ด NOTE: at the end of the hook, we return, using the spread operator, the value of the "dataState" state.

๐Ÿ”ด NOTE: the interfaces were moved to their respective folder, inside src/interfaces.

import { useState, useEffect, useCallback } from 'react';
import { DataState, Response } from '../interface';

const url = '<https://rickandmortyapi.com/api/character/?page=18>';

export const useFetch = () => {

    const [dataState, setDataState] = useState<DataState>({
        data: [],
        loading: true,
        error: null
    });

    const handleFetch = useCallback(
        async () => {
            try {
                const response = await fetch(url);

                if(!response.ok) throw new Error(response.statusText);

                const dataApi: Response = await response.json();

                setDataState( prev => ({
                    ...prev,
                    loading: false,
                    data: dataApi.results
                }));

            } catch (error) {

                setDataState( prev => ({
                    ...prev,
                    loading: false,
                    error: (error as Error).message
                }));
            }
        },
        [],
    )

    useEffect(() => {
        if (dataState.data.length === 0) handleFetch();
    }, []);

    return {
        ...dataState
    }
}

Enter fullscreen mode Exit fullscreen mode

Before using the new properties of this hook, we will do a refactoring and create more components. ๐Ÿ˜ณ
ย 

ใ€ฝ๏ธ Adding more components and refactoring.

The first thing is to create a components folder inside src..

Inside the components folder we create the following files.
ย 

๐ŸŸก Header.tsx

Inside this component will be only the title and the subtitle previously created. ๐Ÿ˜‰

export const Header = () => {
    return (
        <>
            <h1 className="title">Fetching data and create custom Hook</h1>
            <span className="subtitle">using Rick and Morty API</span>
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

ย 

๐ŸŸก Loading.tsx

This component will only be displayed if the loading property of the hook is set to true. โณ

export const Loading = () => {
  return (
    <p className='loading'>Loading...</p>
  )
}

Enter fullscreen mode Exit fullscreen mode

show loading
ย 

๐ŸŸก ErrorMessage.tsx

This component will only be displayed if the error property of the hook contains a string value. ๐Ÿšจ

export const ErrorMessage = ({msg}:{msg:string}) => {
  return (
    <div className="error-msg">{msg.toUpperCase()}</div>
  )
}

Enter fullscreen mode Exit fullscreen mode

show error
ย 

๐ŸŸก Card.tsx

Displays the API data, that is, the image and its text. ๐Ÿ–ผ๏ธ

import { Result } from '../interface';

export const Card = ({ image, name }:Result) => {

    return (
        <div className='card'>
            <img src={image} alt={image} width={100} />
            <p>{name}</p>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

ย 

๐ŸŸก LayoutCards.tsx

This component serves as a container to do the traversal of the data property and display the letters with their information. ๐Ÿ”ณ

๐Ÿ”ด NOTE: we use memo, enclosing our component, in order to avoid re-renders, which probably won't be noticed in this application but it's just a tip. This memo function is only re-rendered if the "data " property changes its values.

import { memo } from "react"
import { Result } from "../interface"
import { Card } from "./"

interface Props { data: Result[] }

export const LayoutCards = memo(({data}:Props) => {
    return (
        <div className="container-cards">
            {
                (data.length > 0) && data.map( character => (
                    <Card {...character} key={character.id}/>
                ))
            }
        </div>
    )
})

Enter fullscreen mode Exit fullscreen mode

This is how our App.tsx component would look like.

We create a function showData, and we evaluate:

  • If the loading property is true, we return the <Loading/> component.
  • If the error property is true, we return the component <ErrorMessage/> , sending the error to the component.
  • If none of the conditions is true, it means that the API data is ready and we return the <LayoutCards/> component and send it the data to display.

Finally, below the component, we open parentheses and call the showData function.

import { ErrorMessage, Header, Loading, LayoutCards } from './components'
import { useFetch } from './hook';

const App = () => {

  const { data, loading, error } = useFetch();

  const showData =  () => {
    if (loading) return <Loading/>
    if (error) return <ErrorMessage msg={error}/>
    return <LayoutCards data={data} />
  }

  return (
    <>
      <Header/>
      { showData() }
    </>
  )
}
export default App;

Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ด NOTE: You can also move the showData function to the hook, and change the hook file extension to .tsx, this is because JSX is being used when returning various components.


Thanks for getting this far. ๐Ÿ™Œ

I leave the repository for you to take a look if you want. โฌ‡๏ธ

GitHub logo Franklin361 / fetching-data-custom-hook

Tutorial on how to fetching data and creating a custom hook

Fetching data and create custom Hook

Tutorial on how to fetching data and creating a custom hook

demo

Link to tutorial post โžก๏ธ






๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
franklin030601
Franklin Martinez

Posted on June 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About