1

I am trying to create a reusable custom hook (useRequest) where I can fetch data with axios, display it and have a loading state. In case of an error I want it to be caught by useRequest. I'm having trouble catching eventual errors and passing the axios request to useRequest. Currently I'm only getting null for the error message.

EDIT: I use generated api which uses axios. So to make my fetch request it would look something like this:

import {GeneratedApi} from '/generatedApi'

const generatedApi = new GeneratedApi(configuration) //configuration is for editing the headers etc.
const response = await generatedApi.getData();
setData(response.data);

My code:

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

const useRequest = (promise: Promise<AxiosResponse<any>>) => {
  const [loading, setLoading] = useState<boolean>(true);

  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setError(null);
        await promise;
        setLoading(false);
        setError(null);
      } catch (error) {
        setLoading(false);
        setError("Error: " + JSON.stringify(error));
      }
    };
    fetchData();
  }, [promise]);

  return [loading, error];
};

export default function App() {
  const [data, setData] = useState<any | null>(null);

  const [loading, error] = useRequest(async () => {

    const response = await axios.get("https://jsonplaceholder.typicode.com/todos");
    setData(response.data);
    return response;
  });

  if (loading) {
    return <p>Loading ...</p>;
  } else if (data) {
    return <p>{data}</p>;
  } else {
    return <p>Error: {error}</p>;
  }
}
1
  • Maybe you would like to reference this: swr.vercel.app Commented Feb 18, 2021 at 15:30

2 Answers 2

1

You can pass a function, wrapped in useCallback hook, which would invoke your api call:

import axios, { AxiosResponse } from "axios";
import { useCallback, useEffect, useState } from "react";

const url = "https://jsonplaceholder.typicode.com/todos"

const useRequest = (apiCall: () => Promise<AxiosResponse<any>>, setData: (data: any) => void) => {
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setError(null);
        const response = await apiCall()
        setData(response.data)
        setLoading(false);
        setError(null);
      } catch (error) {
        setLoading(false);
        setData(null)
        setError("Error: " + JSON.stringify(error));
      }
    };
    fetchData();
  }, [apiCall, setData]);

  return [loading, error];
};

export default function App() {
  const [data, setData] = useState<any | null>(null);
  const fun = useCallback(() => axios.get(url), [])

  const [loading, error] = useRequest(fun, setData);

  if (loading) {
    return <p>Loading ...</p>;
  } else if (data) {
    return <p>{'data'}</p>;
  } else {
    return <p>Error: {error}</p>;
  }
}
Sign up to request clarification or add additional context in comments.

5 Comments

Thanks, sadly when I open your link I only get the default Codesandbox project. If you move the complete logic to useRequest then I would only be able to make that specific axios call right? What I actually want is to be able to reuse useRequest with different axios calls (actually generated by OpenApi). Sorry if I'm missing the point, quite new to programming.
You could also pass an additional parameter using which useRequest would differentiate the type of axios call.
Sorry I should probably have made it more clear that I am actually using a generated api which uses axios calls. Which in that case means I don't have a specific url I can use. I'll edit the question so it's more clear.
Ok, I got it. What about this? Check the updated answer.
Your answer helped point me in a working direction. Thank you :) I posted my current solution as a comment and will update it if I find a better solution.
1

Konstantin Samarin's answer helped to point me in the right direction.

My current solution has a missing dependency(callback) and might not be ideal. Adding the dependency causes infinite rerenders.

EDIT 1: Added isMounted reference to avoid setting state on an unmounted component. Moved [data, setData] to the custom hook.

import axios, { AxiosResponse } from "axios";
import { useCallback, useEffect, useState } from "react";

interface RequestReponse {
    loading: boolean,
    error: string | null
}

function useRequest<T>(callback: any, dependencies: any[]): [boolean, (string | null), (T | null)] {
    const [loading, setLoading] = useState<boolean>(true);

    const [error, setError] = useState<string | null>(null);

    const [data, setData] = useState<T | null>(null);

    const navigate = useNavigate();

    useEffect(() => {
        let isMounted = true;

        async function doRequest() {
            try {
                setError(null);
                setData(null);
                await callback();
                if (isMounted) {
                    setLoading(false);
                    setData(null);
                }
            } catch (error) {
                setLoading(false);
                setError(error)
            }
        }

        doRequest();
        return () => {
             isMounted = false;
        };
    }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps

    return [loading, error, data];
}

export default function App() {

  const generatedApi = new GeneratedApi();

  const [loading, error, data] = useRequest(() => generatedApi.getData(), [])

  if (loading) {
    return <p>Loading ...</p>;
  } else if (data) {
    return <p>{data}</p>;
  } else {
    return <p>Error: {error}</p>;
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.