Why do people use Axios instead of Fetch
Tags: webdev, javascript, beginners, programming
Published: 2024-01-29 13:44:18

## Axios vs. Fetch
In the dynamic realm of JavaScript and front-end development, selecting the appropriate tool for HTTP requests is critical. [Axios](https://github.com/axios/axios) and [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) stand out as two leading contenders, each offering distinct features and benefits. This article delves into their differences and practical applications, providing a comprehensive comparison.

## Data Fetching in React Using Axios
Fetching data in React with `axios` is a straightforward process. `axios` is a promise-based HTTP client for both the browser and Node.js, often lauded for its simplicity and ease of use. Here, we'll explore a practical example involving data retrieval from the Star Wars API.

## Basic Implementation
Let's begin with a basic implementation using the `fetchData` function within a `useEffect` hook to fetch data upon component mount. `axios` performs a `GET` request, and upon promise resolution, the state variable `data` is updated with the fetched information. If `data` exists, it's then displayed in the UI.

```typescript
export default function App() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get("https://swapi.dev/api/starship/9");
      const d = response.data;
      setData(d);
    };

    fetchData();
  }, []);

  return data ? <pre>{JSON.stringify(data)}</pre> : null;
}
```

However, this basic implementation lacks essential features and is prone to errors. For a more robust solution, consider using [tanstack-query](https://tanstack.com/query/latest), an excellent library by [Tanner Linsley](https://twitter.com/tannerlinsley). It simplifies many complex tasks, offering features that are challenging to build from scratch. The final version of our code above is inspired by an [article](https://tkdodo.eu/blog/) from the main maintainer of this package, [TkDodo](https://tkdodo.eu/blog/).

## Enhanced Implementation
The improved version addresses the shortcomings of the initial code by handling loading states, errors, and canceling fetch operations when a component unmounts.

```typescript
export default function App() {
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState(null);
  const [error, setError] = useState<AxiosError | null>(null);

  useEffect(() => {
    let ignore = false;

    const fetchData = async () => {
      setIsLoading(true);

      try {
        const response = await axios.get("https://swapi.dev/api/starship/9");
        const d = response.data;

        if (!ignore) {
          setData(d);
          setError(null);
        }
      } catch (error) {
        if (!ignore) {
          setError(error as AxiosError);
          setData(null);
        }
      } finally {
        if (!ignore) setIsLoading(false);
      }
    };

    fetchData();
    return () => { ignore = true; };
  }, []);

  if (isLoading) return <span>Loading data...</span>;
  if (error) return <span>An error occurred...</span>;
  return data ? <pre>{JSON.stringify(data)}</pre> : null;
}
```

For a deeper understanding of this implementation, exploring TkDodo's insightful [article](https://tkdodo.eu/blog/why-you-want-react-query) is recommended. However, our focus here is on implementing similar functionality without external dependencies like `axios`.

## Data Fetching in React Using Fetch
Fetching data in React applications is a common task. Understanding the subtleties of different methods can greatly enhance your development process. In this segment, we'll delve deeper into the nuances of using `fetch` and compare it with `axios`.

### Barebones Example
The simplest code to fetch data might look like this:

```typescript
const response = await fetch("https://swapi.dev/api/starship/9")
const data = await response.json()
```

This snippet illustrates the straightforward nature of fetch. As a native web API, it requires no additional libraries and is supported by most modern browsers. This simplicity is one of `fetch`'s key strengths.

## JSON Data Transformation with Fetch
A significant difference between `axios` and `fetch` is in handling JSON data. `axios` automatically transforms the response to JSON format under the hood, allowing you to use the response data directly.

```typescript
const data = await response.json()
```

`fetch`, as a lower-level approach, requires explicit conversion of responses to JSON, unlike `axios`'s automatic handling. This requirement might seem cumbersome, but it provides a more detailed level of control over HTTP requests and responses.

## Basic Error Handling
The primary distinction between `fetch` and `axios` lies in their approach to JSON data transformation and error handling. `fetch` requires manual conversion of the response to JSON and does not throw errors for HTTP status codes like 400 or 500. Instead, you need to check the `ok` status of the response, which can be seen as both a feature and a limitation, depending on your application's needs.

By default, `fetch` doesn't throw errors for non-200 statuses, so the code below won't behave the same as `axios` for "400 bad request", "404 not found", or "500 internal server error":

```typescript
try {
    const response = await fetch("https://swapi.dev/api/starship/9")
    const data = await response.json()
} catch (error) {
  // Handle the error
}
```

On the `response` object, `fetch` has an `ok` property which is a boolean. We can write a simple if statement that throws an error for non-200 statuses like this:

```typescript
try {
    const response = await fetch("https://swapi.dev/api/starship/9")

    if (!response.ok) {
        throw new Error('Bad fetch response')
    }

    const data = await response.json()
} catch (error) {
  // Handle the error
}
```

While this code is a start, it's far from the error handling already implemented in `axios`. We're now catching errors for all responses with non-200 statuses without even trying to process the response body.

### Custom ResponseError
For more robust error handling, consider creating a custom `ResponseError` class. This approach offers more control and specificity when managing different HTTP status codes, ensuring a more resilient and user-friendly experience.

We can create a custom `ResponseError` suited to our use case:

```typescript
class ResponseError extends Error {
    response: Response;

    constructor(message: string, response: Response) {
        super(message);
        this.response = response;
        this.name = 'ResponseError';
    }
}
```

Replace the error throwing logic in our fetch routine with this:

```typescript
try {
    const response = await fetch("https://swapi.dev/api/starship/9")

    if (!response.ok) {
        throw new ResponseError('Bad fetch response', response)
    }

    const data = await response.json()
} catch (error) {
    switch (error.response.status) {
      case 404: /* Handle "Not found" */ break;
      case 401: /* Handle "Unauthorized" */ break;
      case 418: /* Handle "I'm a teapot!" */ break;
      // Handle other errors
      default: /* Handle default */ break;
    }
}
```

## Sending POST Requests Using Fetch
While `axios` simplifies the process of sending POST requests by automatically stringifying the request body and setting appropriate headers, `fetch` requires these steps to be done manually. This grants developers more control but also adds complexity.

For fetch, you need to remember three actions:

1. Set the method to POST,
2. Set headers (in our case) to `{ "Content-Type": "application/json" }` (Many backends require this, as they will not process the body properly otherwise.)
3. Manually stringify the body using `JSON.stringify()` (if sending JSON, the body must be a JSON-serialized string).

Let's switch from a GET to a POST request to expand our logic:

```typescript
try {
    const response = await fetch("https://swapi.dev/api/starship/9", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ "hello": "world" })
    })

    if (!response.ok) {
        throw new ResponseError('Bad fetch response', response)
    }

    const data = await response.json()
} catch (error) {
    switch (error.response.status) {
      case 404: /* Handle "Not found" */ break;
      case 401: /* Handle "Unauthorized" */ break;
      case 418: /* Handle "I'm a teapot!" */ break;
      // Handle other errors
      default: /* Handle default */ break;
    }
}
```

## Using Fetch with TypeScript
Incorporating TypeScript into your `fetch` requests brings an added layer of robustness through type safety. By defining interfaces and using generics, TypeScript ensures that your data fetching logic is more predictable and less prone to runtime errors. This practice enhances code maintainability and readability, especially in larger applications.

## Type-Safe Fetch Responses
Implementing type-safe responses ensures that your application correctly handles the data structure returned by the API. This approach minimizes runtime errors and ensures consistency throughout your application.

```typescript
export async function customFetch<TData>(url: string, options?: RequestInit): Promise<TData> {
    const defaultHeaders = {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    };

    let fetchOptions: RequestInit = {
        method: 'GET', // Default method
        headers: defaultHeaders,
        ...options
    };

    // If there's a body and it's an object, stringify it
    if (fetchOptions.body && typeof fetchOptions.body === 'object') {
        fetchOptions.body = JSON.stringify(fetchOptions.body);
    }

    // Merge the default headers with any provided headers
    fetchOptions.headers = { ...defaultHeaders, ...(options?.headers || {}) };

    try {
        const response = await fetch(url, fetchOptions);

        if (!response.ok) {
            throw new ResponseError('Bad fetch response', response);
        }

        return response.json() as Promise<TData>;
    } catch (error) {
        handleFetchError(error);
    }
}

// Usage example
interface Starship {
    // Define the properties of a Starship here
}

try {
    const data = await customFetch<Starship>("https://swapi.dev/api/starship/9")
    // ... use data ...
} catch (error) {
  // ... handle error ...
}
```

### Typing the Rejected Value of the Promise
By typing the rejected value of the promise, you provide more precise error handling. This helps in distinguishing between different types of errors and dealing with them appropriately, enhancing the robustness of your application.

In TypeScript, by default, the error in a catch block is of any type. Because of this, our previous snippets will cause an error in our IDE:

```typescript
try {
    // ...
} catch (error) {
    // 🚨 TS18046: error is of type unknown
  switch (error.response.status) {
    case 404: /* Handle "Not found" */ break
    case 401: /* Handle "Unauthorized" */ break
    case 418: /* Handle "I'm a teapot!" */ break

    // Handle other errors
    default: /* Handle default */ break;
  }
}
```

We can't directly type error as `ResponseError`. TypeScript assumes you can't know the type of the error, as the fetch itself could throw an error other than `ResponseError`. Knowing this, we can have an implementation ready for nearly all errors and handle them in a type-safe manner like this:

```typescript
try {
    // ...
} catch (error) {
    if (error instanceof ResponseError) {
        // Handle ResponseError
        switch (error.response.status) {
          case 404: /* Handle "Not found" */ break;
          case 401: /* Handle "Unauthorized" */ break;
          case 418: /* Handle "I'm a teapot!" */ break;
          // Handle other errors
          default: /* Handle default */ break;
        }
    } else {
        // Handle non-ResponseError errors
        throw new Error('An unknown error occurred when fetching data', {
          cause: error
        });
    }
}
```

To further enhance type safety and developer experience, you might consider using the zod package to parse the output of your `customFetch`. This approach is similar to what TypeScript ninja [Matt Pocock](https://twitter.com/mattpocockuk) does in his package, [zod-fetch](https://github.com/mattpocock/zod-fetch/).

## Conclusion
In this comprehensive exploration of data fetching in React, we've dissected the functionalities and nuances of `axios` and `fetch`. Both tools come with their strengths and particularities, catering to various development needs. As we wrap up, let's distill the essence of this discussion and consider practical applications.

`axios` shines with its straightforward syntax and automatic JSON data handling, making it a developer favorite for its simplicity and ease of use. On the other hand, `fetch`, being a native browser API, offers fine-grained control over HTTP requests, a boon for developers seeking a more hands-on approach.

However, as with all tools, understanding their limitations and how to overcome them is crucial. For instance, `fetch`'s lack of automatic error handling for non-200 status responses can be a stumbling block. But with the custom `ResponseError` class and proper error handling mechanisms, you can significantly enhance its robustness.

Let's revisit the enhanced error handling and TypeScript integration in `fetch` to solidify our understanding:

```typescript
class ResponseError extends Error {
    response: Response;

    constructor(message: string, response: Response) {
        super(message);
        this.response = response;
        this.name = 'ResponseError';
    }
}

function handleFetchError(error: unknown) {
    if (error instanceof ResponseError) {
        // Detailed error handling based on status code
        switch (error.response.status) {
            case 404: /* Handle "Not found" */ break;
            case 401: /* Handle "Unauthorized" */ break;
            case 418: /* Handle "I'm a teapot" */ break;
            // ... other status codes ...
            default: /* Handle other errors */ break;
        }
    } else {
        // Handle non-ResponseError errors
        throw new Error('An unknown error occurred', { cause: error });
    }
}

export async function customFetch<TData>(url: string, options?: RequestInit): Promise<TData> {
    const defaultHeaders = {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    };

    let fetchOptions: RequestInit = {
        method: 'GET', // Default method
        headers: defaultHeaders,
        ...options
    };

    // If there's a body and it's an object, stringify it
    if (fetchOptions.body && typeof fetchOptions.body === 'object') {
        fetchOptions.body = JSON.stringify(fetchOptions.body);
    }

    // Merge the default headers with any provided headers
    fetchOptions.headers = { ...defaultHeaders, ...(options?.headers || {}) };

    try {
        const response = await fetch(url, fetchOptions);

        if (!response.ok) {
            throw new ResponseError('Bad fetch response', response);
        }

        return response.json() as Promise<TData>;
    } catch (error) {
        handleFetchError(error);
    }
}
```

In this code, we see how TypeScript adds a layer of type safety and predictability. By defining a `ResponseError` class, we gain control over how errors are handled and presented. Furthermore, the `customFetch` function illustrates how to build a more robust and versatile fetch utility, one that can be tailored to various data types through generics.

For developers leaning towards TypeScript, integrating type safety into your data fetching strategy isn't just about preventing errors; it's about creating a more predictable, maintainable, and scalable codebase.

As you weigh your options between `axios` and `fetch`, consider your project's needs, your team's familiarity with these tools, and the kind of control or simplicity you're aiming for. Remember, the best tool is the one that aligns with your project's objectives and enhances your development workflow.

Lastly, for those seeking a middle ground between the simplicity of `axios` and the control of `fetch`, consider libraries like [wretch](https://github.com/elbywan/wretch). It offers a much better API and functionalities like:
- request cancellation,
- progress monitoring,
- and request interception,
all while maintaining a small footprint.

In conclusion, whether you choose `axios`, `fetch`, or an alternative like [wretch](https://github.com/elbywan/wretch), your focus should be on writing clear, maintainable, and robust code. Understanding the strengths and weaknesses of each tool will empower you to make informed decisions and build applications that are not only functional but also resilient and enjoyable to develop.