DEV Community

loading...
Cover image for Building custom hooks in React to fetch Data

Building custom hooks in React to fetch Data

Shahid Rizwan
・4 min read

Fetching data from the backend is one of the crucial parts of the web application. For every application to work dynamically, it fetches the data from the server and then displays it in the user interface.

We retrieve the data using the API call and using built-in React hooks like useState, useEffect, and useReducer, the retrieved data is set to the state variable. It is then used in components for showing it in the view.

Before jumping into the code of how's it's done, let's first look at what React hooks are and why it is used.

What is React Hooks

React hooks were first introduced in React 16.8. They are functions that let you hook into React state.

Some of the built-in hooks provided by React are useState, useEffect, useContext, useReducer, useRef, useCallback, and useMemo.

Why React Hooks are used

One of the main advantages of using React hooks is the re-usability of logic. The hooks can be used in multiple components where we have to use a specific function.

It also makes the code more readable, efficient, and easy to maintain.

The normal code for fetching the data from the server and updating in the component is shown below

export function Home(){

    const [data,setData] = useState(null)
    const [loading,setLoading] = useState(false)
    const [error,setError] = useState(null)

    useEffect(()=>{
        (
        async function(){
            try{
                setLoading(true)
                const response = await axios.get('http:localhost:4000')
                setData(response.data)
            }
            catch(err){
                setError(err)
            }finally{
                setLoading(false)
            }
        }
        )()
    },[])

    return(
        {loading && <div>Loading...</div>}
        {data && <div>{data}</div>
    )
}
Enter fullscreen mode Exit fullscreen mode

We write the logic inside the useEffect hook to update the state properties like data, loading, and error.

While it's perfectly fine to write like this, what if we want to do the same kind of thing in multiple components where we have to fetch another data.

We have to rewrite all of these codes multiple times in all of those components which is not very efficient and hard to manage.

In big codebases, it is better to follow the Don't Repeat Yourself (DRY) principles, that is, it's better to write code once and make it reusable instead of writing it again and again in multiple components.

That's where the real magic of Custom Hook is. We can write the code in a separate js file and call it with URL from all the components that might need to fetch the data from the server.

This makes the code efficient and easily maintainable.

Like useState and useEffect have their function, we create custom hooks by combining them for a specific ability.

Creating custom useFetch hook

We first create a new javascript file with the name useFetch.js.
The name of the hooks starts with use as a part of react hooks convention.

useFetch javascript file

Inside the file, create a new function with the name of the hook. The difference between React hook and a React component is that hook doesn't return JSX. It only returns the state variable or function that you want to use in a component.

export function useFetch(){

}
Enter fullscreen mode Exit fullscreen mode

To make an API call, use a useEffect hook because it will trigger the API call function inside it when rendered. Here, the API call is made using Axios.

The API Url that needs to be called is passed to the hook as an argument from the component.

import { useEffect } from "react"
import axios from axios

export function useFetch(url){
   useEffect(()=>{
      (
         async function(){
            const response = await axios.get(url)
         }
      )()
   },[url])

}
Enter fullscreen mode Exit fullscreen mode

Usually, we have 3 state variables that are data, error, and loading created using useState to store the response data, error and loading respectively,

If the data is received, we set it to the data variable. If not, the error message will be set to the error variable.

Loader is initialized as false. When the API is called, it is set to true so that a loader component can be loaded in the view.

At the end of the API call, this loader is set back to false by using the finally block.

import { useEffect, useState } from "react"
import axios from "axios"


export default function useFetch(url){

    const [data,setData] = useState(null)
    const [error,setError] = useState(null)
    const [loading,setLoading] = useState(false)

    useEffect(() => {
        (
            async function(){
                try{
                    setLoading(true)
                    const response = await axios.get(url)
                    setData(response.data)
                }catch(err){
                    setError(err)
                }finally{
                    setLoading(false)
                }
            }
        )()
    }, [url])

    return { data, error, loading }

}
Enter fullscreen mode Exit fullscreen mode

The only dependency we're going to put in the useEffect dependency array is Url because if the Url changes, we have to request new data.

That's basically for useEffect. Now we return the states that are created inside the hook as an object.

Using Custom Hook in the Component

Inside the component, import the useFetch hook from its javascript file. After importing, call the hook with the API Url as an argument.

Fetching data using custom Hook

export function Home(){
    const {data,loading,error} = useFetch('https://localhost:4000')

        if(error){
           console.log(error)
        }

    return(
        {loading && <div>Loading...</div>}
        {data && <div>{data.map(item => <div>{item}</div>)}</div>}
    )
}
Enter fullscreen mode Exit fullscreen mode

In addition to this, we can also customize the hook by making it return any function that can be called from the component.

For example, we can create a refetch() function inside the hooks that re-fetches the API when called.

This function can be returned from the hook and can be called from the component.

Discussion (25)

Collapse
lukeshiru profile image
LUKESHIRU

If the endpoint just returns a JSON, you can simplify it quite a bit by just using fetch and not using async/await:

import { useEffect, useState } from "react";

export const useFetch = url => {
    const [data, setData] = useState();
    const [error, setError] = useState();
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);
        fetch(url)
            .then(response => response.json())
            .then(setData)
            .catch(setError)
            .finally(() => setLoading(false));
    }, [url]);

    return { data, error, loading };
};
Enter fullscreen mode Exit fullscreen mode

