Search code examples
reactjstypescriptaxiosrefactoringdry

How can I make my axios function reusable in other React/Typescript components?


I am copying/pasting the same code for making axios requests in multiple components like this:

React.useEffect(() => {
    axios
        .get<IDownloads[]>(`${process.env.PUBLIC_URL}/api/downloads`, {

        headers: {
          'Content-Type': 'application/json',
        },
        timeout: 5000,
      })
      .then((response) => {
        setFaqs(response.data);
      })
      .catch((ex) => {
        const err = axios.isCancel(ex)
          ? 'Request cancelled'
          : ex.code === 'ECONNABORTED'
            ? 'A timeout has occurred'
            : ex.response.status === 404
              ? 'Resource not found'
              : 'An unexpected error has occurred';
        setError(err);
      });

  }, []);

Which works, but doesn't follow DRY. I want to be able to reuse this code in other areas of my application but need to be able to change the .get<IDownloads[]>(${process.env.PUBLIC_URL}/api/downloads to work in other areas. Like .get<ISomethingElse[]>(${process.env.PUBLIC_URL}/api/somethingElse I made a new component in attempt to do this

export default function useApiRequest<T>(url: string): { response: T | null; error: Error | null} {
   const [response, setResponse] = React.useState<T | null>(null);
   
    const [error, setError] = React.useState<Error | null>(null);
  
    React.useEffect(() => {
      const fetchData = async (): Promise<void> => {
       try {
           const res = await axios(`${process.env.PUBLIC_URL}${url}`);
           setResponse(res.data);
       } catch (error) {
           setError(error);
       }
    };
    fetchData();
}, [url]);
  
    return { response, error };
  };

and using it in this component like this:

interface IDownloads {
  db_id: number;
  file_description: string;
  file_name: string;
  developer_name: string;
  date_uploaded: string;
  file_url: string;
}

const defaultProps: IDownloads[] = [];

const DownloadCodeSamplesPage: React.FC = () => {

  const downloadQuery = useApiRequest<IDownloads[]>('/api/download');
 
  const [downloads, setDownloads]: [IDownloads[], (posts: IDownloads[]) => void] =
    React.useState(defaultProps);

In my return I am mapping through the downloads like so


                  downloads.map((download) => (
                    <tr key={download.db_id}>
                      <td>{download.file_description}</td>
                      <td>{download.file_name}</td>
                      <td>{download.developer_name}</td>
                      <td>{download.date_uploaded}</td>

When I run the program I am not receiving any data from the api call. What am I doing wrong?


Solution

  • Duplication of State

    Your hook looks great. The issues are with how you are using it in your component. You don't need a local state for downloads -- that's the point of the hook! So kill that React.useState and wherever you were calling setDownloads.

    You can access the downloads from the hook and replace it with an empty array if it's null.

    const downloads = downloadQuery.response ?? [];
    
    const DownloadCodeSamplesPage: React.FC = () => {
      const downloadQuery = useApiRequest<IDownloads[]>("/api/download");
    
      const downloads = downloadQuery.response ?? [];
    
      return (
        <table>
          <tbody>
          {downloads.map((download) => (
            <tr key={download.db_id}>
              <td>{download.file_description}</td>
              <td>{download.file_name}</td>
              <td>{download.developer_name}</td>
              <td>{download.date_uploaded}</td>
            </tr>
          ))}
        </tbody>
        </table>
      );
    };
    

    You might consider creating an axios instance for your site that is preconfigured with the baseUrl and any other settings and use this everywhere in your app.