useWatchState
The standalone by-id subscription to a single state key — or, with no key, to every key at once. 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.useWatchState(key) (same hook, no id arg, no generic restatement).
Signature
Two overloads — keyed and keyless. Both take pgTableId as the first argument; the state-shape generic narrows the return type so the slice comes back fully typed.
// Imported from @westopp/pgtable useWatchState<T extends PgTableState>(pgTableId: string): PgTableStateValues<T> useWatchState<T extends PgTableState, K extends keyof T>(pgTableId: string, key: K): PgTableStateValues<T>[K]
When to reach for the standalone hook
Prefer tableApi.useWatchState(key) inside any component that already has a typed tableApi — same hook, scoped to the right table, pre-typed against the state shape, no generic restatement. The standalone useWatchState(id, key) is the answer when you only have a pgTableId string: a sibling badge, a render variant fed by a context that only holds the id, an embedded UI shipped from a library that subscribes by id.
// Standalone by-id read — sibling render-component, takes the // pgTableId via props. State-shape generic narrows the slice type. import { useWatchState } from '@westopp/pgtable' import type { charactersState } from './characters-table-config' export const CharactersSelectionBadge = ({ pgTableId }: { pgTableId: string }) => { const selectedIds = useWatchState<typeof charactersState.stateShape, 'selectedIds'>(pgTableId, 'selectedIds') return <span>{selectedIds.length} selected</span> }
Keyless subscription
useWatchState(pgTableId) with no key wakes the component on every state commit. The return value is the full state object, but you usually do not care about it — the common shape is "subscribe to all, then ask tableApi a derived question." Use it for dirty-driven UI, inspector panels, anywhere "something changed" is sufficient.
// No key — subscribes to every key. Wakes on any state commit. // Useful for dirty-driven UI like a Save button. import type { PgTableApi } from '@westopp/pgtable' import type { charactersColumns, charactersState } from './characters-table-config' type Props = { tableApi: PgTableApi<typeof charactersColumns, typeof charactersState> } export const CharactersSaveButton = ({ tableApi }: Props) => { tableApi.useWatchState() // keyless — wakes on every state commit const dirty = tableApi.isDirtyData() || tableApi.isDirtyState() return <button disabled={!dirty}>Save</button> }
Reads are immutable to the caller
The slice value returned reflects the committed store. Mutating it in place — sort.direction = 'desc' — does not notify subscribers and corrupts the store's view of "what is current" for the next reader. Writes go through tableApi.setState (or the global setTableState).
// Reads from useWatchState are read-only by convention. const sort = useWatchState<S, 'sort'>(pgTableId, 'sort') // BAD — silent no-op for subscribers. // sort.direction = 'desc' // GOOD — explicit write through setState; subscribers wake. // tableApi.setState((s) => { s.sort.direction = 'desc' })
Rules of Hooks apply
Call useWatchState at the top level of your component, in the same order on every render. It runs useSyncExternalStore under the hood, so conditional or looped calls break the snapshot equality contract.
// useWatchState is a hook — Rules of Hooks apply. // Top-level only, fixed call order per render. Same constraint applies // to tableApi.useWatchState — the use* prefix is the signal. // GOOD // BAD const sort = tableApi.useWatchState('sort') if (showFilters) { const query = tableApi.useWatchState('query') const query = tableApi.useWatchState('query') return showFilters } ? <Filters query={query} sort={sort} /> : <Grid sort={sort} />
Behaviour when the table isn't mounted
Same leniency as useWatchData — no throw, sensible empty returns until the table appears.
// Like useWatchData, useWatchState is lenient on a missing pgTableId: // - keyless: returns a frozen empty object // - keyed: returns undefined // // Once the table mounts, the subscription begins delivering real values.