[React Basics] Optimizing Fetch API in React: Handling Asynchronous Data

In modern web development, communicating with a server to fetch or send data is an essential part of any application. For React developers, there are many ways to achieve this, but the Fetch API stands out as a powerful, modern, and natively supported tool in browsers.

Optimizing Fetch API in React: Handling Asynchronous Data

This article is a comprehensive guide, diving deep into every aspect of using the Fetch API in React. Whether you're a beginner or an experienced developer, you'll find useful knowledge to optimize API calls in your projects.

What is the Fetch API? Why use it in React?

The Fetch API is a Promise-based method, built into most modern browsers (except IE), providing an easy and logical way to fetch network resources. It is a powerful and flexible replacement for the old XMLHttpRequest (XHR).

So why is Fetch popular in React?

  • Native integration: No need to install extra libraries like Axios or jQuery. This keeps your application bundle lighter.
  • Modern syntax: Uses Promises, allowing you to write asynchronous code cleanly and readably, especially with async/await.
  • Flexible: Offers a powerful set of options (via the options object) to handle complex HTTP requests, including different methods (POST, PUT, DELETE), headers, CORS, and cache management.
  • Works well with Hooks: Integrates smoothly with React Hooks like useState and useEffect for effective data, loading, and error state management.

Basic Operations with Fetch API in React

Let's start with the basics. We'll use the useState and useEffect hooks to perform a simple GET request to fetch data and display it in the UI.

1. Performing a GET Request

This is the most common scenario: fetching data from an API and displaying it.

Scenario: We'll fetch a list of users from the public API JSONPlaceholder.

import React, { useState, useEffect } from 'react'

function UserList() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    // Define fetch function to use async/await
    const fetchUsers = async () => {
      try {
        const response = await fetch(
          'https://jsonplaceholder.typicode.com/users',
        )

        // Fetch does not automatically throw for bad HTTP status codes (like 404, 500)
        // So we need to check manually
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`)
        }

        const data = await response.json() // Convert response to JSON
        setUsers(data)
      } catch (e) {
        setError(e.message)
      } finally {
        setLoading(false) // Stop loading state regardless of success or failure
      }
    }

    fetchUsers()
  }, []) // Empty dependency array ensures useEffect runs once after component mounts

  if (loading) return <div>Loading data...</div>
  if (error) return <div>Error occurred: {error}</div>

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  )
}

export default UserList

Code Analysis:

  • useState: We use three states. users to store data, loading to track loading state, and error to catch any errors.
  • useEffect: This is where the API call logic lives. Placing it in useEffect ensures data fetching doesn't block UI rendering. The empty dependency array [] means this effect runs only once, similar to componentDidMount in class components.
  • async/await: This syntax makes asynchronous code much more readable than chaining .then().
  • Error handling: Fetch only rejects a promise on network errors. For HTTP errors like 404 or 500, it still resolves. So, we must check the response.ok (or response.status) property to handle errors properly.
  • response.json(): This method is also asynchronous and returns a Promise that resolves with the JSON data.

2. Sending Data with a POST Request

Now, let's see how to send data to the server, for example, creating a new post.

import React, { useState } from 'react'

function CreatePost() {
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')
  const [message, setMessage] = useState('')

  const handleSubmit = async (e) => {
    e.preventDefault() // Prevent default form submission
    setMessage('Sending...')

    try {
      const response = await fetch(
        'https://jsonplaceholder.typicode.com/posts',
        {
          method: 'POST', // Specify method
          body: JSON.stringify({
            // Convert JS object to JSON string
            title: title,
            body: body,
            userId: 1, // Usually userId comes from login info
          }),
          headers: {
            'Content-type': 'application/json; charset=UTF-8', // Specify content type
          },
        },
      )

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      const data = await response.json()
      setMessage(`Post created successfully with ID: ${data.id}`)
      setTitle('')
      setBody('')
    } catch (error) {
      setMessage(`Error: ${error.message}`)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Create New Post</h2>
      <div>
        <label>Title:</label>
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          required
        />
      </div>
      <div>
        <label>Body:</label>
        <textarea
          value={body}
          onChange={(e) => setBody(e.target.value)}
          required
        />
      </div>
      <button type="submit">Create</button>
      {message && <p>{message}</p>}
    </form>
  )
}

Key points in the POST request:

  • fetch takes a second argument as an options object.
  • method: 'POST': We must specify the HTTP method.
  • body: This is where you put the data to send. It should be stringified, usually as JSON using JSON.stringify().
  • headers: Very important! The 'Content-Type': 'application/json' header tells the server we're sending JSON data.

Advanced: Reusing Logic with a Custom Hook

As your app grows, you'll find yourself repeating useState and useEffect logic for API calls in many components. This is where Custom Hooks shine! We can encapsulate this logic into a reusable hook.

Let's create a versatile useFetch hook.

// hooks/useFetch.js
import { useState, useEffect } from 'react'

function useFetch(url, options = {}) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    // Use AbortController to cancel request when component unmounts
    const controller = new AbortController()
    const signal = controller.signal

    const fetchData = async () => {
      setLoading(true)
      setError(null)
      try {
        const response = await fetch(url, { ...options, signal })
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`)
        }
        const result = await response.json()
        setData(result)
      } catch (e) {
        if (e.name !== 'AbortError') {
          setError(e.message)
        }
      } finally {
        // Only set loading to false if not aborted
        if (signal.aborted === false) {
          setLoading(false)
        }
      }
    }

    fetchData()

    // Cleanup function
    return () => {
      controller.abort() // Cancel fetch when component unmounts
    }
  }, [url]) // Rerun effect if URL changes

  return { data, loading, error }
}

