An empty-looking list can mean that nothing has been created, a filter found no matches, access was denied or the request failed. “Create your first record” is not a suitable response to all four.

I defined what the model knows before choosing interface copy. A new synthetic loader lets tests change query generations and outcomes without a real service.

Do not call an unread list empty

load_state begins at initial. begin_load advances a generation and enters loading. Only a successful response can become ready or one of the empty states. Not having asked yet and having confirmed the absence of records are different facts, even when both would otherwise draw a blank rectangle.

Rows in this model are synthetic identifier strings, not an actual API response schema. Skeleton design, loading duration and screen-reader announcements are not implemented. Naming the states and making their distinctions understandable in a rendered interface remain different stages, so this work does not claim a completed accessibility or device audit.

Different next actions for different empty results

A successful empty array becomes empty_initial or empty_filtered according to the supplied filter indicator. The former can lead to a first-entry explanation; the latter can suggest clearing conditions or changing a term. Neither automatically authorizes a create button or establishes the company’s workflow.

Here filtered is a boolean supplied by a trusted caller. A real query layer should normalize conditions and determine whether they differ from its baseline. This small state model does not understand every possible filter. It provides a boundary between the layer defining query meaning and the layer selecting the appropriate result presentation.

Keep previous rows explicitly stale

After one successful result, a failed refresh retains those rows but marks them stale. Failure without earlier rows becomes error. Retaining content does not turn a failed current request into ready. A retry flag distinguishes a recoverable query attempt from a normal successful result.

If conditions changed, retained rows can belong to an earlier query. A real interface must preserve and display that earlier basis or choose to hide the rows. This module retains only the minimal row state and does not render a query label. Consumers must not infer that the latest filter has been applied merely because something remains visible.

Handle denial and late responses separately

A denied result clears previous rows and does not automatically recommend retry. Access denial should not resemble a normal search with no matches. Clearing an array does not revoke already-read information or implement server-side authorization; access changes still need enforcement at the service boundary.

A response from an earlier generation leaves current state unchanged. Tests complete an old query after a newer one starts and check that it cannot replace the current result. Future generation numbers and invalid outcomes are rejected. The module does not cancel network requests or stop server work; it governs which received result can update the local state.

Reproduce the state before polishing copy

Tests separately create unfiltered and filtered empty results, retain explicit failure, mark a failed refresh stale, clear rows on denial and ignore older generations. A reproducible state helps copy review agree on the situation being explained instead of arguing about a sentence without its trigger.

The shared package contains pure functions and fixed evidence rather than a newly deployed application screen. Buttons, help and accessible announcements must fit the adopting site’s structure. I share a basis for saying what is currently known and what someone can do next. That basis is useful even when two websites choose different layouts for the same state.