Define the backend contract

Assume a request returns a canonical collection and retrieval timestamp. A refresh may replace that collection or fail while the previous collection remains usable. The backend still owns data validity and action authorization.

Use a minimal model:

const initial = {
  data: null,
  phase: "idle",
  error: null,
  receivedAt: null,
};

data: null means no successful result yet. data: [] means a successful empty result. That distinction prevents initial loading and empty success from collapsing into the same branch.

React's state-structure guidance recommends avoiding contradictory and redundant state. Store the canonical request facts; derive the screen mode during render.

Derive one render policy

function viewState(model) {
  if (model.data === null && model.phase === "loading") return "initial-loading";
  if (model.data === null && model.phase === "failed") return "initial-failure";
  if (model.data?.length === 0 && model.phase === "ready") return "empty";
  if (model.data !== null && model.phase === "refreshing") return "refreshing";
  if (model.data !== null && model.phase === "failed") return "failed-refresh";
  return "ready";
}

This illustrative function has not been browser-tested. Test it independently for every branch before connecting a data library.

Keep useful data during refresh

For refreshing, retain the current rows, label them as updating, and decide whether actions remain available. A filter change may make old rows misleading; in that case disable consequential actions or state which filter produced the visible results.

For failed-refresh, retain the last successful rows with their timestamp, show a persistent error, and offer retry. Do not replace useful data with a full-screen error unless using stale data would itself be unsafe.

Render every state intentionally

State Visible data Status Action
Initial loading None Loading records Cancel if supported
Initial failure None Could not load Retry
Empty Empty success No matching records Clear filter or create
Ready Current result Optional timestamp Normal actions
Refreshing Previous result Refreshing Policy-dependent
Failed refresh Previous result Update failed; data timestamp Retry

Use a status region for meaningful asynchronous updates. The W3C status-message guidance explains how status changes can be exposed to assistive technology without moving focus. Test the assembled screen with keyboard and target assistive technologies; adding role="status" alone is not proof.

Test the state logic

Write cases for:

  • initial request starts and succeeds empty;
  • initial request fails and retry succeeds;
  • ready data begins refresh without disappearing;
  • refresh succeeds with a new empty collection;
  • refresh fails while old data and timestamp remain;
  • a late older response cannot overwrite a newer result; and
  • navigation or unmount does not report a false success.

The pattern does not prescribe Suspense, a query library, or a fetch client. It defines the business-visible states those tools must preserve.