Home/Learn/React/Designing the Loading, Error and Empty States

Designing the Loading, Error and Empty States

Intermediate
Data

The states around the data are most of the perceived quality of an app. They are also where juniors are most visibly junior in a code review.

Overview

A screen spends a meaningful fraction of its life not showing data — it is loading, it failed, or there is nothing there yet. Users judge an application on those moments far more than on the happy path, and reviewers read them as a direct signal of experience. The work is not difficult; it is remembering to do it every time, and choosing the right treatment for each case: a skeleton rather than a spinner, an error that says what to do next, and an empty state that distinguishes "nothing yet" from "nothing matched your filter".

Loading

Match the shape of what is coming, and do not flash for fast responses.

Skeletons, delayed, and refresh vs first load
// A skeleton reserves the final layout, so nothing jumps when data lands
function ProblemsSkeleton() {
  return (
    <ul aria-busy="true" aria-label="Loading problems">
      {Array.from({ length: 6 }, (_, i) => (
        <li key={i} className="h-16 rounded animate-pulse bg-muted" />
      ))}
    </ul>
  )
}

// A spinner in the middle of an empty page tells the user nothing
// about what is arriving, and guarantees a layout shift.

// Avoid the flash: if the response takes 80ms, showing a skeleton
// for 80ms looks like a glitch. Delay it.
const showSkeleton = useDelayedFlag(isLoading, 200)

// Distinguish first load from a background refresh:
//   isLoading    -> no data yet, show the skeleton
//   isFetching   -> data on screen, show a subtle indicator instead
{isFetching && <span className="text-xs">Updating…</span>}

Errors

What failed, whether it is the user's problem, and what to do next.

Different causes, different responses
// Useless
<p>Something went wrong</p>

// Useful — distinguishes the causes the user can act on
function ErrorState({ error, onRetry }) {
  if (error.status === 401) return <SignInPrompt />
  if (error.status === 403) return <UpgradePrompt />
  if (error.status === 404) return <NotFound />
  if (error.status >= 500 || error.name === 'TypeError')
    return (
      <div role="alert">
        <p>We could not load your problems. This is on us.</p>
        <button onClick={onRetry}>Try again</button>
      </div>
    )
  return <p role="alert">{error.message}</p>
}

// Never surface a raw stack trace or a database message to a user.
// Log the detail, show the intent.

// An offline check costs one line and explains a lot of failures:
if (!navigator.onLine) return <p>You appear to be offline.</p>

Empty States

Three different emptinesses, and they need different words.

Nothing yet, nothing matched, nothing left
// 1. Nothing yet — the onboarding moment, the highest-value empty state
<Empty
  title="No submissions yet"
  body="Solve your first problem and your progress appears here."
  action={<Link to="/dsa/problems">Browse problems</Link>}
/>

// 2. Nothing matched — the filter is the cause, so offer to clear it
<Empty
  title={`No problems tagged "${topic}"`}
  action={<button onClick={clearFilters}>Clear filters</button>}
/>

// 3. Nothing left — a completed queue, which deserves a positive tone
<Empty title="All caught up." />

// The three are frequently collapsed into one blank area, which is
// how a working screen ends up looking broken.

// And keep the transitions calm: a page that renders skeleton ->
// empty -> data within 200ms reads as flickering. Hold the previous
// data while refetching where the library supports it.

Key Points to Remember

  • 1A skeleton matching the final layout prevents the layout shift a centred spinner guarantees
  • 2Delay the loading indicator by ~200ms so fast responses do not flash
  • 3Distinguish the first load from a background refresh — the second should not blank the screen
  • 4Map error causes to different responses: sign in, upgrade, not found, or retry
  • 5"Nothing yet", "nothing matched" and "nothing left" are three different empty states with different copy

Interview Questions

Sign in to ask Aria
1

Why is a skeleton usually better than a spinner?

Easy
2

How would you present errors differently for a 401, a 403 and a 500?

Medium
3

Why does a screen that briefly flashes an empty state feel broken?

Medium

Ask Aria about Designing the Loading, Error and Empty States

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…