React Data Fetching : Fetch API, AJAX, Axios
Fetching data from a server is a critical skill for any React developer. Whether building a small widget or a large enterprise application, understanding how to request, receive, and handle data effectively will elevate the performance and reliability of your app. In this comprehensive guide, we will explore three core techniques for HTTP requests in React:
- Fetch API
- AJAX (XMLHttpRequest)
- Axios
Along the way, you'll see real-world code examples implemented with both class components and functional components using hooks. We'll cover features like error handling, request cancellation, request interception, and best practices for organizing and testing your data‑fetching logic. By the end, you'll feel confident choosing the right approach for your next React project.
Table of Contents
- Why HTTP Requests Matter in React
- Prerequisites
- The Fetch API
- Basic Example
- Class Components with Fetch
- Functional Components with Hooks
- Error Handling Patterns
- AbortController for Cancellation
- AJAX with XMLHttpRequest
- XHR Basics
- Integrating XHR in React
- Axios
- Why Choose Axios?
- Installation and Setup
- GET and POST Requests
- Configuring Interceptors
- Request Cancellation
- Global Instances and Defaults
- Comparisons and Best Practices
- Testing HTTP Logic
- Conclusion
Why HTTP Requests Matter in React
React excels at building reusable UI components, but real-world apps almost always need to display dynamic data fetched from remote servers—user profiles, posts, products, analytics dashboards, and more. Efficient data fetching in React involves:
- Performance: Minimizing over‑fetching, controlling concurrency, and leveraging caching.
- Error Handling: Gracefully managing network failures or invalid responses.
- Cancellation: Cleaning up pending requests on unmount to avoid memory leaks.
- Readability: Writing clear, declarative fetching logic that is easy to maintain and test.
Investing time to master these concepts will help you build smoother UIs, avoid common pitfalls, and ship more robust applications.
Prerequisites
Before diving in, ensure you have:
- Node.js and npm or yarn installed.
- A React project bootstrapped using Create React App, Next.js, or similar.
- Familiarity with JavaScript Promises and ES6+ syntax.
If any of these are unfamiliar, refer to the official React documentation or JavaScript tutorials first.
The Fetch API
The Fetch API is a modern, promise‑based browser API for making HTTP requests. It’s built into most modern browsers and provides a native alternative to XMLHttpRequest.
Pros:Cons:
- Native, no extra dependencies
- Promise‑based, clean syntax
- Supports streaming via Response.body
- No built‑in cancellation (requires AbortController)
- Must manually handle JSON parsing errors and HTTP status codes
Basic Example
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Fetch Error:', error));Class Components with Fetch
import React from 'react';
class PostList extends React.Component {
state = {
posts: [],
loading: true,
error: null,
};
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => {
if (!response.ok) {
throw new Error(`Status: ${response.status}`);
}
return response.json();
})
.then(posts => this.setState({ posts, loading: false }))
.catch(error => this.setState({ error, loading: false }));
}
render() {
const { posts, loading, error } = this.state;
if (loading) return <p>Loading posts...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
}
export default PostList;Functional Components with Hooks
import React, { useState, useEffect } from 'react';
function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true; // Cleanup flag to avoid state updates after unmount
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => {
if (!response.ok) throw new Error(`Status: ${response.status}`);
return response.json();
})
.then(data => {
if (isMounted) {
setPosts(data);
setLoading(false);
}
})
.catch(err => {
if (isMounted) {
setError(err);
setLoading(false);
}
});
return () => {
isMounted = false;
};
}, []);
if (loading) return <p>Loading posts...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
);
}
export default PostList;Error Handling Patterns
Fetch only rejects on network failures. To handle HTTP errors, always check response.ok. For robust error handling:
- Check status codes:
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);- Wrap JSON parsing:
let data; try { data = await response.json(); } catch (parseError) { throw new Error('Failed to parse JSON'); }- Centralize error messages:
const handleError = err => { console.error(err); // Show user-friendly UI };
AbortController for Cancellation
Use the AbortController API to cancel in‑flight requests, especially to avoid setting state on unmounted components.
import React, { useEffect, useState } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const response = await fetch('https://api.example.com/users', {
signal: controller.signal,
});
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setUsers(data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
}
}
fetchData();
return () => controller.abort();
}, []);
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
export default Users;AJAX with XMLHttpRequest
Before Fetch, the primary method for asynchronous HTTP in browsers was XMLHttpRequest (XHR). While more verbose, understanding XHR can help maintain or migrate legacy code.
Pros:Cons:
- Wide browser support
- Verbose API
- Callback hell without wrapper
XHR Basics
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/comments');
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.error(`Error: ${xhr.status}`);
}
}
};
xhr.send();
Integrating XHR in React
import React, { useState, useEffect } from 'react';
function CommentList() {
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/comments');
xhr.onload = () => {
if (xhr.status === 200) {
setComments(JSON.parse(xhr.responseText));
} else {
console.error(`Error: ${xhr.status}`);
}
setLoading(false);
};
xhr.onerror = () => {
console.error('Network error');
setLoading(false);
};
xhr.send();
}, []);
if (loading) return <p>Loading comments...</p>;
return (
<ul>
{comments.map(c => <li key={c.id}>{c.body}</li>)}
</ul>
);
}
export default CommentList;
Axios
Axios is a popular, promise‑based HTTP client for browsers and Node.js. It simplifies many aspects of HTTP requests compared to Fetch and XHR.
Pros:Cons:
- Automatic JSON transformation
- Request and response interceptors
- Built‑in cancellation via CancelToken
- Wide browser compatibility
- Additional dependency (~12KB gzipped)
Why Choose Axios? {#why-choose-axios}
- Simplified syntax for setting headers and query parameters
- Uniform handling of JSON and non-JSON responses
- Canceling requests is straightforward
- Create reusable instances for consistent configuration
Installation and Setup
npm install axios # or yarn add axios
Import in your modules:
import axios from 'axios';
GET and POST Requests
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function UserList() {
const [users, setUsers] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(response => setUsers(response.data))
.catch(err => setError(err));
}, []);
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
export default UserList;
// POST example
axios.post('https://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: 'bar',
userId: 1,
})
.then(response => console.log(response.data))
.catch(error => console.error(error));
Configuring Interceptors
Interceptors let you modify requests or responses globally.
import axios from 'axios';
// Request interceptor
axios.interceptors.request.use(
config => {
// e.g., attach auth token
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
},
error => Promise.reject(error)
);
// Response interceptor
axios.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 401) {
// Redirect to login
}
return Promise.reject(error);
}
);
Request Cancellation
import React, { useEffect } from 'react';
import axios from 'axios';
function DataFetcher() {
useEffect(() => {
const source = axios.CancelToken.source();
axios.get('/api/data', { cancelToken: source.token })
.then(res => console.log(res.data))
.catch(thrown => {
if (axios.isCancel(thrown)) {
console.log('Request canceled:', thrown.message);
} else {
console.error(thrown);
}
});
return () => {
source.cancel('Component unmounted');
};
}, []);
return <p>Fetching data...</p>;
}
export default DataFetcher;
Global Instances and Defaults
Create a reusable client with defaults:
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
export default apiClient;
Then import apiClient across your app for consistent behavior.
Comparisons and Best Practices
Feature Fetch API XMLHttpRequest Axios
1. Dependency
- Fetch API: None (native)
- AJAX (XMLHttpRequest): None (native)
- Axios: ~12KB gzipped
2. Syntax
- Fetch API: Promise-based
- AJAX (XMLHttpRequest): Callback-based
- Axios: Promise-based
3. JSON Transformation
- Fetch API: Manual (res.json())
- AJAX (XMLHttpRequest): Manual (JSON.parse)
- Axios: Automatic (response.data)
4. Cancellation Support
- Fetch API: AbortController
- AJAX (XMLHttpRequest): xhr.abort()
- Axios: CancelToken
5. Request Interception
- Fetch API: Not Available (N/A)
- AJAX (XMLHttpRequest): Not Available (N/A)
- Axios: Built-in interceptors
6. Older Browser Support
- Fetch API: Modern browsers only
- AJAX (XMLHttpRequest): All browsers
- Axios: Broad (polyfill to Internet Explorer)
7. Streaming Support
- Fetch API: Yes (streaming body)
- AJAX (XMLHttpRequest): No
- Axios: No (wraps Fetch/XHR)
Best Practices
- Centralize all HTTP logic in service modules rather than scattering fetch calls throughout components.
- Handle errors at both request and response stages—show user-friendly messages.
- Use cancellation or cleanup flags to prevent state updates on unmounted components.
- For larger apps, consider using a library like React Query or SWR for caching, refetching, and background updates.
Testing HTTP Logic
Testing network requests can be done via mocking:
- Jest + msw (Mock Service Worker):
// handlers.js import { rest } from 'msw'; export const handlers = [ rest.get('/api/users', (req, res, ctx) => { return res(ctx.json([{ id: 1, name: 'Alice' }])); }), ]; // UserList.test.js import { setupServer } from 'msw/node'; import { handlers } from './handlers'; import { render, screen } from '@testing-library/react'; import UserList from './UserList'; const server = setupServer(...handlers); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); test('renders users', async () => { render(<UserList />); expect(await screen.findByText('Alice')).toBeInTheDocument(); });- Mocking Axios:
jest.mock('axios'); import axios from 'axios'; import { render, screen } from '@testing-library/react'; import UserList from './UserList'; test('fetches and displays users', async () => { axios.get.mockResolvedValue({ data: [{ id: 1, name: 'Bob' }] }); render(<UserList />); expect(await screen.findByText('Bob')).toBeInTheDocument(); });- Fetch Mock:
import fetchMock from 'jest-fetch-mock'; fetchMock.enableMocks(); test('fetches posts', async () => { fetchMock.mockResponseOnce(JSON.stringify([{ id: 1, title: 'Hello' }])); // Render component and assert... });
Mocking network calls ensures your tests are fast, predictable, and don’t rely on external services.
Conclusion
Mastering data fetching in React—whether via the native Fetch API, traditional XMLHttpRequest, or a robust client like Axios—will help you build dynamic, performant applications. Key takeaways:
- Choose the right tool: Native Fetch for minimal dependencies; Axios for ease of use and advanced features.
- Handle errors and cancellations: Prevent unhandled rejections and memory leaks.
- Organize your code: Centralize HTTP logic, use instances or custom hooks for reuse.
- Test thoroughly: Mock network calls to keep tests reliable and fast.