React 18 - useData() hook with axios and TypeScript

3 min read

React 18, useData

Because of the changes from the latest version of React, the useEffect() runs twice. I created a custom hook which will fix race condition and will fetch and set the data only once using axios.

Implementation:

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

// replace this to match your backend's error response body
interface IErrorResponse {
  error: { message?: string };
}

// http://localhost:4000 for example
axios.defaults.baseURL = process.env.API_URL;

const useData = <T>(
  url: string,
  method: Method,
  body?: any,
): [boolean, string | null, T | null] => {
  const [loading, setLoading] = useState<boolean>(false);
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    // from https://beta.reactjs.org/learn/you-might-not-need-an-effect#fetching-data
    let ignore = false;

    /**
     * used to abort an ongoing request, it will not work with Internet Explorer
     * it can be removed and use just the `ignore` variable
     */
    const controller = new AbortController();

    const fetchData = async () => {
      try {
        setLoading(true);

        const response = await axios({
          url: url,
          method: method,
          data: body,
          cancelToken: source.token,
        });

        const data = response?.data;

        if (!ignore) {
          setData(data);
        }
      } catch (err: any) {
        const error = err as Error | AxiosError<IErrorResponse>;
        // replace here with your own error handling
        if (axios.isAxiosError(error)) {
          if (error.code === AxiosError.ERR_CANCELED) {
            console.info('Request was canceled');
            return;
          }
          if (!ignore) {
            setError(error?.response?.data?.error?.message || error.message);
          }
        } else {
          if (!ignore) {
            setError(error.message);
          }
        }

        console.error(err);
      } finally {
        if (!ignore) {
          setLoading(false);
        }
      }
    };

    fetchData().then((r) => r);

    // To fix the race condition, we need to add a cleanup function to ignore stale responses:
    return () => {
      ignore = true;
      controller.abort();
    };
  }, [url]);

  return [loading, error, data];
};

export default useData;

Usage:

import { Link } from 'react-router-dom';

import useData from '@/hooks/use-data';

interface IMovie {
  id: number;
  title?: string;
  description?: string;
  year?: number;
  releaseDate?: string;
}

interface IAllMoviesResponse {
  movies: IMovie[];
}

export default function Movies() {
  const [loading, error, data] = useData<IAllMoviesResponse>(`/v1/movies`, 'GET');

  if (loading) {
    return <div>Loading..</div>;
  }

  if (error) {
    return (
      <div>
        Error: <code>{error}</code>
      </div>
    );
  }

  return (
    <>
      <h2>Choose a movie</h2>

      <ul>
        {data?.movies.map((m) => (
          <li key={m.id}>
            <Link to={`/movies/${m.id}`}>{m.title}</Link>
          </li>
        ))}
      </ul>
    </>
  );
}

These are some snippets from one of my pet projects where I study GO: Full code.

  • #react_18
  • #axios
  • #typescript
  • #javaScript
  • #custom_hook