Modern web applications are powered by data. Whether it’s user profiles, blog posts, or products, most React apps interact with APIs to send and receive data. Among the various ways to make HTTP requests, the Fetch API is one of the most common and straightforward options available in JavaScript.
In this post, we will explore how to use the Fetch API in React for all major HTTP operations — GET, POST, PUT, and DELETE. We’ll also cover how to handle asynchronous data fetching, manage component lifecycle events, handle loading and error states, and integrate best practices for clean and efficient code.
What is the Fetch API?
The Fetch API is a modern JavaScript interface for making HTTP requests. It allows developers to fetch resources from a network — such as REST APIs — and returns a Promise that resolves to the response object.
Unlike older methods like XMLHttpRequest, Fetch is simpler, cleaner, and works seamlessly with asynchronous operations using async/await syntax.
Basic Fetch Example
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
This example makes a simple GET request, converts the response to JSON, and logs the data.
Why Use the Fetch API in React?
React is a UI library that focuses on rendering components, not data fetching. However, most real-world React apps need to communicate with a backend API.
The Fetch API integrates smoothly with React because:
- It’s built-in: No need to install additional libraries.
- Promise-based: Works well with modern JavaScript features like async/await.
- Flexible: Supports GET, POST, PUT, DELETE, and more.
- Easy to combine with React hooks: Particularly
useEffectanduseState.
Setting Up a React Project
Before fetching data, you need a React environment.
Using Create React App
Run the following commands in your terminal:
npx create-react-app fetch-demo
cd fetch-demo
npm start
After the app starts, open App.js. This is where you can implement your data fetching examples.
Performing a GET Request
GET requests are the most common. They are used to retrieve data from an API.
Let’s build an example where we fetch and display a list of users.
Example: Fetching Data in useEffect
import React, { useState, useEffect } from "react";
function App() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then((data) => setUsers(data))
.catch((error) => setError(error.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h1>User List</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
export default App;
Explanation
- useState:
Used to manageusers,loading, anderrorstates. - useEffect:
Fetches data when the component first mounts. - fetch():
Sends a GET request to the API endpoint. - Error Handling:
Uses.catch()to handle errors gracefully. - Conditional Rendering:
Displays loading or error messages before showing data.
Using Async/Await for Fetch
The Fetch API works even better with async/await, making code more readable.
Example: Using Async/Await
import React, { useState, useEffect } from "react";
function App() {
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const data = await response.json();
setPosts(data);
} catch (error) {
console.error("Error fetching posts:", error);
}
};
fetchPosts();
}, []);
return (
<div>
<h2>Posts</h2>
{posts.slice(0, 5).map((post) => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
}
export default App;
Here, the async function inside useEffect fetches posts and sets them into state.
Performing a POST Request
A POST request is used to send data to the server — typically to create new records.
Example: Submitting Data with Fetch
import React, { useState } from "react";
function CreatePost() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [response, setResponse] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
const newPost = {
title: title,
body: body,
userId: 1,
};
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newPost),
});
const data = await res.json();
setResponse(data);
} catch (error) {
console.error("Error:", error);
}
};
return (
<div>
<h2>Create a New Post</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<textarea
placeholder="Body"
value={body}
onChange={(e) => setBody(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
{response && (
<div>
<h3>New Post Created:</h3>
<p>ID: {response.id}</p>
<p>Title: {response.title}</p>
</div>
)}
</div>
);
}
export default CreatePost;
Explanation
- The
POSTmethod is used to send data. - The
headersproperty specifies that the body content is JSON. JSON.stringify()converts the JavaScript object into a JSON string.- The response from the server is handled with
.json()to parse the returned data.
Performing a PUT Request
A PUT request updates an existing resource.
Example: Updating Data
import React, { useState } from "react";
function UpdatePost() {
const [title, setTitle] = useState("");
const [postId, setPostId] = useState("");
const [message, setMessage] = useState("");
const handleUpdate = async () => {
try {
const response = await fetch(https://jsonplaceholder.typicode.com/posts/${postId}, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title: title, body: "Updated content" }),
});
if (response.ok) {
const data = await response.json();
setMessage(Post ${data.id} updated successfully);
}
} catch (error) {
setMessage("Error updating post");
}
};
return (
<div>
<h2>Update Post</h2>
<input
type="text"
placeholder="Post ID"
value={postId}
onChange={(e) => setPostId(e.target.value)}
/>
<input
type="text"
placeholder="New Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button onClick={handleUpdate}>Update</button>
<p>{message}</p>
</div>
);
}
export default UpdatePost;
This example updates a post’s title by sending a PUT request.
Performing a DELETE Request
DELETE requests remove resources from a server.
Example: Deleting Data
import React, { useState } from "react";
function DeletePost() {
const [postId, setPostId] = useState("");
const [message, setMessage] = useState("");
const handleDelete = async () => {
try {
const response = await fetch(https://jsonplaceholder.typicode.com/posts/${postId}, {
method: "DELETE",
});
if (response.ok) {
setMessage(Post ${postId} deleted successfully);
} else {
setMessage("Failed to delete post");
}
} catch (error) {
setMessage("Error deleting post");
}
};
return (
<div>
<h2>Delete Post</h2>
<input
type="text"
placeholder="Post ID"
value={postId}
onChange={(e) => setPostId(e.target.value)}
/>
<button onClick={handleDelete}>Delete</button>
<p>{message}</p>
</div>
);
}
export default DeletePost;
This demonstrates a basic DELETE request pattern.
Handling Loading and Error States
When fetching data, it’s important to handle both loading and error states properly.
Example:
import React, { useState, useEffect } from "react";
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
if (!response.ok) throw new Error("Failed to fetch");
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <p>Loading data...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h3>Todo Item:</h3>
<p>{data.title}</p>
</div>
);
}
export default DataFetcher;
This ensures users see feedback during network operations.
Fetching Data on User Actions
Sometimes, you don’t want to fetch data on component load but rather in response to user interaction.
Example:
import React, { useState } from "react";
function SearchUser() {
const [userId, setUserId] = useState("");
const [user, setUser] = useState(null);
const handleFetch = async () => {
const response = await fetch(https://jsonplaceholder.typicode.com/users/${userId});
const data = await response.json();
setUser(data);
};
return (
<div>
<h2>Search User</h2>
<input
type="text"
placeholder="Enter user ID"
value={userId}
onChange={(e) => setUserId(e.target.value)}
/>
<button onClick={handleFetch}>Fetch User</button>
{user && (
<div>
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
</div>
)}
</div>
);
}
export default SearchUser;
This pattern is useful for search fields and data lookups.
Managing Multiple API Calls
Sometimes you need to call multiple APIs at once. You can use Promise.all() to fetch multiple endpoints concurrently.
Example:
import React, { useState, useEffect } from "react";
function MultiFetch() {
const [posts, setPosts] = useState([]);
const [users, setUsers] = useState([]);
useEffect(() => {
const fetchAll = async () => {
const [postsResponse, usersResponse] = await Promise.all([
fetch("https://jsonplaceholder.typicode.com/posts"),
fetch("https://jsonplaceholder.typicode.com/users"),
]);
const postsData = await postsResponse.json();
const usersData = await usersResponse.json();
setPosts(postsData.slice(0, 5));
setUsers(usersData.slice(0, 5));
};
fetchAll();
}, []);
return (
<div>
<h2>Posts and Users</h2>
<div>
<h3>Posts</h3>
{posts.map((p) => (
<p key={p.id}>{p.title}</p>
))}
</div>
<div>
<h3>Users</h3>
{users.map((u) => (
<p key={u.id}>{u.name}</p>
))}
</div>
</div>
);
}
export default MultiFetch;
This improves performance by fetching data simultaneously.
Best Practices
- Always handle errors: Network requests can fail for many reasons.
- Show loading indicators: Keep users informed.
- Use async/await for cleaner code: It’s more readable than chaining
.then(). - Avoid fetching in loops: Use
Promise.all()for multiple requests. - Abort fetches when unmounted: Prevent memory leaks using AbortController.
- Use caching where possible: To reduce redundant API calls.
Using Fetch with Custom Hooks
To keep code organized, you can create a custom hook for fetching data.
Example:
import { useState, useEffect } from "react";
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error("Error fetching data");
const json = await response.json();
setData(json);
} catch (err) {
if (err.name !== "AbortError") setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
Then use it like this:
import React from "react";
import { useFetch } from "./useFetch";
function Users() {
const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/users");
if (loading) return <p>Loading...</p>;
if (error) return <p>{error}</p>;
return (
<ul>
{data.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
export default Users;
This reusable hook simplifies data fetching logic across multiple components.
Leave a Reply