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).

Async Component State Machine
1. Loading State isLoading: true Render: <Spinner /> ✓ 200 OK 2. Success State data: [...] | error: null ➔ Render <DataList /> ✕ Catch / Err 3. Error State error: "404 Not Found" ➔ Render <Alert />

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>
    );
}
Rendered Async Directory Output:
👥 User Directory 3 Users Loaded
LG
Leanne Graham
Sincere@april.biz
EH
Ervin Howell
Shanna@melissa.tv
CB
Clementine Bauch
Nathan@yesenia.net

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]);
Network Console Output:
[Request] GET /api/search?q=Re -> IN-FLIGHT [Query Changed] Query updated to 'React' [AbortController] 🛑 Cancelled fetch('/api/search?q=Re') [Request] GET /api/search?q=React -> 200 OK (Clean freshest response rendered!)

3. Hands-On Practice Challenges

Master async data transformations, HTTP status parsing, and optimistic updates. Click "Solve Challenge" to open the code editor!

🎯 Hands-on Coding Challenge Async State Machine

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.

📋 Requirements & What to Write:
  • 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.
Expected Result: Loading state: { data: null, isLoading: true, error: null } | Success state: { data: [...], isLoading: false, error: null }
💡
Editor Instructions: Complete the reducer switch-case branches for async actions.
asyncReducer.js
Console / Terminal Output
// Output will appear here when you click "Run Code"...
🎯 Hands-on Coding Challenge Error Formatting

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.

📋 Requirements & What to Write:
  • 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.
Expected Result: Resource not found (404). | Internal server error. Try again later.
💡
Editor Instructions: Complete formatApiError to format HTTP status codes into friendly messages.
errorParser.js
Console / Terminal Output
// Output will appear here when you click "Run Code"...
🎯 Hands-on Coding Challenge Optimistic Mutations

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.

📋 Requirements & What to Write:
  • Optimistically construct new array with appended newItem.
  • If simulateFailure is true, return original unmodified itemsList.
  • Otherwise return the updated optimistic array.
Expected Result: Good result length: 2 | Fail result length: 1 (reverted)
💡
Editor Instructions: Implement optimistic append and rollback logic.
optimisticMutation.js
Console / Terminal Output
// Output will appear here when you click "Run Code"...