Cheers!

Collapse
ehaynes99 profile image
Eric Haynes • Edited

That's not any simpler... It's just using the old Promise api...

Collapse
lukeshiru profile image
LUKESHIRU • Edited

Do you objectively believe that? I mean, IDK about you, but this:

const fetchAPI = async () => {
    try {
        const response = await fetch("/api");
        const data = await response.json();
        setData(data);
    } catch (error) {
        setError(error);
    } finally {
        setLoading(false);
    }
};
Enter fullscreen mode Exit fullscreen mode

Is unnecessarily more complex than this:

const fetchAPI = () =>
    fetch("/api")
        .then(response => response.json())
        .then(setData)
        .catch(setError)
        .finally(() => setLoading(false));
Enter fullscreen mode Exit fullscreen mode

async/await + try/catch makes sense sometimes in really complex promise chains that depend on each other, but having something as simple as fetch, they don't make much sense :/

Thread Thread
hernanfarruggia profile image
Hernán Farruggia

I don't see any complexity in any of those... 🤷🏻‍♂️ They just have different sintax, and usually which one to use, will depend on the conventions of your project... Some companies decide to go with async to use latest festures, others prefer to stay with plain old promises... These sre just different points of view...

Thread Thread
lukeshiru profile image
LUKESHIRU

I love how you keep using "old" as an argument. Using async/await instead of Promises is not the same as using class instead of prototype. When writing a "class", is always simpler to just use class instead of prototype, so in that case "old is worst", yes.

But in the case of async/await, in some scenarios the new approach is way more complex. You shouldn't use "one or the other", you should just use the one that makes the code easier to read. For simple stuff like fetch, the "new" approach makes it harder to read:

fetch("/api")
  .then((response) => response.json())
  .then(setData)
  .catch(setError);

// vs

try {
  const response = await fetch("/api")
  const data = await response.json();
  setData(data)
} catch (error) {
  setError(error);
}
Enter fullscreen mode Exit fullscreen mode

We should try to avoid using stuff just because is "new" and being like this...
Gif from "The Simpsons" where Smithers says: But she's got a new hat!

And instead use the new features where they are useful. For async/await that's when the promise chain becomes complex (promises that depend on other promises, async iterables and so on).

Thread Thread
ehaynes99 profile image
Eric Haynes • Edited

Note that the former reply was someone else, so no one "kept using 'old'". It is, in fact, older, but we can refer to it as "the .then syntax".

To each their own, but the allure of async/await is that the code reads exactly the same as it would if it were synchronous. There is a valid argument for consistency. E.g. it's pretty much universally accepted that we don't intermittently use callbacks for async, even if it would save a line or 2.

It's not "way more complex", either. It's slightly more verbose, and only in a rare set of cases. The .then syntax is only cleaner where you can constrain the action to lambda expressions and have at least 4 of them. As soon as you need a block for any of them, IMO it's rather messy. What is simply another line with async/await becomes a new function, another level of nesting, and a new scope. It takes a hit for readability when shorter than 4 as well, since formatters like prettier will inline:

myApi.getOrder(123).then(setOrder).catch(setError);
Enter fullscreen mode Exit fullscreen mode

So yes, in the cases where you have the catch in the same place as the call, can chain 4 or more operations that are all one liners, and never need to refer to something in a prior step of the chain, it's slightly cleaner, but it's rare enough that I wouldn't fault anyone for just forgetting about the syntax altogether.

