Design your loading and empty states before your happy path
Every component that fetches data has at least four states, not one. There is loading (the request is in flight), empty (it succeeded but there is nothing to show), error (it failed), and loaded (the happy path everyone designs). The mock in Figma shows the loaded state; the other three are where real apps feel broken — a spinner that never resolves, a blank screen that looks like a bug when the list is simply empty, a silent failure that leaves the user staring at nothing. Designing all four up front is the difference between an app that feels finished and one that feels finished only when the network is fast and the data is present.
Model the four states, then render each
The cleanest way to guarantee you handle all four is to make status a single value (the request lifecycle from another post) and branch on it exhaustively, so the compiler and your own eyes catch a missing case:
function Results({ status, items }) {
switch (status) {
case "loading": return <SkeletonList />; // structure, not a bare spinner
case "error": return <ErrorState onRetry={reload} />; // a way OUT of the failure
case "success":
return items.length
? <List items={items} />
: <EmptyState message="No results — try a broader search" />; // empty ≠ error
default: return null;
}
}
The items.length check is the state people forget: a successful, empty response is
not an error and not a bug — it needs its own friendly, actionable message.
Each non-happy state has a job
These states are not just “not the content” — each one has work to do. Loading should preserve layout (a skeleton, so nothing jumps when data lands) and only appear after a beat, so a fast response does not flash a spinner. Empty should explain why it is empty and offer the next step (“No orders yet — place your first”). Error must give a way forward — a retry button, a support link — never a dead end:
function EmptyState({ message, action }) {
return (
<div className="empty">
<p>{message}</p>
{action && <Button onClick={action.onClick}>{action.label}</Button>}
</div>
);
}
Designing them first changes the component
The practical reason to design the four states before the happy path is that it
changes the component’s API. If you only think about “loaded,” you write a component
that takes items and breaks when items is undefined during loading. If you
design all four first, you naturally give it status, an error, and an empty
message — and it is robust from the start rather than patched later under a bug
report. This is also where the request/success/fail triple and the four-state UI
meet: the triple produces the states, and this design work spends them. Handle all
four deliberately and your app stops having a “works on my fast connection with
seeded data” quality. The skeleton-list and toast-notifications exercises build the
loading and error states specifically, because those are the two most often skipped
until a user hits them.