DEV Community

loading...

useAxios: React hook for any Axios call

Kevin White
Kevin lives in Oregon brewing his coffee, walking his puppy, and slowly learning to program better.
・2 min read

useAxios() is a React hook that simplifies async fetching and state management. Source code and live example

Want to suggest an improvement? I'm all ears! Please file an issue or open a PR!

Usage

import React, { useState } from "react";
import { useAxios } from "./use-axios";

const App = () => {
  const [id, setId] = useState("1");
  const axiosConfig = { method: "get", timeout: 2500 };
  const { isLoading, isError, response } = useAxios(
    `https://pokeapi.co/api/v2/pokemon/${id}`,
    axiosConfig
  );

  return (
    {response?.data && <div>{data}</div>}
    {isLoading && <LoadingIcon/>}
    {isError && <ErrorMsg/>}
  );
};
Enter fullscreen mode Exit fullscreen mode

Overview

useAxios is an Axios-specific implementation of my generic useAsyncFunc React hook.

One issue for async operations is when the return value is no longer required. For example, the user leaves the page (the requesting component is unmounted) or the user provides a new search query (the old search query's response is superfluous).

You might see an error like this:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

In these situations, we want to cancel the initial request. The browser Web API provides the AbortController interface; it is a controller object that allows you to abort one or more Web requests. Axios provides similar capability with the CancelToken class. CancelTokens are straightforward to implement if you are already using the Axios library. You read a little more about each implementation here.

useAxios

/**
 *
 * @param {string} url      - The url to call
 * @param {object} [config] - The axios config object; defaults to GET, etc
 * @returns {state}         - { isLoading, isError, response }
 */
const useAxios = (url, config) => {
  // useReducer manages the local complex state of the async func hook's lifecycle.
  // See the source code for the full reducer!
  // NOTE: it is easy to modify or expand the reducer to fit your needs.
  const [state, dispatch] = useReducer(axiosReducer, {
    isLoading: false,
    isError: false
  });

  useEffect(() => {
    // Declare Axios cancel token
    const source = CancelToken.source();

    // Define the axios call
    const callAxios = async () => {
      // Begin with a clean state
      dispatch({ type: "AXIOS_INIT" });

      try {
        // Straightforward axios call,
        // With cancel token inserted into config
        const response = await axios(url, {
          ...config,
          cancelToken: source.token
        });
        dispatch({ type: "AXIOS_SUCCESS", payload: response });
      } catch (err) {
        // Two options on error:
        // 1. If error is an axios cancel, simply return and move on
        // 2. For all other errors, assume async failure and dispatch failure action
        if (isCancel(err)) {
          console.log("Canceled request.");
          return;
        }
        dispatch({ type: "AXIOS_FAILURE" });
      }
    };

    // Invoke the defined axios call
    callAxios();

    // On unmount, cancel the request
    return () => {
      source.cancel("Operation canceled.");
    };

    // NOTE: here be dragon!
    // My instinct was include the axios config in this array, e.g. [url, config]
    // This causes an infinite re-render loop that I have not debugged yet :-/
  }, [url]);

  return state;
};

export default useAxios;
Enter fullscreen mode Exit fullscreen mode

Conclusion

It is good to cancel superfluous requests so that they do not become memory leaks! I hope you find this example helpful.

Discussion (21)

Collapse
sroehrl profile image
neoan

I fail to understand the "why". In what scenario is this approach easier than using axios directly, or better, an axios instance?

Collapse
kwhitejr profile image
Kevin White Author

Mainly it is syntactical sugar around common needs, such as hooks for isLoading, isError, and the built-in request cancellation.

Collapse
sroehrl profile image
neoan • Edited

I see. More syntactical consistency rather than sugar then it that case?
But still, mentioned problems wouldn't exist if API calls happened on the store level. In your components, you would still use hooks depending on the capabilities of your state management. If you combine that with a layer for an axios instance that handles headers & authorisation, then you remove the need for a custom config each time. It's a way cleaner and more maintainable code base then.
Don't get me wrong,there is nothing wrong with your approach, but at the end you produce a lot of lines of code whenever you make something as standard as an API call (which easily happens hundreds of times in a medium sized application)

Thread Thread
kwhitejr profile image
Kevin White Author

I dig it. Do you have an example repo that implements that pattern from which I could learn?

Thread Thread
sroehrl profile image
neoan

Unfortunately I have nothing I can share, no. But it shouldn't be hard:
We utilize axios interceptors to check if a used is authenticated and attach the JWT token accordingly. With hookState , recoil or redux (whatever you use), you can then assign the transactions accordingly.

Collapse
arieled91 profile image
Ariel

I think you should delete any prop from useEffect dependencies array, that's causing the loops. Also it can be a good idea to export a refresh function, letting the ui decide when to call axios again.

Collapse
kwhitejr profile image
Kevin White Author

Can you point me to an example of a refresh function? This concept is new to me. Thanks for the advice!

Collapse
arieled91 profile image
Ariel • Edited

It's just an internal function that you can write inside your hook. It calls the axios function (again), and you have to return it without the '()', with the rest of the state values. So, from the ui you call it whenever you want to refresh.

//hook
const useExample = () => {
//state props
const refresh = () => {
// call axios impl
}
return {data, refresh}
}

//component
const [data, refresh] = useExample ()

button onClick={refresh}

I wrote this with my phone, sorry if it has a bug.

Thread Thread
kwhitejr profile image
Kevin White Author

Thank you! I'm following your pattern. Gonna meditate on it a while then refactor it in. Cheers!

Collapse
simplecookie profile image
Marcus

I've done similar. But I don't see any reason for useReducer. useState us sufficient and less clottery, makes it more readable when you don't have to deal with actions.

Collapse
kwhitejr profile image
Kevin White Author

I agree that a baseline implementation is more readable if implemented with useState. The idea here is that the reducer is ready and waiting if a user requires more complex local state management.

Collapse
simplecookie profile image
Marcus

Sure but most often this will not be required 😊 it's a form of premature optimisation imo. Although it's not an expensive one.

Collapse
jay8142 profile image
Sir Broddock

This looks interesting. Your comment at the end indicates that this hook is incomplete or not ready for wider implementation. Is that the case?

Collapse
kwhitejr profile image
Kevin White Author

I'd use with caution until I can figure out the infinite render loop issue. This is intended more for learning by way of proof-of-concept than for use in prod.

Collapse
merthod profile image
Merthod

I believe there's already an axios hook on npm. Great dive though.

Collapse
kwhitejr profile image
Kevin White Author • Edited

Found it here: preview.npmjs.com/package/use-axios

though it looks like it just wraps another hook, useAsync: github.com/ArnoSaine/use-axios/blo...

Unfortunately I can't find the source code for the core useAsync hook :-/ Lastly, looks like the use-axios package contains no tests. Neither do I, yet :-) Buyer beware.

Collapse
kalashin1 profile image
Kinanee Samson

Thanks, pal but I think I would rather just stick with the useEffect hook and fetch

Collapse
kwhitejr profile image
Kevin White Author

✊ more power to you!

Collapse
gaurav5430 profile image
Gaurav Gupta

does the infinite render loop happen because the config object is created new in every rerender of the parent, which leads to running useEffect again?

Collapse
kwhitejr profile image
Kevin White Author

Moving the axiosConfig declaration out of the App component did indeed stop the re-render loop! Great tip!