DEV Community

Cover image for useAxios : A simple custom hook for calling APIs using axios
Yogini Bende
Yogini Bende

Posted on • Edited on

useAxios : A simple custom hook for calling APIs using axios

Hello folks,

Frontend apps are not complete if there are no api calls involved and calling an api becomes a little repetitive thing to do. By creating a custom hook for this, we can save this repetition. To make an api call from frontend, popular methods are fetch and axios. Because of the feature like interceptors which axios support, we will be using them in this hook.

We will create useAxios hook in the following steps -
1- Do the api call from a component using axios.
2. Add states for the API response, loading and error.
3. Create a hook for calling an API using all above.
4. Make the hook dynamic, to call all types of API methods.

If you don’t want to go through these steps and directly jump to the final code, check here.

Now, let's dive in and create our custom hook, step by step!

1. Simple API call from the component

To create this example, we will be using jsonplaceholder’s posts api. They have many more such APIs created for the practice purpose.

Generally, all the apis of an app have the same base URL. We will first set up our base URL for axios, so will not need to pass it again and again. In case you are using more than one base URLs, axios supports it via creating instances. You can check that in their documentation.

In our App component, we will just call a get api to get the list of posts. For this, we are using useEffect hook. The basic api call from an App component will look something like this -

//App Component

import { useEffect } from 'react';
import axios from 'axios';

axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';

const App = () => {
    const fetchData = () => {
        axios
            .get('/posts')
            .then((res) => {
                console.log(res);
            })
            .catch((err) => {
                console.log(err);
            });
    };

    useEffect(() => {
        fetchData();
    }, []);

    return (
          <div className='app'>
              //do something
          </div>
       );
};

export default App;

Enter fullscreen mode Exit fullscreen mode

The api call made above is simple. We used axios.get to call an api and using promises we will get the result or the error. Because we already had set up a baseURL, we just passed the specific path to the axios method.

2. Adding different states to the API call

But till now, we are just logging the response coming from api. Let’s use react’s states to save our response and error, if occurred. Also, we will be adding a loading state to conditionally show loaders on the page.

// App Component

import { useState, useEffect } from 'react';
import axios from 'axios';

axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';

const App = () => {
    const [response, setResponse] = useState(null);
    const [error, setError] = useState('');
    const [loading, setloading] = useState(true);

    const fetchData = () => {
        axios
            .get('/posts')
            .then((res) => {
                setResponse(res.data);
            })
            .catch((err) => {
                setError(err);
            })
            .finally(() => {
                setloading(false);
            });
    };

    useEffect(() => {
        fetchData();
    }, []);

    return (
        <div className='app'>
            //do something
        </div>
    );
};

export default App;

Enter fullscreen mode Exit fullscreen mode

3. Creating a custom hook

Custom hooks might be overwhelming in the beginning. But, if you view them just like other components, they will make more sense. One thing to keep in mind, custom hooks are just another component, which returns values instead of JSX. This is my definition for custom hooks and somehow it made the concept more clear to me. You can read more about custom hooks here.

So now, let’s copy the logic of calling an api from our app component to our custom hook. So, the first draft of our useAxios will look something like this -

// useAxios hook (first draft)

import { useState, useEffect } from 'react';
import axios from 'axios';

axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';

const useAxios = () => {
    const [response, setResponse] = useState(null);
    const [error, setError] = useState('');
    const [loading, setloading] = useState(true);

    const fetchData = () => {
        axios
            .get('/posts')
            .then((res) => {
                setResponse(res.data);
            })
            .catch((err) => {
                setError(err);
            })
            .finally(() => {
                setloading(false);
            });
    };

    useEffect(() => {
        fetchData();
    }, []);

    // custom hook returns value
    return { response, error, loading };
};

export default useAxios;

Enter fullscreen mode Exit fullscreen mode

If you notice carefully, we have literally copy pasted the code and created a custom hook. The only difference is this hook is returning us 3 values, loading, response and error.

Till now, everything looks fine but the hook we created is not at all dynamic. If we need to change the API path or if we want to make a post call instead of get, then we are right now not capable of doing so.

Hence, here comes the last step of making our hook more flexible. -

4. Making our hook more dynamic

