APIs and Data Fetching

React apps need to fetch, display, and manipulate data from APIs. Here are the patterns for handling async data.

fetch API

JSX
function UserList() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        async function loadUsers() {
            try {
                const response = await fetch('https://api.example.com/users');
                if (!response.ok) throw new Error(`HTTP ${response.status}`);
                const data = await response.json();
                setUsers(data);
            } catch (err) {
                setError(err.message);
            } finally {
                setLoading(false);
            }
        }
        loadUsers();
    }, []);

    if (loading) return <div className="spinner">Loading...</div>;
    if (error) return <div className="error">Error: {error}</div>;

    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
}

CRUD Operations

JSX
function TodoApp() {
    const [todos, setTodos] = useState([]);
    const [newTodo, setNewTodo] = useState('');

    // CREATE
    const addTodo = async () => {
        if (!newTodo.trim()) return;

        const response = await fetch('/api/todos', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ text: newTodo }),
        });
        const created = await response.json();
        setTodos(prev => [...prev, created]);
        setNewTodo('');
    };

    // UPDATE
    const toggleTodo = async (id, completed) => {
        await fetch(`/api/todos/${id}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ completed: !completed }),
        });
        setTodos(prev =>
            prev.map(t => t.id === id ? { ...t, completed: !completed } : t)
        );
    };

    // DELETE
    const deleteTodo = async (id) => {
        await fetch(`/api/todos/${id}`, { method: 'DELETE' });
        setTodos(prev => prev.filter(t => t.id !== id));
    };

    return (
        <div>
            <input
                value={newTodo}
                onChange={e => setNewTodo(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && addTodo()}
            />
            <button onClick={addTodo}>Add</button>
            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <span
                            onClick={() => toggleTodo(todo.id, todo.completed)}
                            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
                        >
                            {todo.text}
                        </span>
                        <button onClick={() => deleteTodo(todo.id)}>✕</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

Loading States Pattern

JSX
function useAsync(asyncFn) {
    const [state, setState] = useState({
        data: null,
        loading: true,
        error: null,
    });

    const execute = async (...args) => {
        setState(prev => ({ ...prev, loading: true, error: null }));
        try {
            const data = await asyncFn(...args);
            setState({ data, loading: false, error: null });
            return data;
        } catch (error) {
            setState({ data: null, loading: false, error });
            throw error;
        }
    };

    return { ...state, execute };
}

// Usage
function UserProfile({ userId }) {
    const { data: user, loading, error, execute } = useAsync(
        () => fetch(`/api/users/${userId}`).then(r => r.json())
    );

    useEffect(() => { execute(); }, [userId]);

    if (loading) return <Spinner />;
    if (error) return <ErrorMessage error={error} />;

    return <div>{user.name}</div>;
}

Optimistic Updates

Update the UI immediately, then sync with the server:

JSX
function TodoItem({ todo }) {
    const toggleTodo = async () => {
        // Optimistic update
        setTodos(prev =>
            prev.map(t => t.id === todo.id ? { ...t, completed: !t.completed } : t)
        );

        try {
            await fetch(`/api/todos/${todo.id}`, {
                method: 'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ completed: !todo.completed }),
            });
        } catch (err) {
            // Revert on error
            setTodos(prev =>
                prev.map(t => t.id === todo.id ? { ...t, completed: todo.completed } : t)
            );
        }
    };

    return (
        <li onClick={toggleTodo}>
            {todo.completed ? '✓' : '○'} {todo.text}
        </li>
    );
}

💡 Tip: Never fetch data directly in the render function — always use useEffect. And always handle loading, error, and success states for a good user experience.

Next: Styling and CSS-in-JS