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.loading — correct, renders nothing while pending;
- the desktop branch is
DataTableForObservableQuery — wrong, 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:
- 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.
loadingMessage?: ReactNode on the query tables (and DataPage), used in place of emptyMessage while
the query is performing.
- 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.
What happens
DataTableForObservableQueryandDataTableForQuerydrive 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.datais the proxy's default[], and the table rendersemptyMessage.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/components3.4.0,@cratis/arc/@cratis/arc.react21.x, read from the installed packages.Arc supplies the flag.
@cratis/arc/dist/esm/queries/QueryResultWithState.d.ts:The component receives it.
useObservableQueryWithPagingreturns[QueryResultWithState<TDataType>, SetSorting, SetPage, SetPageSize], anddist/esm/DataTables/DataTableForObservableQuery.jsbinds the whole result:And then never consults it. The only members that module reads are
result.data,result.paging.totalItems,result.paging.totalPagesandresult.paging.page.result.isPerformingdoes notappear. Searching the two relevant folders for any state-ish identifier:
DataTableForQuery.jsis the same shape viauseQueryWithPaging.The consequence is mechanical.
result.data(default[]) andemptyMessagego straight toDataTableCore, which rendersDataTable.EmptyTBodywith that message for any empty array. NeitherDataTableCorePropsnor either query-table prop type declares a loading, pending, or busy member, so themessage cannot be suppressed from outside.
Observed, not only read. Instrumenting the client boundary (a patched
EventSource/fetchinstalledbefore 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
isPerformingdescribes.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:
getQueryResultStatus(result) === QueryResultStatus.loading— correct, renders nothing while pending;DataTableForObservableQuery— wrong, 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:
emptyMessagebefore the first emission. Zero new API: whileresult.isPerformingand noresult has yet arrived, render an empty table body rather than the empty message. Silence is not a false
claim.
loadingMessage?: ReactNodeon the query tables (andDataPage), used in place ofemptyMessagewhilethe query is performing.
isPerformingthrough a render prop, anonStateChangecallback, or byletting
emptyMessageaccept 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
should stay there.
correct and should be preserved exactly.
problem and is out of scope here.
DataTableCoreto change — it is handed an array and behaves correctly for one.