React Data Fetching : Fetch API, AJAX, Axios

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:

  1. Fetch API
  2. AJAX (XMLHttpRequest)
  3. 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

  1. Why HTTP Requests Matter in React
  2. Prerequisites
  3. The Fetch API
    • Basic Example
    • Class Components with Fetch
    • Functional Components with Hooks
    • Error Handling Patterns
    • AbortController for Cancellation
  4. AJAX with XMLHttpRequest
    • XHR Basics
    • Integrating XHR in React
  5. Axios
    • Why Choose Axios?
    • Installation and Setup
    • GET and POST Requests
    • Configuring Interceptors
    • Request Cancellation
    • Global Instances and Defaults
  6. Comparisons and Best Practices
  7. Testing HTTP Logic
  8. 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:
  • Native, no extra dependencies
  • Promise‑based, clean syntax
  • Supports streaming via Response.body
Cons:
  • 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:

  1. Check status codes:
  2. if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  3. Wrap JSON parsing:
  4. let data;
    try {
      data = await response.json();
    } catch (parseError) {
      throw new Error('Failed to parse JSON');
    }
  5. Centralize error messages:
  6. 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:
  • Wide browser support
Cons:
  • 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:
  • Automatic JSON transformation
  • Request and response interceptors
  • Built‑in cancellation via CancelToken
  • Wide browser compatibility
Cons:
  • 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:

  1. Jest + msw (Mock Service Worker):
  2. // 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();
    });
    
  3. Mocking Axios:
  4. 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();
    });
    
  5. Fetch Mock:
  6. 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.
#React #Fetch API #AJAX #XMLHttpRequest #Axios #data fetching #React hooks #HTTP requests #error handling #cancellation

Subscribe to our newsletter

12k+

Subscribers

Weekly

Frequency

Free

Always