When your app fetches data, there’s a gap between clicking and seeing results. Without a loading state, users see either nothing (blank screen) or stale data. They don’t know if the app is working.
Copy
// ❌ No loading state — user sees blank page for 1-3 secondsfunction UserList() { const [users, setUsers] = useState([]); useEffect(() => { fetch("/api/users") .then(r => r.json()) .then(data => setUsers(data)); }, []); return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>; // Empty list until data arrives — looks broken}
This is the pattern you’ll use in almost every component that fetches data. Three states, three checks at the top of the return.
Notice the order: check loading first, then error, then empty state, then render data. This order matters because you want to show the most relevant state.
Use full-page loading for initial loads and inline loading for refreshes. If the user already sees data, don’t replace it with a spinner — show the spinner alongside the existing content.
disabled={submitting} prevents the button from being clicked again
Button text changes to show progress (“Creating…”)
Input is also disabled to prevent editing during submission
finally ensures the button re-enables even if the request fails
Always disable submit buttons during requests. Without this, users can click multiple times and create duplicate entries. This is one of the most common bugs in web applications.
This is the pattern you’ll reuse across your entire application:
Copy
function DataComponent() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function fetchData() { try { setLoading(true); setError(null); const response = await fetch("/api/endpoint"); if (!response.ok) throw new Error(`HTTP ${response.status}`); const result = await response.json(); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); } } fetchData(); }, []); if (loading) return <LoadingSpinner />; if (error) return <ErrorMessage message={error} />; if (!data) return <EmptyState />; return <DataDisplay data={data} />;}
This three-state pattern (loading / error / data) shows up so often that libraries like React Query and SWR were created to automate it. For now, writing it manually teaches you what’s happening under the hood.
You’ve covered the complete async & APIs section — from understanding async JavaScript to making real API calls with proper error handling and loading states.Next up: the DOM and browser APIs. You’ll learn how JavaScript interacts with the page itself — selecting elements, modifying content, and responding to user actions.