Paramclasses Academy - Blog

Tutorials Details

←
Post 14 of 29
→

How to Fetching Data from APIs Using fetch(), Promises, and async/await in React.js ?

How to Fetching Data from APIs Using fetch(), Promises, and async/await in React.js ?

October 29, 2026

1. Introduction

Modern web applications frequently need data from external servers. For example:

  • An e-commerce website retrieves products and prices.

  • A weather application retrieves current weather information.

  • A news website retrieves articles from an API.

  • An admin dashboard retrieves users, orders, and reports.

An API (Application Programming Interface) provides a way for different software systems to communicate. A web API commonly exposes endpoints that a frontend application can request over HTTP.

React applications can use JavaScript's built-in fetch() function to send HTTP requests and process the responses.

In this tutorial, we will learn how to request API data, convert JSON responses into JavaScript values, manage asynchronous operations, and display the results in React components.

2. Learning Objectives

By the end of this tutorial, you will be able to:

  1. Understand APIs, endpoints, HTTP requests, and responses.

  2. Use fetch() to request data from a server.

  3. Understand Promises and the .then() method.

  4. Use async/await for asynchronous operations.

  5. Convert JSON responses into JavaScript objects.

  6. Display API data using React components.

  7. Handle loading, error, and empty states.

  8. Fetch data when a component loads or a dependency changes.

  9. Avoid common API integration mistakes.

3. Core Concepts and Explanation

3.1 What Is an API?

An API allows one software application to access functionality or data provided by another system.

For example, a React application might request product information from this endpoint:

https://api.example.com/products

The server may return a JSON response containing product details.

Example JSON:

{
  "id": 1,
  "title": "Wireless Headphones",
  "price": 2499
}

JSON stands for JavaScript Object Notation. It is a widely used format for exchanging structured data between applications.

3.2 What Is an API Endpoint?

An endpoint is a specific URL through which an API exposes a resource or operation.

Examples:

GET /products
GET /products/1
GET /users
GET /users/1

The actual URLs depend on the API provider.

Common HTTP methods include:

  • GET — retrieve data.

  • POST — submit or create data.

  • PUT — replace a resource.

  • PATCH — update part of a resource.

  • DELETE — delete a resource.

In this tutorial, our main focus is retrieving data with GET.

3.3 What Is fetch()?

fetch() is a built-in JavaScript function that sends HTTP requests and returns a Promise.

Basic syntax:

fetch(url)
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

Here is what happens:

  1. fetch(url) sends a request.

  2. The returned Promise resolves to a Response object when a response is received.

  3. response.json() reads the response body and parses its JSON content.

  4. The parsed data becomes available in the next .then().

  5. .catch() handles rejected Promises, such as certain network failures.

Important: fetch() generally does not reject just because the server returns an HTTP error such as 404 Not Found or 500 Internal Server Error. Check response.ok to detect unsuccessful HTTP status codes.

3.4 What Is a Promise?

A Promise represents the eventual result of an asynchronous operation.

A Promise has three states:

  • Pending: The operation has not completed.

  • Fulfilled: The operation completed successfully.

  • Rejected: The operation failed.

Example:

const promise = fetch(
  "https://jsonplaceholder.typicode.com/posts"
);

promise
  .then(response => {
    console.log("Response received");
  })
  .catch(error => {
    console.error("Request failed:", error);
  });

The request can take time to complete. Promises allow JavaScript to continue processing other work while waiting for the response.

3.5 Understanding async and await

async and await provide a readable way to work with Promises.

  • async declares an asynchronous function that returns a Promise.

  • await pauses that function until a Promise settles, then provides its fulfilled value or throws its rejection.

Example:

async function getPosts() {
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/posts"
  );

  if (!response.ok) {
    throw new Error("Failed to fetch posts");
  }

  const data = await response.json();

  console.log(data);
}

getPosts().catch(error => {
  console.error(error);
});

This approach is often easier to read when a function contains several asynchronous steps.

3.6 What Is JSON Parsing?

The response body from an API is not automatically a JavaScript object. When an endpoint returns JSON, call:

const data = await response.json();

This reads the response body and parses the JSON.

Remember:

  • fetch() sends the request.

  • response represents the HTTP response.

  • response.json() parses the JSON body.

  • data contains the parsed JavaScript value.

A response body can generally only be consumed once, so avoid calling response.json() repeatedly on the same response.

4. Practical Code Examples

Example 1: Fetch Data Using Promises

First, create a React project if you do not already have one:

npm create vite@latest react-api-app -- --template react
cd react-api-app
npm install
npm run dev

Open src/App.jsx and replace its contents with:

