
Day 17 – Handling Async useEffect, Loading and Error States in React
In the last blog, we learned how to fetch data with fetch
and axios
. But in real projects, things are never that simple.
When making an API call, we often face three main challenges:
- Loading State – We must show a spinner or “Loading…” text while waiting for the data.
- Error State – If something fails (e.g., server down, wrong URL), we must show an error message.
- Cleanup – If the user leaves the page before the API responds, React should not try to update state on an unmounted component.
This blog will teach you how to solve all three using Async useEffect in React.
Why Not Write async
Directly in useEffect?
A common beginner mistake:
useEffect(async () => {
const res = await fetch("https://api.example.com/data");
}, []);
❌ This is not correct.
React expects useEffect
to either return nothing or a cleanup function. An async
function returns a Promise instead, which confuses React.
✅ The correct way is:
useEffect(() => {
const fetchData = async () => {
// async code here
};
fetchData();
}, []);
Example 1 – Basic Async useEffect with Loading & Error
import React, { useEffect, useState } from "react";
function UsersList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
let res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) throw new Error("Failed to fetch users");
let data = await res.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false); // stop loading in both success/fail
}
};
fetchUsers();
}, []);
if (loading) return <p>Loading users...</p>;
if (error) return <p style={{ color: "red" }}>Error: {error}</p>;
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name} – {u.email}</li>
))}
</ul>
);
}
export default UsersList;
Step-by-Step Explanation
loading
starts astrue
.- While fetching → we show
Loading users...
. - If successful → data is stored in
users
state. - If failed → error message is stored in
error
. finally
ensures thatloading
becomesfalse
no matter what.
👉 This pattern (loading → success/error → stop loading
) is the standard data fetching cycle in React.
Example 2 – Cleaning up Async Requests
Imagine:
- A user visits the page.
- API call is still running.
- User navigates away before response arrives.
Now React will try to call setState
on an unmounted component, causing this warning:
Warning: Can't perform a React state update on an unmounted component.
Solution: Track Mounting State
useEffect(() => {
let isMounted = true; // flag
const fetchData = async () => {
try {
let res = await fetch("https://jsonplaceholder.typicode.com/posts");
let data = await res.json();
if (isMounted) setUsers(data); // only update if mounted
} catch (err) {
if (isMounted) setError(err.message);
} finally {
if (isMounted) setLoading(false);
}
};
fetchData();
return () => {
isMounted = false; // cleanup on unmount
};
}, []);
👉 Now even if the user leaves the page, React won’t try to update state after unmount.
Example 3 – Axios with Request Cancellation
Unlike fetch
, Axios supports request cancellation natively.
import axios from "axios";
import { useEffect, useState } from "react";
function Products() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelToken = axios.CancelToken.source();
const fetchProducts = async () => {
try {
let res = await axios.get("https://fakestoreapi.com/products", {
cancelToken: cancelToken.token,
});
setProducts(res.data);
} catch (err) {
if (axios.isCancel(err)) {
console.log("Request canceled:", err.message);
} else {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchProducts();
return () => cancelToken.cancel("Request canceled on unmount");
}, []);
if (loading) return <p>Loading products...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
👉 This ensures no wasted requests if the component unmounts.
Visual Flow
Initial Render → Loading = true
|
v
Fetch Data (async)
| Success | Failure
v v
Update state Set error
| |
v v
Loading = false Loading = false
Best Practices
✅ Always handle loading and error states.
✅ Never use async
directly in useEffect
.
✅ Use cleanup (return () => {...}
) to prevent memory leaks.
✅ For complex apps → prefer React Query or SWR.
External Links
Conclusion
Handling async useEffect in React is not just about fetching data — it’s about managing the whole lifecycle:
- Show a loading indicator.
- Handle errors gracefully.
- Perform cleanup to avoid bugs.
👉 Next Blog (Day 18): React Router Basics – Routes, Links & Navigation