React
Chapter 08: APIs & Async React — The 3-state fetching pattern (Loading, Error, Success), AbortController race condition prevention, and CRUD mutations.
1. The 3-State Data Fetching Pattern
Every asynchronous operation in modern React web applications revolves around 3 fundamental states: Loading ($\rightarrow$ Spinner), Error ($\rightarrow$ Alert), and Success ($\rightarrow$ Data UI).
Production Data Fetching Implementation
function UserDirectory() {
const [users, setUsers] = React.useState([]);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
let isCancelled = false; // Guard against setting state if unmounted
async function fetchUsers() {
try {
setIsLoading(true);
setError(null);
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`HTTP Error! Status: ${response.status}`);
}
const data = await response.json();
if (!isCancelled) {
setUsers(data);
}
} catch (err) {
if (!isCancelled) {
setError(err.message || "Failed to load directory.");
}
} finally {
if (!isCancelled) {
setIsLoading(false);
}
}
}
fetchUsers();
return () => {
isCancelled = true; // Cleanup flag
};
}, []); // Runs once on mount
if (isLoading) return <div className="loading-spinner">Loading Users...</div>;
if (error) return <div className="error-banner">⚠️ {error}</div>;
return (
<ul className="users-list">
{users.map(u => (
<li key={u.id}>{u.name} — <small>{u.email}</small></li>
))}
</ul>
);
}
2. Handling Race Conditions with AbortController
A Race Condition happens when a user rapidly changes search filters (e.g. searching "A" then "AB"). The network request for "A" might finish after "AB", overwriting the fresh results with stale data!
⚠️ The AbortController Solution
Use browser native AbortController in the useEffect cleanup function to cancel in-flight HTTP requests whenever dependencies change:
useEffect(() => {
const controller = new AbortController(); // 1. Create controller
async function searchAPI() {
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: controller.signal // 2. Attach signal to fetch
});
const results = await res.json();
setData(results);
} catch (err) {
if (err.name !== "AbortError") {
setError(err.message); // Ignore intentional abort cancellations!
}
}
}
if (query) searchAPI();
return () => {
controller.abort(); // 3. Cancel network request if query changes!
};
}, [query]);
3. Hands-On Practice Challenges
Master async data transformations, HTTP status parsing, and optimistic updates. Click "Solve Challenge" to open the code editor!
Challenge 1: Async State Machine Reducer
Write an asyncStateReducer(state, action) function that manages the 3 states: FETCH_START, FETCH_SUCCESS, and FETCH_ERROR immutably.
FETCH_START:isLoading: true, error: null, data: null.FETCH_SUCCESS:isLoading: false, data: action.payload, error: null.FETCH_ERROR:isLoading: false, error: action.payload, data: null.
Loading state: { data: null, isLoading: true, error: null } | Success state: { data: [...], isLoading: false, error: null }
Challenge 2: HTTP Error Status Parser
Implement formatApiError(errorObj) that maps HTTP status codes (404, 401, 500+) or Error instances into clear, user-friendly alert messages.
- Handle status 404: "Resource not found (404).".
- Handle status 401: "Unauthorized. Please log in.".
- Handle status $\ge 500$: "Internal server error. Try again later.".
- Handle generic error instances with
errorObj.message.
Resource not found (404). | Internal server error. Try again later.
formatApiError to format HTTP status codes into friendly messages.
Challenge 3: Optimistic UI Mutation Simulator
Write a simulation applyOptimisticUpdate(itemsList, newItem, simulateFailure) that updates the list immediately, and rolls back to the original list if simulateFailure is true.
- Optimistically construct new array with appended
newItem. - If
simulateFailureis true, return original unmodifieditemsList. - Otherwise return the updated optimistic array.
Good result length: 2 | Fail result length: 1 (reverted)