Axios vs. Fetch
In the dynamic realm of JavaScript and front-end development, selecting the appropriate tool for HTTP requests is critical. Axios and Fetch 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.
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, an excellent library by Tanner Linsley. 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 from the main maintainer of this package, TkDodo.
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.
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 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:
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.
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":
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:
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:
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:
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:
- Set the method to POST,
- Set headers (in our case) to
{ "Content-Type": "application/json" }
(Many backends require this, as they will not process the body properly otherwise.) - 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:
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.
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:
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:
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 does in his package, 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:
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. 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, 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.
Top comments (10)
There are a few extra aspects of the Fetch API that I think folks forget about:
axios
to keep up with security problems, but not with the Fetch API.axios
takes up space in yournode_modules
and bundle, but the Fetch API does not.axios
does not.While I know that this isn't the point of your article, I think it's important to note that String Manipulation of URLs is an Anti-Pattern.
In many cases, the biggest argument against using fetch directly is running the same code in node and in the browser. Fetch is not accessible in node, so you would need to use a library like node-fetch in order to access similar functionality on the backend. Rather than taking this route, many REST libraries can run on both sides because they polyfill on the inside.
This is outdated. You can use fetch in recent versions.
Anyway, the argument of having a stable layer in between is still compelling.
Another argument to make is that axios gives you a better thought out API and more options - eg having a progress callback when uploading or downloading large files.
I agree its outdated, however, many companies run with older versions of technologies and shifting the infrastructure requires jumping through hoops. In an ideal scenario, those in charge of infrastructure should keep up with the versions, but unfortunately its not always the case.
We got rid of
node-fetch
from a large nextjs-project (both server and client side) in a single commit and two files changed:package.json
services.ts
(the composition root for our DI)The problem is not in the
fetch
, the problem is that view directly depends on the implementation isntead of depending on the abstraction.So, detailed research. But I guess it contains a problem, that could be expressed with the simple question: why React components/hooks have to warry about fetch or axios?
React components MUST rely on the abstraction, that MUST be provided on the highest level as it is possible (ideally on the application level, but considering SPA architecture it could be a "page"-level or something like that).
Let's guess we need to display the Books fetched via some rest-like API.
It doesn't mean that we need to write fetch requests inside of the react-components or even hooks.
We need to create an interface (or type):
Then we need to declare correspondent dependency inside of the react-component or hook. For simplicity let's select the context api
then we can use it:
Having this code we can write tests for this components WITHOUT mocking the
fetch
.Please note, that component
LibraryBooks
doesn't know anything about the source of the data, it is not aware of authorization, content types, http-respones, base urls, protocols etc.Sure, we need to create function that implements the
IBooksProvider
interface.But we should to do the same.
This function MUST NOT depend directly on the
fetch
oraxios
. Again, it MUST depend on the abstraction:Where the types
HttpOptions
andHttpResponse
could be as narrow as it allows your project (the backend you are using, or are going to use).Let's guess, the following:
Then we need to create a function
getLibraryBooks
:Again, we can write tests for this function. Note, it is not aware of the base url and authorization. Also it assumes that
httpClient
deserializes body according to the response headers.Please note, that for the function
getLibraryBooks
any response status except of200
and404
is unexpected, so it can just throw an error (nothttpClient
, but this function)So, now we need to write an implementation for our
IHttpClient
.And only now we should choose the fetch or axios. More then, this implementation MUST NOT depend on the
fetch
oraxios
-instance directly, it must depend on the abstraction, and allow to inject the correspondent instances.The practice shows that such implementation has the same complexity for
fetch
andaxios
. We had a nextjs-project (usedfetch
) and pure back-end nodejs project (usedaxios
) and we have a service that must work on both projects. So we had to implement theIHttpClient
for both:fetch
andaxios
to allow to use this Service in both projects. Later when we rewrite other services of the back-end project to useIHttpClient
instead of axios, we got rid of the last one, and after node20 LTS released we removed thenode-fetch
from the both projects.No react-component suffered )
Summarizing:
The title of this article should be modified to say "... in React".
Not all of us use Axios, because not all of us need it.
Fetch works perfectly fine in vanilla JS applications.
Thanks for the great post!!! A lot of details! There are also axios interceptors that are very cool to use 😊
I have never understood why people insists on installing something for things you can already natively do.
This article is not for beginners.