React Async Functional Component

I’ve often found that fetching data is where most developers get stuck. It feels easy until you realize that React components don’t naturally “wait” for data to arrive.

Over the years, I’ve moved from messy class-based fetch calls to clean, asynchronous functional components.

In this guide, I’ll show you exactly how I handle async operations in React without breaking the UI.

Can a React Functional Component be Async?

Before we get into the code, let’s address the elephant in the room: You cannot make a functional component itself an async function.

If you try to write an async function MyComponent(), React will throw an error because it expects a component to return a JSX element, not a Promise.

Instead, we use hooks like useEffect or React’s newer Suspense architecture to handle the “waiting” part for us.

Method 1: Use useEffect with Async/Await (The Standard Way)

This is my go-to method for most professional projects, especially when dealing with REST APIs.

In this example, let’s imagine we are building a dashboard for a New York City Real Estate firm to fetch current property listings.

The Real-World Example: NYC Property Finder

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

// A mock API call simulating a database fetch for Manhattan listings
const fetchManhattanProperties = async () => {
  const response = await fetch('https://api.example.com/v1/nyc/listings');
  if (!response.ok) {
    throw new Error('Failed to fetch property data');
  }
  return response.json();
};

const PropertyList = () => {
  const [properties, setProperties] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // We define the async function inside the effect
    const getProperties = async () => {
      try {
        setLoading(true);
        const data = await fetchManhattanProperties();
        setProperties(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    getProperties();
  }, []); // Empty array ensures this runs once on mount

  if (loading) return <p>Loading premium Manhattan listings...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div style={{ padding: '20px', fontFamily: 'Arial' }}>
      <h1>Available Manhattan Properties</h1>
      <ul style={{ listStyleType: 'none' }}>
        {properties.map((property) => (
          <li key={property.id} style={{ borderBottom: '1px solid #ccc', padding: '10px 0' }}>
            <strong>{property.address}</strong> - ${property.price.toLocaleString('en-US')}
            <br />
            <small>Neighborhood: {property.neighborhood}</small>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default PropertyList;

You can refer to the screenshot below to see the output.

React Async Functional Component

I prefer this approach because it gives me full control over the “Loading” and “Error” states.

It ensures the user isn’t staring at a blank screen while the server in North Virginia processes the request.

Method 2: Custom Hooks for Cleaner Async Logic

When I’m working on large-scale applications, I hate repeating the same useState and useEffect logic in every file.

I usually extract the async logic into a custom hook to keep my components “thin” and readable.

Example: Track FedEx Shipments in a Logistics App

Let’s create a hook that simulates tracking a package across the United States.

import { useState, useEffect } from 'react';

// Custom Hook: useShipmentTracker
const useShipmentTracker = (trackingId) => {
  const [status, setStatus] = useState(null);
  const [isPending, setIsPending] = useState(true);

  useEffect(() => {
    const fetchStatus = async () => {
      setIsPending(true);
      // Simulating a delay from a logistics API like FedEx or UPS
      const mockFetch = new Promise((resolve) => {
        setTimeout(() => {
          resolve({
            id: trackingId,
            location: 'Memphis, TN Hub',
            status: 'In Transit',
            estimatedDelivery: 'October 24, 2026'
          });
        }, 2000);
      });

      const result = await mockFetch;
      setStatus(result);
      setIsPending(false);
    };

    if (trackingId) {
      fetchStatus();
    }
  }, [trackingId]);

  return { status, isPending };
};

// Component using the hook
const ShippingDashboard = () => {
  const { status, isPending } = useShipmentTracker('7845-NYC-9921');

  if (isPending) return <div>Scanning barcode at Memphis Hub...</div>;

  return (
    <div style={{ background: '#f4f4f4', padding: '15px', borderRadius: '8px' }}>
      <h3>Logistics Tracking (USA)</h3>
      <p><strong>Current Location:</strong> {status.location}</p>
      <p><strong>Status:</strong> {status.status}</p>
      <p><strong>ETA:</strong> {status.estimatedDelivery}</p>
    </div>
  );
};

You can refer to the screenshot below to see the output.

Async React Functional Component

Using custom hooks makes your code look much more professional and makes it easier for a team to maintain.

It also makes testing much simpler, as you can test the logic independently of the UI.

Method 3: Handle Async with React Suspense

Recently, I’ve started using React Suspense for a more declarative way of handling async data.

Suspense allows you to “suspend” rendering until a certain condition (like data loading) is met.

Example: Seattle Tech Job Board

In this scenario, we wrap the component in a Suspense boundary and provide a fallback UI.

import React, { Suspense } from 'react';

// This is a simplified pattern for a resource-friendly fetcher
const createResource = (promise) => {
  let status = 'pending';
  let result;
  let suspender = promise.then(
    (r) => {
      status = 'success';
      result = r;
    },
    (e) => {
      status = 'error';
      result = e;
    }
  );
  return {
    read() {
      if (status === 'pending') throw suspender;
      if (status === 'error') throw result;
      return result;
    },
  };
};

const jobData = createResource(
  new Promise((resolve) =>
    setTimeout(() => resolve(['Senior Cloud Architect - Seattle', 'React Dev - Bellevue']), 1500)
  )
);

const JobList = () => {
  const jobs = jobData.read();
  return (
    <ul>
      {jobs.map((job, index) => <li key={index}>{job}</li>)}
    </ul>
  );
};

const JobBoardApp = () => (
  <div>
    <h2>Pacific Northwest Tech Jobs</h2>
    <Suspense fallback={<div>Loading jobs from Seattle servers...</div>}>
      <JobList />
    </Suspense>
  </div>
);

You can refer to the screenshot below to see the output.

Async Functional Component in React

While Suspense is powerful, I usually recommend it for frameworks like Next.js or libraries like Relay and SWR.

It feels very “clean” because the component doesn’t have to manage its own loading states.

Important Tips for Async Components

In my experience, there are a few “gotchas” that can cause memory leaks or performance issues.

  • Cleanup: Always check if the component is still mounted before updating the state if you aren’t using modern frameworks.
  • Race Conditions: If a user clicks a button twice, make sure the second request doesn’t overwrite the first incorrectly.
  • Error Boundaries: Always wrap your async components in an Error Boundary to prevent the whole app from crashing if an API goes down.

Handling asynchronous data in functional components is a skill that separates junior developers from seniors.

I’ve found that mastering these three methods allows me to tackle almost any UI requirement.

Whether you prefer the control of useEffect or the elegance of Suspense, the key is consistency.

I hope this tutorial helps you build faster, more reliable React applications.

You may also like to read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.