🔄 Handling Async State in React
Managing state from asynchronous operations like API calls is critical in React. You’ll typically track three things: loading, data, and error — often called the "async state trio."
⚙️ 1. Basic Pattern
Use three pieces of state: isLoading
, data
, and error
.
import { useState, useEffect } from "react";
function Users() {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
try {
const res = await fetch("https://api.example.com/users");
if (!res.ok) throw new Error("Network response was not ok");
const users = await res.json();
setData(users);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
}
fetchUsers();
}, []);
🔍 2. Displaying Loading / Error / Data
if (isLoading) return <p>Loading...</p>;
if (error) return <p className="text-red-600">Error: {error}</p>;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
✨ 3. Tips & Best Practices
- Initialize
isLoading
astrue
by default - Always wrap async calls in try/catch
- Use
finally
to stop the loading state - Optionally use
AbortController
to cancel fetch on unmount - Debounce or throttle rapid updates (e.g., search input)
🔁 4. Create a Reusable Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
async function fetchData() {
try {
const res = await fetch(url);
if (!res.ok) throw new Error("Error fetching data");
const json = await res.json();
if (!ignore) setData(json);
} catch (err) {
if (!ignore) setError(err.message);
} finally {
if (!ignore) setLoading(false);
}
}
fetchData();
return () => { ignore = true; };
}, [url]);
return { data, loading, error };
}
✅ 5. Summary
- Track
loading
,error
, anddata
separately - Display each state accordingly to improve UX
- Extract logic into reusable hooks for cleaner code