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.