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