Multiple tables on one page
pgTable's store is keyed by pgTableId, so two tables on the same page are just two entries. Each one is a standalone-table with its own config, its own usePgTable call, no cross-state, no provider tree, no lifting.
Two simple-tier tables, two stable ids
Side-by-side tables don't share state, so each one is its own simple-table: a single <name>-table.tsx with the config inline. The only rule that matters between them is that each carries a stable, caller-assigned pgTableId — never Math.random() per render.
// Simple-tier table — config inline, no orchestration, stable pgTableId. import z from 'zod' import { type PgTableApi, pgt, usePgTable } from '@westopp/pgtable' const charactersColumns = pgt.tableColumns<{ label: string; sortable: boolean }>()({ id: { type: z.string(), label: 'ID', sortable: false }, name: { type: z.string(), label: 'Name', sortable: true }, }) const charactersState = pgt.tableState({}, {}) type InnerProps = { tableApi: PgTableApi<typeof charactersColumns, typeof charactersState> } const CharactersInner = ({ tableApi }: InnerProps) => { const data = tableApi.useWatchData() // …render rows } export const CharactersTable = () => { const { tableApi } = usePgTable(charactersColumns, charactersState, { pgTableId: 'characters', data: charactersSeed, }) return <CharactersInner tableApi={tableApi} /> }
// Simple-tier table next door — its own config, its own stable pgTableId. import z from 'zod' import { type PgTableApi, pgt, usePgTable } from '@westopp/pgtable' const itemsColumns = pgt.tableColumns<{ label: string; sortable: boolean }>()({ id: { type: z.string(), label: 'ID', sortable: false }, name: { type: z.string(), label: 'Name', sortable: true }, }) const itemsState = pgt.tableState({}, {}) type InnerProps = { tableApi: PgTableApi<typeof itemsColumns, typeof itemsState> } const ItemsInner = ({ tableApi }: InnerProps) => { const data = tableApi.useWatchData() // …render rows } export const ItemsTable = () => { const { tableApi } = usePgTable(itemsColumns, itemsState, { pgTableId: 'items', data: itemsSeed, }) return <ItemsInner tableApi={tableApi} /> }
// Page composes both. No shared state, no provider tree, no context. export const DashboardPage = () => ( <div> <CharactersTable /> <ItemsTable /> </div> )
One save for many tables
The classic multi-table form: a dashboard with several editable grids and one "Save" button. Walk the store by id using global-helpers (they take inputs through params), collect dirty payloads, post once.
// One submit, many tables. The global helpers walk the store by pgTableId. import { getTable } from '@westopp/pgtable' export const submitDashboard = async () => { const ids = ['characters', 'items', 'locations'] const payload = ids.reduce<Record<string, unknown>>((acc, id) => { const api = getTable(id) if (!api) return acc if (api.isDirtyData() || api.isDirtyState()) { acc[id] = { rows: api.isDirtyData() ? api.getData() : undefined, changes: api.dirtyStateProperties(), } } return acc }, {}) if (Object.keys(payload).length === 0) return await api.saveDashboard(payload) }
Cross-table writes from outside React
A change in one table can drive writes into another via setTableData / setTableState (caller-agnostic, identified by pgTableId). The store is the contract — no prop-drilling, no callback gymnastics.
// Refresh two dependent tables from a single trigger. Uses the global // helpers (caller-agnostic, identified by pgTableId). import { setTableData, setTableState } from '@westopp/pgtable' export const refreshFromOrg = async (org: string) => { setTableState('characters', { loading: true }) setTableState('items', { loading: true }) const [characters, items] = await Promise.all([ api.fetchCharacters({ org }), api.fetchItems({ org }), ]) setTableData('characters', characters) setTableData('items', items) setTableState('characters', { loading: false }) setTableState('items', { loading: false }) }
Same id = same entry
Two usePgTable calls with the same id share the store entry — that's the contract of pgTableId. Useful when a sidebar widget and a main grid read the same table; not useful when you want side-by-side independents. For "compare" views, give the two mounts distinct ids.
// One table per id at a time. Two components mounting usePgTable with the // same id reuse the same store entry — the second mount picks up wherever // the first left off (data, state, state listeners are replaced). That's the // intended behaviour for "open the same dashboard in two panels", not for // "render two copies of the same table side by side". // If you do want side-by-side independent copies, give them different ids. usePgTable(columns, state, { pgTableId: 'characters:left', data: rows }) usePgTable(columns, state, { pgTableId: 'characters:right', data: rows })
Cleanup on exit
Tables persist by pgTableId, not by component lifecycle (except for persist: { mode: 'none' }, which clears on unmount). For UI-only tables that don't need to survive, call removeTable(id) on the page's unmount. For app-level reset, dropTableEverywhere(id) wipes memory and webstorage in one call.
// On dashboard exit, drop the entries that aren't meant to persist. import { dropTableEverywhere, removeTable } from '@westopp/pgtable' useEffect(() => () => { // Pure-UI tables (no persistence) can be cleaned up here. removeTable('characters') removeTable('items') }, []) // Or, for a full wipe (memory + any persisted webstorage entry): const onResetAll = () => { dropTableEverywhere('characters') dropTableEverywhere('items') }