Reading the state object useTableCollection hands you
Scope. The Component Docs Gate covers component names and props, and the package's own
TableStatedeclaration is the member list. This file covers only what that declaration cannot tell you: the plausible members that are absent from it, and the behaviour behind the ones that are there.
Why guessing here always fails
useTableCollection() returns a TableState. For the members themselves, read the declaration the
index names — Read <pkgRoot>/dist/dts-bundle/index.json, then its file. Reading them is not the
hard part; the hard part is that several plausible members don't exist at all, and one that does
means something other than its name suggests (see
Four that bite in practice).
One thing not to conclude from the docs bundle. dist/dts-bundle/ publishes CollectionState
as a stub:
declare class CollectionState<T = any, F = any> { [key: string]: unknown }That is a documentation abridgement, not the declaration your code compiles against. tsc resolves
dist/types/index.d.ts, which re-exports the fully typed CollectionState from
@wix/bex-core — no index signature. So state.collection.… is properly typed, a wrong member is a
compile error at the access, and reading state.collection.status.status to debug a stuck table is
sound. See WIX_PATTERNS_DOCS.md § 5.
Prefer a typed member on TableState itself anyway — it is the object the hook hands you, and the
table's own view of the collection.
Four that bite in practice
state.filters does not exist. Filter state lives on the filter objects you created with
stringsArrayFilter() / dateRangeFilter() and passed into filters. Keep those in module-level
consts and reference the const in both the hook config and the JSX — that is also what
MultiSelectCheckboxFilter's filter prop wants.
showLoadingState is not "the query is running". It stays true when the query resolves with
zero rows, so binding SummaryBar's status to it leaves the bar on skeleton pills permanently on
an empty collection — which reads as a hung network rather than an empty result. Use
state.showErrorState ? 'error' : 'success'.
collection.isLoading and collection.refresh() are not members. They look plausible, they
compile against the index signature, and neither exists at runtime.
A fetchTotal that resolves undefined shows 0, not the rows
This is the "SummaryBar says 0 while the table under it is full" report, and it is worth tracing
once because every step is silent. FetchTotalState wraps your function in a second, one-item
collection whose page is literally { items: [], total: await fetchTotal(query) }. That page then
goes through CollectionState.updateResult, which computes
lastPage.total ?? lastPage.items.length — so a total of undefined becomes 0, because the
synthetic page has no items to fall back on. And QueryResultState.total reads
fetchTotal ? fetchTotal.total : this._totals.processed: the moment fetchTotal exists it wins,
and the loaded-rows fallback is gone.
Two consequences worth keeping in mind:
- A wrong
fetchTotalis worse than nofetchTotal. Omit it andtotaldegrades to the rows you have; wire it to something that resolvesundefinedand a page with 4,000 matching rows reports0. Nothing throws, nothing failstsc, and the table beside it is correct. pagingMetadata.totalis not a count. A cursor-paged response carries no total, and@wix/datadocuments the field as returned only under offset paging withreturnTotalCount: true. Under the cursor paging these pages default to, expectundefined. Call something that counts — QUERY_AND_PAGING.md.
To tell the two apart in a running page, show state.collection.totalStatus next to the number:
it is the totals collection's own status, so a success beside a 0 means your function resolved
nothing, not that the query failed.
Reading state outside the table: it is MobX
@wix/patterns depends on mobx and mobx-react-lite, so the state object is observable and a
plain React component does not track it. Anything you derive from state in your own component —
a SummaryBar count, a badge total, a header figure — is computed once on first render, when the
collection is still empty, and never recomputed. The table itself updates because it observes
internally; your derived numbers sit at zero next to a table full of rows, which reads as a bug in
the query rather than in the wiring.
useSelector from @wix/patterns subscribes:
const loadedCount = useSelector(() => state.keyedItems.length);
const rows = useMemo(() => state.keyedItems.map((k) => k.item), [loadedCount]);Select a primitive. Returning a fresh array or object from the selector gives it a new identity on every evaluation. Select a length, a total or an id, then derive the rest once it fires.
Query and result shapes
fetchData(query) receives a ComputedQuery<F>: limit, offset, page, search, rawSearch,
cursor, filters, rawFilters, sort.
Read the return type from @wix/bex-core, not from @wix/patterns. The name
CursorQueryResult exists in both packages and they are different types. The one your fetchData
must satisfy is the generic in node_modules/@wix/bex-core/dist/types/hooks/paginationModeConfig.d.ts:
interface DataResultRaw<T> {
items: T[];
total?: number | null;
available?: number; // total BEFORE filters
cursor?: string | undefined | null;
hasNext?: boolean | null;
}
type OffsetQueryResult<T> = Omit<DataResultRaw<T>, 'cursor'>;
type CursorQueryResult<T> = Omit<DataResultRaw<T>, 'cursor'> & { cursor: string | undefined | null };Three consequences, each of which has shipped as a bug:
In cursor mode the cursor key is required; its value may be undefined. The intersection
re-declares cursor without ?, so return { items } does not compile — but
{ items, cursor: undefined } does. "Required" is about the key, not the value.
On the last page return undefined, never ''. An empty string is still a cursor to the
collection: it requests the next page forever and appends the same rows each pass, a table that
grows without end while the API is perfectly happy, rendering as a spinner under the last row that
reads as "still loading". Derive it so the empty case collapses:
cursor: response.pagingMetadata?.cursors?.next || undefined,hasNext and total exist in both modes — they are on DataResultRaw, which both aliases
extend. Cursor mode also accepts a separate fetchTotal, since a cursor-paged response carries no
total; build its filter exactly as the page's or the count disagrees with the rows it counts.
Look any of these up yourself with Read <pkgRoot>/dist/dts-bundle/index.json and the file path it
gives; the index is the single source of truth, and it moves between versions.