In my experience of building React applications, one of the most common hurdles I see developers face is handling asynchronous data.
It sounds simple enough: you want to fetch some data and show it on the screen. But if you’ve ever tried to put an await directly inside your functional component, you know things get messy fast.
In this tutorial, I will show you exactly how to handle await in React components using the best practices I’ve picked up over the years.
The Problem with Async in React Components
When you are working with React, you have to remember that functional components are just functions that run every time a render happens.
If you mark your component function as async, it will return a Promise instead of JSX. React doesn’t know how to render a Promise.
I remember the first time I tried to fetch a list of California state parks and just threw an await in the body of the function. The whole app crashed because I broke the fundamental rules of hooks.
To use await, we need to wrap it in a way that plays nice with the React lifecycle.
Method 1: Use Await inside useEffect
The most traditional way to use await is inside the useEffect hook. This is the “bread and butter” approach for most React developers.
Since the effect callback itself cannot be an async function, we define the async function inside the effect and then call it immediately.
Suppose we are building a dashboard for a logistics company based in Chicago. We need to fetch the current shipping rates for different zones.
Here is the full code for this approach:
import React, { useState, useEffect } from 'react';
const ShippingDashboard = () => {
const [rates, setRates] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// We define an async function inside the effect
const fetchShippingRates = async () => {
try {
setLoading(true);
// Simulating an API call to a US Logistics Provider
const response = await fetch('https://api.us-logistics.example/v1/rates/midwest');
if (!response.ok) {
throw new Error('Failed to fetch shipping data');
}
const data = await response.json();
setRates(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchShippingRates();
}, []); // Empty dependency array means this runs once on mount
if (loading) return <p>Loading Midwest shipping rates...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
<h1>Chicago Logistics Hub: Current Rates</h1>
<table border="1" cellPadding="10">
<thead>
<tr>
<th>Destination State</th>
<th>Rate per Lb ($)</th>
<th>Estimated Delivery</th>
</tr>
</thead>
<tbody>
{rates.map((item) => (
<tr key={item.id}>
<td>{item.state}</td>
<td>{item.rate}</td>
<td>{item.days} Business Days</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default ShippingDashboard;I executed the above example code and added the screenshot below.

In this example, I used a standard try-catch-finally block. This ensures that even if the API goes down, the user isn’t left staring at a blank screen.
Method 2: Handle Await with Custom Hooks
As I moved into more complex projects, I realized that writing the same useEffect logic over and over was a waste of time.
I prefer creating a custom hook. This separates the “how” of fetching data from the “what” of the UI.
Let’s imagine we are building an app for a real estate agency in New York City. We want to fetch the latest property listings in Manhattan.
Here is how you can create a reusable useFetch hook to handle the await logic:
import { useState, useEffect } from 'react';
// Custom Hook: useApi
const useApi = (url) => {
const [data, setData] = useState(null);
const [isPending, setIsPending] = 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("Could not fetch the data for that resource");
}
const result = await response.json();
setIsPending(false);
setData(result);
setError(null);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setIsPending(false);
setError(err.message);
}
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, isPending, error };
};
// Component using the hook
const ManhattanListings = () => {
const { data: listings, isPending, error } = useApi('https://api.nyc-realty.example/listings/manhattan');
return (
<div className="listings-container">
<h2>Latest Manhattan Property Listings</h2>
{error && <div>{error}</div>}
{isPending && <div>Loading NYC Properties...</div>}
{listings && listings.map(listing => (
<div key={listing.id} style={{ borderBottom: '1px solid #ccc', margin: '10px 0' }}>
<h3>{listing.address}</h3>
<p>Price: ${listing.price.toLocaleString()}</p>
<p>Neighborhood: {listing.neighborhood}</p>
</div>
))}
</div>
);
};
export default ManhattanListings;I executed the above example code and added the screenshot below.

I added an AbortController here. This is a pro tip: it prevents memory leaks if the user navigates away before the await finishes.
Method 3: The Modern Way with the ‘use’ Hook (React 18+)
If you are working with the latest versions of React, you can now use the use hook. This is a game-changer.
Unlike useEffect, the use hook can be called inside loops or conditional statements in some cases, and it works directly with Promises.
Let’s look at an example for a Texas-based weather station application.
import React, { Suspense, use } from 'react';
// This function returns a promise
const fetchTexasWeather = async () => {
const res = await fetch('https://api.weather.gov/gridpoints/FWD/88,91/forecast');
return res.json();
};
// Global promise (or passed via props/context)
const weatherPromise = fetchTexasWeather();
const WeatherDisplay = ({ promise }) => {
// 'use' will read the value from the promise when it resolves
const weatherData = use(promise);
return (
<div>
<h3>Current Forecast for Dallas/Fort Worth</h3>
<p>{weatherData.properties.periods[0].detailedForecast}</p>
</div>
);
};
const TexasWeatherApp = () => {
return (
<div style={{ padding: '20px', backgroundColor: '#f0f8ff' }}>
<h1>Texas State Weather Portal</h1>
<Suspense fallback={<p>Consulting the weather satellites...</p>}>
<WeatherDisplay promise={weatherPromise} />
</Suspense>
</div>
);
};
export default TexasWeatherApp;This approach is much cleaner because you don’t need to manually manage loading and error states inside the component. Suspense handles the loading for you.
Handle Multiple Awaits (Parallel vs. Sequential)
Sometimes you need to fetch data from multiple sources. In my experience, doing this incorrectly can slow down your app significantly.
If you are fetching data for a user’s profile in a Seattle tech company, you might need their employee details and their recent project history.
Don’t await them one by one (Sequential). Use Promise.all (Parallel).
import React, { useState, useEffect } from 'react';
const SeattleEmployeePortal = ({ employeeId }) => {
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const getFullProfile = async () => {
try {
setLoading(true);
// Parallel fetching
const [empRes, projectRes] = await Promise.all([
fetch(`https://api.seattle-tech.example/employees/${employeeId}`),
fetch(`https://api.seattle-tech.example/employees/${employeeId}/projects`)
]);
const empData = await empRes.json();
const projectData = await projectRes.json();
setProfile({ ...empData, projects: projectData });
} catch (error) {
console.error("Data retrieval error", error);
} finally {
setLoading(false);
}
};
getFullProfile();
}, [employeeId]);
if (loading) return <p>Loading Seattle HQ Records...</p>;
return (
<div>
<h1>Employee: {profile.name}</h1>
<p>Role: {profile.position}</p>
<h2>Active Projects in Washington State:</h2>
<ul>
{profile.projects.map(p => <li key={p.id}>{p.projectName}</li>)}
</ul>
</div>
);
};Using Promise.all saved my team seconds of load time on our high-traffic dashboards.
Common Mistakes to Avoid
In the years I’ve spent reviewing PRs, I see the same three mistakes when using await in React:
- Forgetting the Dependency Array: If you use await inside a useEffect and forget the [], your API will be hit on every single keystroke or state change.
- Not Handling Unmounting: If the component unmounts before the await finishes, you might get a warning about state updates on unmounted components.
- Directly making the component async: As mentioned, async function MyComponent() will simply not work in React and will throw a console error.
I hope this tutorial helped you understand how to manage asynchronous operations in your React apps.
By using these patterns, you can ensure your applications are performant and easy to maintain.
Whether you are building a small tool or a massive enterprise platform for a US-based firm, handling await correctly is a vital skill.
You may also like to read:
- How to Use React Arrow Function Components
- Custom Component Mapping in React Markdown
- Mantine React Component Library
- React Toggle Switch Component

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.