Fetching Data with the Fetch API

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:

  1. It’s built-in: No need to install additional libraries.
  2. Promise-based: Works well with modern JavaScript features like async/await.
  3. Flexible: Supports GET, POST, PUT, DELETE, and more.
  4. Easy to combine with React hooks: Particularly useEffect and useState.

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 (
&lt;div&gt;
  &lt;h1&gt;User List&lt;/h1&gt;
  &lt;ul&gt;
    {users.map((user) =&gt; (
      &lt;li key={user.id}&gt;{user.name}&lt;/li&gt;
    ))}
  &lt;/ul&gt;
&lt;/div&gt;
); } export default App;

Explanation

  1. useState:
    Used to manage users, loading, and error states.
  2. useEffect:
    Fetches data when the component first mounts.
  3. fetch():
    Sends a GET request to the API endpoint.
  4. Error Handling:
    Uses .catch() to handle errors gracefully.
  5. 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 () =&gt; {
  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 (
&lt;div&gt;
  &lt;h2&gt;Posts&lt;/h2&gt;
  {posts.slice(0, 5).map((post) =&gt; (
    &lt;div key={post.id}&gt;
      &lt;h3&gt;{post.title}&lt;/h3&gt;
      &lt;p&gt;{post.body}&lt;/p&gt;
    &lt;/div&gt;
  ))}
&lt;/div&gt;
); } 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 (
&lt;div&gt;
  &lt;h2&gt;Create a New Post&lt;/h2&gt;
  &lt;form onSubmit={handleSubmit}&gt;
    &lt;input
      type="text"
      placeholder="Title"
      value={title}
      onChange={(e) =&gt; setTitle(e.target.value)}
    /&gt;
    &lt;textarea
      placeholder="Body"
      value={body}
      onChange={(e) =&gt; setBody(e.target.value)}
    /&gt;
    &lt;button type="submit"&gt;Submit&lt;/button&gt;
  &lt;/form&gt;
  {response &amp;&amp; (
    &lt;div&gt;
      &lt;h3&gt;New Post Created:&lt;/h3&gt;
      &lt;p&gt;ID: {response.id}&lt;/p&gt;
      &lt;p&gt;Title: {response.title}&lt;/p&gt;
    &lt;/div&gt;
  )}
&lt;/div&gt;
); } export default CreatePost;

Explanation

  1. The POST method is used to send data.
  2. The headers property specifies that the body content is JSON.
  3. JSON.stringify() converts the JavaScript object into a JSON string.
  4. 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 (
&lt;div&gt;
  &lt;h2&gt;Update Post&lt;/h2&gt;
  &lt;input
    type="text"
    placeholder="Post ID"
    value={postId}
    onChange={(e) =&gt; setPostId(e.target.value)}
  /&gt;
  &lt;input
    type="text"
    placeholder="New Title"
    value={title}
    onChange={(e) =&gt; setTitle(e.target.value)}
  /&gt;
  &lt;button onClick={handleUpdate}&gt;Update&lt;/button&gt;
  &lt;p&gt;{message}&lt;/p&gt;
&lt;/div&gt;
); } 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 (
&lt;div&gt;
  &lt;h2&gt;Delete Post&lt;/h2&gt;
  &lt;input
    type="text"
    placeholder="Post ID"
    value={postId}
    onChange={(e) =&gt; setPostId(e.target.value)}
  /&gt;
  &lt;button onClick={handleDelete}&gt;Delete&lt;/button&gt;
  &lt;p&gt;{message}&lt;/p&gt;
&lt;/div&gt;
); } 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 () =&gt; {
  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 (
&lt;div&gt;
  &lt;h3&gt;Todo Item:&lt;/h3&gt;
  &lt;p&gt;{data.title}&lt;/p&gt;
&lt;/div&gt;
); } 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 (
&lt;div&gt;
  &lt;h2&gt;Search User&lt;/h2&gt;
  &lt;input
    type="text"
    placeholder="Enter user ID"
    value={userId}
    onChange={(e) =&gt; setUserId(e.target.value)}
  /&gt;
  &lt;button onClick={handleFetch}&gt;Fetch User&lt;/button&gt;
  {user &amp;&amp; (
    &lt;div&gt;
      &lt;h3&gt;{user.name}&lt;/h3&gt;
      &lt;p&gt;Email: {user.email}&lt;/p&gt;
    &lt;/div&gt;
  )}
&lt;/div&gt;
); } 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 () =&gt; {
  const &#91;postsResponse, usersResponse] = await Promise.all(&#91;
    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 (
&lt;div&gt;
  &lt;h2&gt;Posts and Users&lt;/h2&gt;
  &lt;div&gt;
    &lt;h3&gt;Posts&lt;/h3&gt;
    {posts.map((p) =&gt; (
      &lt;p key={p.id}&gt;{p.title}&lt;/p&gt;
    ))}
  &lt;/div&gt;
  &lt;div&gt;
    &lt;h3&gt;Users&lt;/h3&gt;
    {users.map((u) =&gt; (
      &lt;p key={u.id}&gt;{u.name}&lt;/p&gt;
    ))}
  &lt;/div&gt;
&lt;/div&gt;
); } export default MultiFetch;

This improves performance by fetching data simultaneously.


Best Practices

  1. Always handle errors: Network requests can fail for many reasons.
  2. Show loading indicators: Keep users informed.
  3. Use async/await for cleaner code: It’s more readable than chaining .then().
  4. Avoid fetching in loops: Use Promise.all() for multiple requests.
  5. Abort fetches when unmounted: Prevent memory leaks using AbortController.
  6. 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 () =&gt; {
  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 () =&gt; 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 (
&lt;ul&gt;
  {data.map((u) =&gt; (
    &lt;li key={u.id}&gt;{u.name}&lt;/li&gt;
  ))}
&lt;/ul&gt;
); } export default Users;

This reusable hook simplifies data fetching logic across multiple components.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *