What you'll learn
- Always model loading, success, empty, and error explicitly
- Keep layouts stable to avoid jumps
- Give recovery actions on errors
- Checklist for list and detail screens
Direct answer: every async screen needs four outcomes: loading, success with data, success with no data (empty), and error. Render each on purpose. Do not leave a blank page or a spinner that never ends.
When to use this
- Lists, dashboards, and detail pages that fetch
- Forms that wait on a server action
- Any UI that can legitimately have zero results
When not to use this
- Pure static content with no async work
- Optimistic UI that already shows local data (still plan a failure path)
Practical example
JSX
function UsersList({ status, users, onRetry }) {
if (status === 'loading') return <p role="status">Loading users…</p>
if (status === 'error') {
return (
<div role="alert">
<p>Could not load users.</p>
<button type="button" onClick={onRetry}>
Try again
</button>
</div>
)
}
if (users.length === 0) return <p>No users yet. Invite your first teammate.</p>
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
)
}Decision guide
| Outcome | Show |
|---|---|
| Loading | Status text or skeleton that matches final layout |
| Empty | Explanation + next action |
| Error | What failed + retry or support path |
| Success | The data, with stable structure |
Common mistakes
- Using an empty array as both “still loading” and “loaded empty”
- Spinners that replace the whole page layout (CLS)
- Error UI with no recovery
Checklist
- Loading and empty are distinguishable
- Errors use
role="alert"(or equivalent) and a retry - Skeleton reserves space for the final content
- Empty state tells the user what to do next
Related
Final recommendation
Model the four outcomes in your data layer or UI state, then render each deliberately. Users trust screens that explain waits, emptiness, and failures.