To make our hook dynamic, we can create a variable for the url path and pass it as a prop to our hook. Also, axios can have any method from get, put, post and delete. Hence, we will need a variable for method name too. With path and methods, we will be adding two variables which can be used to pass body and headers to the request. After adding all these, our hook will look something like this -

Final code


// useAxios hook

import { useState, useEffect } from 'react';
import axios from 'axios';

axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';

const useAxios = ({ url, method, body = null, headers = null }) => {
    const [response, setResponse] = useState(null);
    const [error, setError] = useState('');
    const [loading, setloading] = useState(true);

    const fetchData = () => {
        axios[method](url, JSON.parse(headers), JSON.parse(body))
            .then((res) => {
                setResponse(res.data);
            })
            .catch((err) => {
                setError(err);
            })
            .finally(() => {
                setloading(false);
            });
    };

    useEffect(() => {
        fetchData();
    }, [method, url, body, headers]);

    return { response, error, loading };
};

export default useAxios;

Enter fullscreen mode Exit fullscreen mode

As our useAxios hook is ready, let’s now use it into our app component and try to create a new post using that. Hence, the App component will be -


// App Component

const App = () => {
    const { response, loading, error } = useAxios({
        method: 'post',
        url: '/posts',
        headers: JSON.stringify({ accept: '*/*' }),
        body: JSON.stringify({
            userId: 1,
            id: 19392,
            title: 'title',
            body: 'Sample text',
        }),
    });
    const [data, setData] = useState([]);

    useEffect(() => {
        if (response !== null) {
            setData(response);
        }
    }, [response]);

    return (
        <div className='App'>
            <h1>Posts</h1>

            {loading ? (
                <p>loading...</p>
            ) : (
                <div>
                    {error && (
                        <div>
                            <p>{error.message}</p>
                        </div>
                    )}
                    <div>{data && <p>{data.id}</p>}</div>
                </div>
            )}
        </div>
    );
};

export default App;

Enter fullscreen mode Exit fullscreen mode

This is the very basic version of useAxios hook. You can add more customisations to it as per your requirements.

Thank you so much for reading this article and do let me know your thoughts about this custom hook! Also, for daily updates you can also connect with me on Twitter or buy me a coffee if you like my articles.

Keep learning 🙌

Top comments (61)

Collapse
 
ecyrbe profile image
ecyrbe • Edited

Hello,

here is a reviewed code for final solution :

import { useState, useEffect } from 'react';
import axios from 'axios';

axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';

