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.
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
anduseEffect
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, anderror
to catch any errors.useEffect
: This is where the API call logic lives. Placing it inuseEffect
ensures data fetching doesn't block UI rendering. The empty dependency array[]
means this effect runs only once, similar tocomponentDidMount
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
(orresponse.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 anoptions
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 usingJSON.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?
- Reusable: You can use
useFetch
anywhere in your app. - Clean: Complex logic is abstracted away, letting your components focus on UI.
- Abort handling: This is a crucial improvement.
AbortController
prevents memory leaks when a component unmounts before the API call finishes. The cleanup function inuseEffect
callscontroller.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?
Feature | Fetch API | Axios |
---|---|---|
Installation | Built-in | Needs to be installed via npm/yarn |
HTTP error handling | Does not auto-reject on HTTP errors | Auto-rejects promise on errors (4xx, 5xx) |
Data transformation | Manual .json() or .text() | Auto-converts to/from JSON |
Request cancellation | Supported via AbortController (verbose) | Supported via CancelToken or AbortController |
Upload/download progress | Not directly supported | Supported |
Old browser compatibility | Needs polyfill for IE | Works on most browsers |
Size | 0KB (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!