useWatchData
The standalone by-id subscription. Pass the pgTableId string and get back the current rows; re-renders only when data changes. Used where you don't have tableApi in scope — sibling consumers, multi-renderer per TABLE-217, anything reaching in by id. Where tableApi is in scope, prefer tableApi.useWatchData() (same hook, no id arg, no generic restatement).
Signature
One overload. Pass the pgTableId — the same id usePgTable mounted with — and an optional column-type generic so the return type is narrowed to PgTableData<C>.
// Imported from @westopp/pgtable useWatchData<C extends PgTableColumns>(pgTableId: string): PgTableData<C>
When to reach for the standalone hook
Prefer tableApi.useWatchData() inside any component that already has a typed tableApi — same hook, scoped to the right table, pre-typed against the columns, no generic restatement. The standalone useWatchData(id) is the answer when you only have a pgTableId string: sibling consumers that aren't part of the outer/inner split, multi-renderer setups (TABLE-217), a row-count badge mounted above the table.
// Standalone by-id read — used when tableApi isn't in scope. Pass the // pgTableId as a prop and subscribe directly. import { useWatchData } from '@westopp/pgtable' export const CharactersRowCount = ({ pgTableId }: { pgTableId: string }) => { const data = useWatchData(pgTableId) return <span>{data.length} rows</span> }
Derive view shapes on top
useWatchData returns the raw rows. Filtering, sorting, paginating, grouping are your useMemo calls downstream. The hook re-renders only when rows commit; your derivations re-run only when their actual inputs change.
// The hook returns the raw rows. Anything view-shaped (filtered, // sorted, paginated) is your useMemo over it. pgTable does not ship a // query engine — that is a feature. // // Inside an inner component that has tableApi in scope, prefer the // scoped methods — already typed against this table. const data = tableApi.useWatchData() const query = tableApi.useWatchState('query') const visible = useMemo(() => { const q = query.trim().toLowerCase() if (!q) return data return data.filter((r) => r.name.toLowerCase().includes(q)) }, [data, query])
Reads are immutable to the caller
The array returned reflects the committed store. Mutating it in place — data[0].name = 'x' — does not notify subscribers and corrupts the store's view of "what is current" for the next reader. Writes go through tableApi.setData (or the global setTableData).
// Reads from useWatchData are read-only by convention. const data = useWatchData(pgTableId) // BAD — silent no-op for subscribers, leaves the store inconsistent. // data[0].name = 'edited' // GOOD — explicit write through setData; subscribers wake. // tableApi.setData((rows) => { rows[0].name = 'edited' })
Behaviour when the table isn't mounted
Reading from a pgTableId that does not exist returns a stable empty array. No throw, no warning — a deliberately lenient choice to keep out-of-order mounts from cascading errors.
// If the pgTableId does not exist in the store yet, useWatchData returns // a stable frozen empty array — no throw, no flicker. Once the table // mounts, the subscription begins delivering real values. // // This is intentional: render-order between an outer that mounts the // store and a sibling consumer isn't always guaranteed.