Skip to content

Lesson 8 — Fetching and async state

Objective

Introduce a topic repository with explicit loading, success, empty, and error states.

Prerequisites

Lesson 7 — Typed controlled forms and validation. You should be able to build controlled forms and validate user input.

Mental Model

A request is not just data; it is a state machine. Network success, HTTP success, valid payloads, and current-request relevance are separate questions.

Concept

Async code in the UI is a state machine with at least idle, loading, success, and error states. A repository boundary owns transport, parsing, and validation. Requests can race, so a newer response should not be overwritten by an older one. Keeping previous data visible during reload is a deliberate policy, not a default.

Example

type Async<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

async function fetchTopics(): Promise<Topic[]> {
  const res = await fetch('/api/topics');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const payload = await res.json();
  return validateTopics(payload);
}

Real-World Usage

Search-as-you-type, dashboards that poll, feeds that paginate, and any UI that talks to a remote system.

Common Mistakes

  • Treating a fetch rejection and a non-2xx response as the same failure.
  • Letting an old, slow request overwrite a newer one.
  • Showing nothing during loading because "it is fast on my machine."

Mini Lab

Task

Define a repository interface and discriminated async state. Fetch topics, validate the response boundary, display retry UI, and keep previous data policy explicit.

Constraints

  • Exercise slow, empty, malformed, non-2xx, offline, and retry paths.
  • Assert visible behavior rather than implementation calls alone.

Expected Result

The UI displays a loading state, then either data, an empty message, or a recoverable error. Stale request results are ignored, and retries work without a full reload.

Hints

Keep transport parsing at the repository boundary. For a larger production app, evaluate a server-state library instead of rebuilding caching and deduplication.

Knowledge Check

  • Why does fetch not reject every failed HTTP request?
  • How do stale requests overwrite newer results?
  • Which concerns belong to server-state caching?

Challenge

A search input fires a request on every keystroke. Outline a request cancellation and debounce policy, and explain when you would keep previous results versus clear them.

Summary

Server state is a state machine. Own transport at a repository boundary and make request races, retries, and previous-data policies explicit.

What Comes Next

Lesson 9 — Effects, cancellation, and event reasoning