export default useFetch

How to use the useFetch Custom Hook:

Now, our UserList component becomes much cleaner:

import React from 'react'
import useFetch from './hooks/useFetch' // Import custom hook

function UserList() {
  const {
    data: users,
    loading,
    error,
  } = useFetch('https://jsonplaceholder.typicode.com/users')

  if (loading) return <div>Loading data...</div>
  if (error) return <div>Error occurred: {error}</div>

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users?.map(
          (
            user, // Use optional chaining `?.` in case data is null
          ) => (
            <li key={user.id}>{user.name}</li>
          ),
        )}
      </ul>
    </div>
  )
}

Why is this Custom Hook great?

  1. Reusable: You can use useFetch anywhere in your app.
  2. Clean: Complex logic is abstracted away, letting your components focus on UI.
  3. Abort handling: This is a crucial improvement. AbortController prevents memory leaks when a component unmounts before the API call finishes. The cleanup function in useEffect calls controller.abort(), canceling any pending requests.

Fetch API vs. Axios Comparison

Axios is another popular library for HTTP requests. So, when should you choose Fetch, and when Axios?

Fetch API vs. Axios Comparison

FeatureFetch APIAxios
InstallationBuilt-inNeeds to be installed via npm/yarn
HTTP error handlingDoes not auto-reject on HTTP errorsAuto-rejects promise on errors (4xx, 5xx)
Data transformationManual .json() or .text()Auto-converts to/from JSON
Request cancellationSupported via AbortController (verbose)Supported via CancelToken or AbortController
Upload/download progressNot directly supportedSupported
Old browser compatibilityNeeds polyfill for IEWorks on most browsers
Size0KB (built-in)~12KB (small, but still a dependency)

From the comparison table:

  • Choose Fetch API when: You want a dependency-free, lightweight solution and don't mind writing a bit more code for error handling and data transformation. Great for small projects or when you want full control.
  • Choose Axios when: You need advanced features like auto JSON conversion, simpler HTTP error handling, interceptors, and upload progress support. Ideal for large and complex applications.

Conclusion: Fetch API is a Powerful Tool

The Fetch API is a powerful and essential tool in a React developer's toolkit. By understanding how it works and combining it with React Hooks like useState, useEffect, and custom hooks, you can build efficient, clean, and maintainable React applications.

Hopefully, this comprehensive article has given you the insights and knowledge you need to confidently master API calls with Fetch in your React projects.

Happy coding with React!

Related Posts

[React Basics] Using Axios Library in React: Efficient Asynchronous Data Handling

Learn effective methods for handling asynchronous data with the Axios library in React. Improve your state management, error handling, and application performance.

[React Basics] Guide to Setting Up a React Development Environment

A step-by-step guide for beginners to set up a React development environment. Learn how to install Node.js, npm, and create your first React project with Create React App or Vite.

[React Basics] The key Attribute in React: Understand and Use It Effectively

Do you really understand the key attribute in React? Learn its role, how to use it effectively, and real-world examples to make your code cleaner and more optimized.

[React Basics] Rendering Lists in React: Best Practices and Performance Optimization

Learn techniques for rendering lists in React, from basic .map() usage to advanced methods for handling dynamic data and optimizing performance. This guide is for all levels.