How to Call an Async Function in React
What you’ll build or solve
You’ll call async functions in the two places React expects side effects: effects and event handlers.
When this approach works best
Calling an async function in React works best when you:
Learn React on Mimo
- Fetch data when a component loads.
- Refetch data when props or state change.
- Submit a form and wait for a server response.
- Trigger network calls from clicks or other user actions.
Avoid calling async code during rendering. Rendering must stay synchronous and free of side effects.
Prerequisites
- Basic React knowledge
- Understanding of
useStateanduseEffect - Familiarity with
asyncandawait
Step-by-step instructions
Step 1: Call async functions in useEffect for lifecycle work
Use useEffect for async work that should run after render, like loading data on mount or when dependencies change.
You cannot make the effect callback async. Define an async function inside the effect, then call it.
import {useEffect,useState }from"react";
functionUsers() {
const [users,setUsers]=useState([]);
useEffect(() => {
asyncfunctionfetchUsers() {
constresponse=awaitfetch("/api/users");
constdata=awaitresponse.json();
setUsers(data);
}
fetchUsers();
}, []);
return (
<ul>
{users.map(user => (
<likey={user.id}>{user.name}</li>
))}
</ul>
);
}
What to look for:
- The async function lives inside
useEffect. - The dependency array controls when it runs.
- Add dependencies when the request depends on props or state.
Step 2: Call async functions in event handlers for user actions
Event handlers can be async directly. Use this for clicks, form submissions, and other user actions.
import {useState }from"react";
functionSaveButton() {
const [status,setStatus]=useState("idle");
consthandleClick=async () => {
setStatus("loading");
awaitfetch("/api/save", { method:"POST" });
setStatus("done");
};
return (
<buttononClick={handleClick}>
{status==="loading"?"Saving...":"Save"}
</button>
);
}
What to look for:
- Put
awaitinside the handler, not during render. - Update state after the request finishes.
Examples you can copy
Example 1: Fetch on mount with error handling
useEffect(() => {
asyncfunctionloadProducts() {
try {
constres=awaitfetch("/api/products");
constproducts=awaitres.json();
setProducts(products);
}catch (error) {
setError("Failed to load products");
}
}
loadProducts();
}, []);
Example 2: Fetch when a prop changes
useEffect(() => {
asyncfunctionloadUser() {
constres=awaitfetch(`/api/users/${userId}`);
constuser=awaitres.json();
setUser(user);
}
loadUser();
}, [userId]);
Example 3: Submit a form with async and await
consthandleSubmit=async (event) => {
event.preventDefault();
try {
awaitfetch("/api/submit", {
method:"POST",
headers: {"Content-Type":"application/json" },
body:JSON.stringify(formData)
});
setSubmitted(true);
}catch (error) {
setError("Submit failed");
}
};
Example 4: Run multiple requests in parallel
useEffect(() => {
asyncfunctionloadAll() {
const [usersRes,postsRes]=awaitPromise.all([
fetch("/api/users"),
fetch("/api/posts")
]);
constusers=awaitusersRes.json();
constposts=awaitpostsRes.json();
setUsers(users);
setPosts(posts);
}
loadAll();
}, []);
Common mistakes and how to fix them
Mistake 1: Making useEffect directly async
What you might do:
useEffect(async () => {
constres=awaitfetch("/api/data");
constdata=awaitres.json();
setData(data);
}, []);
Why it breaks:
React expects the effect callback to return nothing or a cleanup function, not a Promise.
Correct approach:
useEffect(() => {
asyncfunctionload() {
constres=awaitfetch("/api/data");
constdata=awaitres.json();
setData(data);
}
load();
}, []);
Mistake 2: Calling async code during render
What you might do:
constres=awaitfetch("/api/data");
Why it breaks:
Components must render synchronously. Async work belongs in effects or event handlers.
Correct approach:
Move the async call into useEffect or an event handler.
Mistake 3: Missing the dependency array
What you might do:
useEffect(() => {
asyncfunctionload() {
constres=awaitfetch("/api/data");
setData(awaitres.json());
}
load();
});
Why it breaks:
Without dependencies, the effect runs after every render.
Correct approach:
useEffect(() => {
asyncfunctionload() {
constres=awaitfetch("/api/data");
setData(awaitres.json());
}
load();
}, []);
Troubleshooting
- If an effect runs repeatedly, check the dependency array.
- If requests fail silently, wrap the async code in
try/catchand store the error in state. - If you see state updates after navigating away, guard updates in the effect with a cleanup flag.
- If data is
undefined, logresponse.statusand confirm the endpoint returns JSON.
Quick recap
- Call async functions in
useEffectfor lifecycle work. - Call async functions in event handlers for user actions.
- Do not call async code during render.
- Use dependencies to control when effects run.
- Add
try/catchand cleanup guards when needed.
Join 35M+ people learning for free on Mimo
4.8 out of 5 across 1M+ reviews
Check us out on Apple AppStore, Google Play Store, and Trustpilot