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.

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.

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.

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:
- How to Destructure Props in React Functional Components
- How to Use Lazy Loading Component in React
- React Force Reload Component
- How to Use React Class Component Ref

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.