const fetch1 = (orderNum) => {
  fetch(`/api/orders/${orderNum}`)
    .then((response) => response.json())
    .then(({ products }) => {
      setProducts(products);
      setError(null);
    })
    .catch((error) => {
      logger.error(error);
      setError(error);
    })
    .finally(() => setLoading(false));
};

// vs
const fetch2 = async (orderNum) => {
  try {
    const response = await fetch(`/api/orders/${orderNum}`);
    const { products } = await response.json();

    setProducts(products);
    setError(null);
  } catch (error) {
    logger.error(error);
    setError(error);
  }
  setLoading(false);
};
Enter fullscreen mode Exit fullscreen mode
Collapse
juancor181192 profile image
Juan Carlos Otero Roca

I use axios

Collapse
lukeshiru profile image
LUKESHIRU • Edited

Try redaxios instead. Pretty much the same API, but closer to fetch and maintained by the same folks behind preact.

Collapse
markyangit profile image
Gabriel Markyan

A good thing to note about this is that using custom hooks and hooks inside a useEffect causes an infinite loop.

Collapse
lukeshiru profile image
LUKESHIRU

Not quite. If he has only url as a dependency of that effect, the effect will only run if that value changes.

Collapse
markyangit profile image
Gabriel Markyan

Yes, of course. But some linters require all the functions, custom hooks and external data at the dependency array. I'm saying because I've been through this.

Thread Thread
lukeshiru profile image
LUKESHIRU

Yup, you can configure ESLint with eslint-plugin-react-hooks to do just that, but it throws a warning mainly because you might want to just depend on some of the things inside the effect, not all of them. Still, the exhaustive-deps rule is smart enough to know that it doesn't have to re-run with state setters from useState.

Thread Thread
marceliwac profile image
Marceli-Wac • Edited

There is also the useCallback hook which can be used to create a memoized function that, when passed to useEffect dependency array, doesn't triger re-renders.

Thread Thread
lukeshiru profile image
LUKESHIRU

Yup, but ideally that should be in the consumer side of the hook, not in its internals.

Thread Thread
marceliwac profile image
Marceli-Wac

I'm not sure I understand what you mean, can you elaborate?

Thread Thread
lukeshiru profile image
LUKESHIRU

Ideally, you shouldn’t memoize a callback set by the consumer of your hook. If you memoize it on your side (inside your hook) with useCallback, then if the consumer of your hook changes the callback, your hook will still be using the old one. Ideally useCallback should be used by the consumer itself, so:

// Avoid this:

const useExample = callback => {
  const memoizedCallback = useCallback(callback, []);
  // use memoziedCallback
}

const ConsumerComponent = ({ aFunction, ...props }) => {
  useExample(aFunction); // if aFunction changes, useExample will not react to that
}

// And instead just do this:

const useExample = callback => {
  // use callback directly
}

const ConsumerComponent = ({ aFunction, ...props }) => {
  // the consumer memoizes if they want
  const memoizedFunction = useMemo(aFunction, []);
  // Your hook will always work as expected
  useExample(memoizedFunction);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
lowlifearcade profile image
Sonny Brown • Edited

Good stuff. I like to use a custom hook I made called useStateAndLocalStorage. I love the self documenting nature of custom hooks.

Collapse
morganjay profile image
James Morgan

Please I'd like to see how that works. 🤔

Maybe I'll like it too

Collapse
imrishabh18 profile image
Rishabh Gupta

Check out my blog on this James.

dev.to/imrishabh18/simplest-custom...

Thread Thread
morganjay profile image
James Morgan

Thanks!

Collapse
slimani_21 profile image
Slimani Mohammed

Thanks for the article. I think it would be better to set the error to null before fetching so that one error in fetching won't stay for all future fetch calls.

Collapse
mnlfischer profile image
Manuel

You should also check:
kent.dev/blog/stop-using-isloading...

Collapse
shaedrizwan profile image
Shahid Rizwan Author

Sure Manuel. Thank you for the article.

Collapse
rahulxyz profile image
Rahul Gupta

How would you handle different http methods? Here you have 1 hook using only get().

Collapse
shaedrizwan profile image
Shahid Rizwan Author

This hook is specifically for fetching data (GET). You can customize it by passing the HTTP method and body as parameter to the hook and use it inside.