/**
 fixed :
  - no need to JSON.stringify to then immediatly do a JSON.parse
  - don't use export defaults, because default imports are hard to search for
  - axios already support generic request in one parameter, no need to call specialized ones
**/
export const useAxios = (axiosParams) => {
    const [response, setResponse] = useState(undefined);
    const [error, setError] = useState('');
    const [loading, setloading] = useState(true);

    const fetchData = async (params) => {
      try {
       const result = await axios.request(params);
       setResponse(result.data);
       } catch( error ) {
         setError(error);
       } finally {
         setLoading(false);
       }
    };

    useEffect(() => {
        fetchData(axiosParams);
    }, []); // execute once only

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

and to use it :

import { useAxios } from 'axioshook';

const App = () => {
    const { response, loading, error } = useAxios({
        method: 'POST',
        url: '/posts',
        headers: { // no need to stringify
          accept: '*/*'
        },
        data: {  // no need to stringify
            userId: 1,
            id: 19392,
            title: 'title',
            body: 'Sample text',
        },
    });

    return (
        <div className='App'>
            <h1>Posts</h1>

            {loading ? (
                <p>loading...</p>
            ) : (
                <div>
                    {error && (
                        <div>
                            <p>{error.message}</p>
                        </div>
                    )}
                    <div> {
                      // no need to use another state to store data, response is sufficient
                      response && <p>{response.id}</p>
                    }
                    </div>
                </div>
            )}
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
hey_yogini profile image
Yogini Bende

This is a really good optimisation, specially the axios.request 👏
Two points to consider here -

  1. JSON.stringify and JSON.parse was used to avoid possibility of any errors.
  2. An extra state was added in the App component, as we may need to process some data coming from the api response (which is mostly the case).

Thanks for sharing this 🙌

Collapse
 
imervinc profile image
👺Mervyn

Exactly what I was thinking while reading the post XD. And maybe wrap this in React Query for an even juicier hook!

Collapse
 
ecyrbe profile image
ecyrbe

You can get it here :

httpss://github.com/ecyrbe/react-axios-query

It's juste a wrapper. Really simple.

Thread Thread
 
ayoubkhan558 profile image
Muhammad Ayoub Khan
Collapse
 
seetaram_yadav profile image
Seetaram Yadav • Edited

There is small syntax error in losing callback. instead of setloading it should be setLoading. `import { useState, useEffect } from 'react';
import axios from 'axios';

axios.defaults.baseURL = 'jsonplaceholder.typicode.com';

/**
fixed :

  • no need to JSON.stringify to then immediatly do a JSON.parse
  • don't use export defaults, because default imports are hard to search for
  • axios already support generic request in one parameter, no need to call specialized ones **/

`import { useState, useEffect } from 'react';
import axios from 'axios';

axios.defaults.baseURL = 'jsonplaceholder.typicode.com';

/**
fixed :

  • no need to JSON.stringify to then immediatly do a JSON.parse
  • don't use export defaults, because default imports are hard to search for
  • axios already support generic request in one parameter, no need to call specialized ones
    **/
    export const useAxios = (axiosParams) => {
    const [response, setResponse] = useState(undefined);
    const [error, setError] = useState('');
    const [loading, setLoading] = useState(true);

    const fetchData = async (params) => {
    try {
    const result = await axios.request(params);
    setResponse(result.data);
    } catch( error ) {
    setError(error);
    } finally {
    setLoading(false);
    }
    };

    useEffect(() => {
    fetchData(axiosParams);
    }, []); // execute once only

    return { response, error, loading };
    };`

Collapse
 
matek075 profile image
Matek

Hi, small fix:
here const [loading, setloading] = useState(true);
to const [loading, setLoading] = useState(true);

upperCase "L"

Greetings

Collapse
 
kamrulhasan12345 profile image
Mohammad Kamrul Hasan

A correction:
Add axiosParams to the empty list in the second param of the useEffect hook. This will remove a dependency error

Collapse
 
peng profile image
Peng

Wow

Collapse
 
bernasantolin profile image
Antolin B. Bernas Jr.

sir what if the structure has axios interceptor and header is with bearer token?

Collapse
 
dastasoft profile image
dastasoft

Nice article and well explained! If you want to go further with the hook I always recommend this article of Kent C Dodds about not using booleans for loading

Collapse
 
hey_yogini profile image
Yogini Bende

Yes, that is also a good approach. But, if I am using reducers, I just go with states as reducers help us updating states synchronously.

Collapse
 
dastasoft profile image
dastasoft

Reducers can also be useful when the different states are closely related, although both options are good in this case :)

Collapse
 
embrycode profile image
Mason Embry

I believe your fetchData function is getting recreated on every render of the hook. Consider moving it into a useCallback. I'm curious what other people think about that or if I'm missing something. Nice hook!

Collapse
 
ecyrbe profile image
ecyrbe • Edited

useCallback don't magically make the function not being recreated... it just makes sure the instance created the first time is used the second time, etc... but unfortunately function creation is still done at runtime, so no performance gain from using useCallback.

So you should only use useCallback if you need a stable function instance for referencing in another component.

Since fetchData is only used internally and not exposed outside the hook, useCallback will in fact bring a memory cost and no performance gain.

See this article or this one for another explanation

Collapse
 
embrycode profile image
Mason Embry

Yeah, I knew the primary purpose of useCallback was for a stable reference but I also thought it was important in some cases for performance reasons. Those articles are helpful for understanding that that's not a big deal. Thanks!

Collapse
 
hey_yogini profile image
Yogini Bende

Really good explanation here! Even I wondered initially if useCallback will make any difference. Thanks for sharing!

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
hey_yogini profile image
Yogini Bende

Any issue occurred? Can you please share details?

Collapse
 
dougantr profile image
Douglas Trofino

Nice hook!
I will try that in my projects. Thank you for sharing!
I just would return the hooks states as an array, so I can rename it.

return [ response, error, loading ];

const [ products, productsError, productsLoading ] = useAxios(params);

In case a I need more than one api request in the same component, I can differentiate them.

Collapse
 
steventhan profile image
Steven Than

You can rename with object returns as well

const { response: products, loading: productsLoading, error: productsError } = useAxios(...)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
hey_yogini profile image
Yogini Bende

Hey, that's a nice improvement!

Collapse
 
anastawfik profile image
Anas Tawfik

wow, great job that looks nice, but it might be impractical to use for POST methods as you want to control the triggering of the request, like when user submit a form, but hooks are triggered once the component is loaded and you can't use it inside a function or a condition,
what about implementing same as Apollo does, if the method is POST, you dont trigger the fetchData yourself, but return it with the other variables.

I also agree with the comment below that suggest using an array so we can rename it on use, so it can be used multiple times same component, like GET the saved form values, then POST after the user fill the rest of the form and submit

Collapse
 
amanzx4 profile image
AmanZrx4

I was thinking the same, but then even if we return the fetch method, we just cannot use a custom hook inside an event handler. Then we have to revert back to a normal js function or a fetch component, no more a custom hook. React is weird. I didn't find any standard way to achieve this

Collapse
 
jamesburton profile image
James Burton

Other potential enhancements:
Add local storage and stale state cache, to show stale whilst loading new version.
Add withAxios HOC to wrap and inject these properties into any "dumb" component.
Provide an example extending the hook to tailored hooks:
const usePost = id => useAxios('/api/posts/' + id);
... It's an option for grouping your data access into a secondary layer, so components then just hooks the most basic hooks.
Providing examples to disable/mock the fetch in unit tests is a handy one to consider ... Storybook too ... Like having a testData parameter which is used if typeof window != 'object', or jest mocking.
Finally a withAxiosDisplay HOC could wrap that with a loader overlay, loading spinner or skeleton display, error display, and then child component, so you get this:
let Post = ({data}) => {
// Just render from data
};
const Post123 = withAxiosDisplay('/api/post/123', Post):
// Render
... Loading display, error handler, retry and passing data then all included.

Collapse
 
itayperry profile image
Itay

I've used this solution with TypeScript and added an option to cancel the request:

import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { useEffect, useRef, useState } from 'react';
import { environment } from '../../environments/environment';

axios.defaults.baseURL = environment.baseUrl;

/**
 * Generic T: represents the data returned from API request.
 *
 * Generic D: represents the body data of the Axios
 * request (usually does not exist in GET method).
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useAxios = <T, D = any>(axiosParams: AxiosRequestConfig<D>) => {
  const [response, setResponse] = useState<T | null>(null);
  const [error, setError] = useState<AxiosError | unknown>();
  const [loading, setLoading] = useState(true);
  const controllerRef = useRef(new AbortController());
  const cancelRequest = () => {
    controllerRef.current.abort();
  };

  useEffect(() => {
    const fetchData = async (params: AxiosRequestConfig<D>) => {
      try {
        const result = await axios.request<T>({
          ...params,
          signal: controllerRef.current.signal,
        });
        setResponse(result.data);
      } catch (err: AxiosError | unknown) {
        if (axios.isAxiosError(err)) {
          setError(err);
          // this will narrow the type :)
          // Access to config, request, and response
        } else {
          setError(err);
          // Just a stock error
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData(axiosParams);
  }, [axiosParams]);

  return { cancelRequest, response, error, loading };
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
brayanarrieta profile image
Brayan Arrieta

nice post I recommend a change from then/catch to async/await approach

Collapse
 
rajvirs99 profile image
Rajvir Singh

Hello,
This is good. However, I want to ask if I want to use in onClick function. Something like calling the hook on user login. How can we do it ?

Collapse
 
ayoubkhan558 profile image
Muhammad Ayoub Khan

I also want this.

Collapse
 
narven profile image
Pedro Luz

Never understood quite the use of hooks like this. Requests should not be made from inside components, mainly due to separation of concerns and specially in the case that you might need to make the same request from multiple places in you application. So would you use that twice in 2 diferent components?

The only point I see in using this is for a quick prototype to trash the next day.

Personally since I also use redux-sagas, there is usage for me.

But still a good tutorial :)

Some comments have been hidden by the post's author - find out more