import { useEffect, useState } from "react";

function App() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then(response => {
        if (!response.ok) {
          throw new Error("Failed to fetch posts");
        }

        return response.json();
      })
      .then(data => {
        setPosts(data.slice(0, 10));
      })
      .catch(error => {
        console.error("API Error:", error);
      });
  }, []);

  return (
    <div>
      <h1>Posts</h1>

      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

export default App;

Explanation:

  1. useState stores the posts returned by the API.

  2. useEffect starts the request after the component's initial commit.

  3. fetch() requests the posts endpoint.

  4. response.ok checks whether the HTTP response indicates success.

  5. response.json() parses the response.

  6. setPosts() saves the first ten posts in state.

  7. map() renders each post, using its id as the React key.

This example demonstrates the Promise approach. It logs errors to the console but does not yet show loading and error messages in the UI.

Example 2: Fetch Data Using async/await

Now replace src/App.jsx with this version:

import { useEffect, useState } from "react";

function App() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    async function fetchPosts() {
      try {
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/posts"
        );

        if (!response.ok) {
          throw new Error("Failed to fetch posts");
        }

        const data = await response.json();

        setPosts(data.slice(0, 10));
      } catch (error) {
        console.error("API Error:", error);
      }
    }

    fetchPosts();
  }, []);

  return (
    <div>
      <h1>Posts Using async/await</h1>

      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

export default App;

Explanation:

  • The inner fetchPosts() function is declared as async.

  • await fetch() waits for the HTTP response.

  • await response.json() waits for the body to be parsed.

  • try/catch handles errors thrown within the asynchronous function.

Do not make the useEffect callback itself async. React expects that callback to return either a cleanup function or nothing, not a Promise.

Example 3: Handle Loading, Error, and Empty States

A professional application should tell users what is happening while a request is in progress or if it fails.

import { useEffect, useState } from "react";

function App() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    let ignore = false;

    async function fetchPosts() {
      setLoading(true);
      setError("");

      try {
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/posts"
        );

        if (!response.ok) {
          throw new Error(
            `Request failed: ${response.status}`
          );
        }

        const data = await response.json();

        if (!ignore) {
          setPosts(data.slice(0, 10));
        }
      } catch (err) {
        if (!ignore) {
          setError(err.message);
        }
      } finally {
        if (!ignore) {
          setLoading(false);
        }
      }
    }

    fetchPosts();

    return () => {
      ignore = true;
    };
  }, []);

  if (loading) {
    return <h2>Loading posts...</h2>;
  }

  if (error) {
    return <p role="alert">Error: {error}</p>;
  }

  if (posts.length === 0) {
    return <p>No posts available.</p>;
  }

  return (
    <div>
      <h1>Latest Posts</h1>

      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

export default App;

What makes this example better?

  • It displays a loading message while the request is running.

  • It checks the HTTP status.

  • It displays a useful error message if the request fails.

  • It displays an empty-state message if the returned list has no posts.

  • Its cleanup flag prevents an outdated request from updating state after cleanup.

The flag does not cancel the underlying network request. For actual cancellation, use AbortController.

Example 4: Fetch a Specific User

Sometimes an application needs one resource rather than a full list.

import { useEffect, useState } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    const controller = new AbortController();

    async function fetchUser() {
      setLoading(true);
      setError("");
      setUser(null);

      try {
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}`,
          { signal: controller.signal }
        );

        if (!response.ok) {
          throw new Error("Unable to load user");
        }

        const data = await response.json();
        setUser(data);
      } catch (err) {
        if (err.name !== "AbortError") {
          setError(err.message);
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    return () => {
      controller.abort();
    };
  }, [userId]);

  if (loading) return <p>Loading user...</p>;
  if (error) return <p role="alert">{error}</p>;
  if (!user) return <p>User not found.</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>City: {user.address.city}</p>
    </div>
  );
}

export default UserProfile;

To test the component, render it with a userId:

<UserProfile userId={1} />

Change the ID to 2 or 3 to request another user.

Important concepts:

  • The URL changes based on userId.

  • userId is included in the dependency array.

  • Cleanup aborts the previous request when the effect is cleaned up.

  • The AbortError is ignored because it is expected during cancellation.

Example 5: Build a Product List from API Data

import { useEffect, useState } from "react";

function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    const controller = new AbortController();

    async function loadProducts() {
      try {
        const response = await fetch(
          "https://fakestoreapi.com/products",
          { signal: controller.signal }
        );

        if (!response.ok) {
          throw new Error("Could not load products");
        }

        const data = await response.json();
        setProducts(data);
      } catch (err) {
        if (err.name !== "AbortError") {
          setError(err.message);
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    }

    loadProducts();

    return () => {
      controller.abort();
    };
  }, []);

  if (loading) return <p>Loading products...</p>;
  if (error) return <p role="alert">{error}</p>;
  if (products.length === 0) return <p>No products found.</p>;

  return (
    <div>
      <h1>Product Catalog</h1>

      {products.map(product => (
        <article key={product.id}>
          <h2>{product.title}</h2>
          <p>Price: ${product.price}</p>
          <p>{product.category}</p>
        </article>
      ))}
    </div>
  );
}

export default ProductList;

This example uses a public demonstration API. Its availability and returned data are controlled by the API provider, so it should not be treated as a production commerce backend.

5. Real-World Applications

E-commerce Application

Fetch products, categories, prices, and product details from a backend API. Use loading indicators, pagination, and error messages to improve the shopping experience.

Weather Application

Send a request using the selected city or coordinates, parse the weather response, and display temperature, humidity, and forecast details.

Admin Dashboard

Retrieve user accounts, order summaries, sales metrics, and reports from authenticated endpoints. Sensitive endpoints should enforce authorization on the server.

News Application

Fetch article lists from an API, render them with map(), and allow users to navigate to individual articles.

Authentication System

Submit credentials to an authentication endpoint using an appropriate HTTP method. Handle the server response securely, and never assume that hiding a frontend route protects the underlying data.

6. Common Mistakes and Best Practices

Mistake 1: Forgetting to Parse JSON

Incorrect:

const response = await fetch(url);
console.log(response);

This logs the Response object rather than the parsed JSON data.

Correct:

const response = await fetch(url);

if (!response.ok) {
  throw new Error("Request failed");
}

const data = await response.json();
console.log(data);

Mistake 2: Not Checking response.ok

A 404 response does not automatically cause fetch() to reject.

Check the response status:

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

Mistake 3: Forgetting the Dependency Array

If an effect depends on userId, include it in the dependency array. Otherwise, the component may show stale data when the ID changes.

Mistake 4: Updating State Without Handling Errors

Requests can fail because of network problems, unavailable servers, or unsuccessful HTTP responses.

Use try/catch/finally or a suitable Promise chain to handle failures and loading state.

Mistake 5: Ignoring Cleanup and Outdated Requests

If a component unmounts or the request inputs change, an old request may finish later. Use AbortController or an appropriate stale-result guard when necessary.

Mistake 6: Using API Secrets in Frontend Code

Environment variables exposed to a browser bundle are not secret. Never put private API keys, database credentials, or server secrets in client-side React code.

Best Practices Checklist

  • Check HTTP status codes.

  • Parse response bodies correctly.

  • Handle loading, error, success, and empty states.

  • Include required effect dependencies.

  • Cancel requests when appropriate.

  • Use stable React keys when rendering lists.

  • Avoid duplicate requests and unnecessary effects.

  • Keep private credentials on the server.

  • Consider a data-fetching library for caching, retries, and complex server-state management.

7. Practice Exercises

Exercise 1: Fetch Posts

Retrieve posts from:

https://jsonplaceholder.typicode.com/posts

Display the first ten post titles.

Exercise 2: Fetch One User

Retrieve a user from:

https://jsonplaceholder.typicode.com/users/1

Display their name, email, and city.

Exercise 3: Loading and Error States

Add a loading message and an error message to your posts application. Verify that the UI handles unsuccessful requests.

Exercise 4: Product Catalog

Fetch products from:

https://fakestoreapi.com/products

Display each product's title, price, and category.

Exercise 5: Compare Promises and async/await

Write the same basic API request using both .then() and async/await. Explain which syntax you find easier to read and why.

Exercise 6: Dynamic User ID

Create a component that accepts a userId prop. Fetch the appropriate user whenever the ID changes, and cancel outdated requests during cleanup.

8. Key Takeaways and Conclusion

In this tutorial, you learned:

  • APIs provide a way for frontend applications to communicate with servers.

  • fetch() sends HTTP requests and returns a Promise.

  • Promises can be handled with .then() and .catch().

  • async/await provides readable syntax for asynchronous code.

  • response.json() parses JSON response bodies.

  • response.ok helps detect unsuccessful HTTP status codes.

  • React state can store API data and render it dynamically.

  • Loading, error, and empty states improve the user experience.

  • Cleanup and request cancellation help prevent outdated work from affecting the UI.

Final takeaway: A reliable API integration requires more than fetching data. You must check the response, parse the body, manage asynchronous state, handle errors, and clean up requests when necessary.

←
Post 14 of 29
→