Skip to content

The query-driven data tables render emptyMessage before the first result arrives, and expose no seam for the pending state Arc already gives them #160

Description

@woksin

What happens

DataTableForObservableQuery and DataTableForQuery drive their query internally and expose no way to tell
"the first result has not arrived yet" from "the query returned nothing". While the query is still performing,
result.data is the proxy's default [], and the table renders emptyMessage.

So the table asserts to the user that there are no rows, before it knows. In our application that message
is "Ingen brukere funnet." ("No users found") on a tenant that demonstrably has users. It is not a missing
spinner — it is a false statement rendered with full confidence, and there is no prop, callback or render slot
through which a caller can prevent it.

The state that would settle it already exists one layer down and is discarded.

Evidence

@cratis/components 3.4.0, @cratis/arc / @cratis/arc.react 21.x, read from the installed packages.

Arc supplies the flag. @cratis/arc/dist/esm/queries/QueryResultWithState.d.ts:

export declare class QueryResultWithState<TDataType> implements IQueryResult<TDataType> {
    readonly data: TDataType;
    readonly paging: PagingInfo;
    readonly isSuccess: boolean;
    
    readonly isPerforming: boolean;      // ← this one
    static initial<TDataType>(defaultValue: TDataType): QueryResultWithState<TDataType>;
}

The component receives it. useObservableQueryWithPaging returns
[QueryResultWithState<TDataType>, SetSorting, SetPage, SetPageSize], and
dist/esm/DataTables/DataTableForObservableQuery.js binds the whole result:

const [result, , setPage] = useObservableQueryWithPaging(props.query, paging, props.queryArguments);

And then never consults it. The only members that module reads are result.data,
result.paging.totalItems, result.paging.totalPages and result.paging.page. result.isPerforming does not
appear. Searching the two relevant folders for any state-ish identifier:

$ grep -rn "isPerforming\|isLoading\|hasError\|loading" dist/esm/DataTables/ dist/esm/DataPage/
(no matches)

DataTableForQuery.js is the same shape via useQueryWithPaging.

The consequence is mechanical. result.data (default []) and emptyMessage go straight to
DataTableCore, which renders DataTable.EmptyTBody with that message for any empty array. Neither
DataTableCoreProps nor either query-table prop type declares a loading, pending, or busy member, so the
message cannot be suppressed from outside.

Observed, not only read. Instrumenting the client boundary (a patched EventSource/fetch installed
before any page script) over batches of fresh page loads: in a 6-tab run, 3 of 120 loads painted the empty
state and 5 of 120 had still not rendered a row when their 30 s deadline expired, while the server-side read
model held 11 instances throughout. In every one of those loads the subscribe POST was still pending — the
first frame had not arrived. That is exactly the state isPerforming describes.

The trigger for the delay in our environment is our own (HTTP/1.1 per-origin connection pressure from
long-lived SSE streams) and is not being reported here. It is the trigger, not the defect: any cause of a slow
first frame produces the same false "no rows" claim.

What it costs a consumer

Concrete and currently live. The clearest statement of the cost is that one of our pages has two branches
over the same query, and only one of them is correct:

  • the mobile branch calls the Arc hook itself and gates on
    getQueryResultStatus(result) === QueryResultStatus.loadingcorrect, renders nothing while pending;
  • the desktop branch is DataTableForObservableQuerywrong, renders "no users found" while pending.

The difference between them is not care or review; it is that the component owns the hook call and does not
forward the state. Our local remedy is to wrap the table in a gate driven by a second subscription to the same
query
, purely to learn something the component already knows and drops.

Suggested fix — the seam

Any one of these closes it; they are listed cheapest-first and we have no stake in which:

  1. Do not render emptyMessage before the first emission. Zero new API: while result.isPerforming and no
    result has yet arrived, render an empty table body rather than the empty message. Silence is not a false
    claim.
  2. loadingMessage?: ReactNode on the query tables (and DataPage), used in place of emptyMessage while
    the query is performing.
  3. Forward the state — expose isPerforming through a render prop, an onStateChange callback, or by
    letting emptyMessage accept a function of the query state — so the caller decides.

(1) alone would remove the false statement, which is the part that matters.

What is explicitly not being asked for

  • Not asking for a spinner, skeleton, shimmer or any specific loading visual — that is the consumer's taste and
    should stay there.
  • Not asking to change the empty state for a query that has genuinely returned zero rows. That behavior is
    correct and should be preserved exactly.
  • Not asking for retry, reconnect, or transport changes; the transport delay in our environment is our own
    problem and is out of scope here.
  • Not asking for DataTableCore to change — it is handed an array and behaves correctly for one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions