| name | preact/compose-with-tanstack-store |
| description | `@tanstack/preact-table` v9 is built on TanStack Store. Each registered state slice is an atom. The table exposes three reactive surfaces: `table.atoms.<slice>` (per-slice readonly), `table.store` (flat readonly view), and `table.baseAtoms.<slice>` (internal writable). Use external atoms via `useCreateAtom` + `options.atoms` to hand slice ownership to your app, share atoms across components with `useSelector`, and subscribe imperatively with `atom.subscribe(...)` for persistence/sync. Routing keywords: preact-store, useCreateAtom, useSelector, atoms, external state, slice ownership, persistence.
|
| type | composition |
| library | tanstack-table |
| framework | preact |
| library_version | 9.0.0-alpha.48 |
| requires | ["state-management","preact/table-state"] |
| sources | ["TanStack/table:docs/framework/preact/guide/table-state.md","TanStack/table:examples/preact/basic-external-atoms/src/main.tsx","TanStack/table:examples/preact/basic-subscribe/src/main.tsx","TanStack/table:packages/preact-table/src/useTable.ts","TanStack/table:packages/preact-table/src/reactivity.ts"] |
@tanstack/preact-table v9 stores every registered state slice as a TanStack Store atom under the hood, and useTable wires Preact to those atoms via @tanstack/preact-store. This skill is the bridge between table state and the rest of your TanStack Store-powered app.
The Three Surfaces
| Surface | Shape | Use when |
|---|
table.atoms.<slice> | ReadonlyAtom<TSliceState> per slice | Read or subscribe to one slice (preferred) |
table.store | ReadonlyStore<TableState<TFeatures>> (flat derived view) | Reading full table state, selecting projections |
table.baseAtoms.<slice> | Atom<TSliceState> (writable internal) | Low-level writes when the slice is internally owned |
External atoms passed via options.atoms.<slice> take precedence over options.state[<slice>] and over table.baseAtoms.<slice>. Writes from feature APIs (table.setSorting(...), table.setPageIndex(...), etc.) flow into whichever atom owns the slice.
Source: docs/framework/preact/guide/table-state.md; packages/preact-table/src/useTable.ts.
Pattern 1 — Hand a slice to your app
import { useCreateAtom, useSelector } from '@tanstack/preact-store'
import {
rowPaginationFeature,
rowSortingFeature,
tableFeatures,
useTable,
type PaginationState,
type SortingState,
} from '@tanstack/preact-table'
const features = tableFeatures({ rowPaginationFeature, rowSortingFeature })
function PeopleTable({ data }) {
const sortingAtom = useCreateAtom<SortingState>([])
const paginationAtom = useCreateAtom<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const sorting = useSelector(sortingAtom)
const pagination = useSelector(paginationAtom)
const table = useTable({
features,
columns,
data,
atoms: { sorting: sortingAtom, pagination: paginationAtom },
})
return null
}
Source: examples/preact/basic-external-atoms/src/main.tsx.
Pattern 2 — Share an atom across components
Lift the atom to a module / context. Any component can read or write it, and the table stays in sync.
import { createAtom } from '@tanstack/preact-store'
import type { RowSelectionState } from '@tanstack/preact-table'
export const selectionAtom = createAtom<RowSelectionState>({})
import { selectionAtom } from '../shared/atoms'
const table = useTable({
features,
columns,
data,
atoms: { rowSelection: selectionAtom },
})
import { useSelector } from '@tanstack/preact-store'
import { selectionAtom } from '../shared/atoms'
function SelectionSummary() {
const sel = useSelector(selectionAtom)
return <span>{Object.keys(sel).length} selected</span>
}
Module-scope atoms are stable identities — no useCreateAtom needed. Don't create module-scope atoms inside a component-render body.
Source: docs/framework/preact/guide/table-state.md.
Pattern 3 — Subscribe imperatively for persistence / sync
Atom.subscribe returns an { unsubscribe } handle. Persist to localStorage, push to a URL, or fan out to other stores.
import { useEffect } from 'preact/hooks'
useEffect(() => {
const sub = paginationAtom.subscribe((next) => {
localStorage.setItem('table:pagination', JSON.stringify(next))
})
return () => sub.unsubscribe()
}, [paginationAtom])
You can also subscribe to table.atoms.<slice> directly without owning the slice. The subscription fires whenever the slice changes — whoever owns it (your atom or baseAtoms).
Source: packages/preact-table/src/reactivity.ts.
Pattern 4 — Read inside cells with the standalone <Subscribe>
Inside a cell or header render context, table is the core Table<TFeatures, TData>, not PreactTable. table.Subscribe is undefined — import the standalone component.
import { Subscribe } from '@tanstack/preact-table'
columnHelper.display({
id: 'select',
cell: ({ row, table }) => (
<Subscribe source={table.atoms.rowSelection} selector={(rs) => rs[row.id]}>
{(isSelected) => (
<input
type="checkbox"
checked={!!isSelected}
onChange={row.getToggleSelectedHandler()}
/>
)}
</Subscribe>
),
})
Source: packages/preact-table/src/Subscribe.ts; examples/preact/basic-subscribe/src/main.tsx.
Common Mistakes
CRITICAL Creating an atom inside the render body without useCreateAtom
Wrong:
function MyTable() {
const sortingAtom = createAtom<SortingState>([])
useTable({ , atoms: { sorting: sortingAtom } })
}
Correct:
function MyTable() {
const sortingAtom = useCreateAtom<SortingState>([])
useTable({ , atoms: { sorting: sortingAtom } })
}
A fresh atom each render unbinds the slice and resets it to the initial value on every render.
Source: examples/preact/basic-external-atoms/src/main.tsx.
HIGH Writing to table.baseAtoms.X.set() when the slice is externally owned
Wrong:
useTable({ , atoms: { pagination: paginationAtom } })
table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 }))
Correct:
table.setPageIndex(0)
paginationAtom.set((old) => ({ ...old, pageIndex: 0 }))
table.baseAtoms is the internal writable atom — used only when the slice is internally owned. When you hand a slice to an external atom, the external atom is the source of truth.
Source: docs/framework/preact/guide/table-state.md.
HIGH Reading .get() and expecting re-renders
Wrong:
function Pager() {
const { pageIndex } = table.atoms.pagination.get()
return <span>Page {pageIndex + 1}</span>
}
Correct:
import { useSelector } from '@tanstack/preact-store'
function Pager() {
const pageIndex = useSelector(table.atoms.pagination, (p) => p.pageIndex)
return <span>Page {pageIndex + 1}</span>
}
;<table.Subscribe source={table.atoms.pagination} selector={(p) => p.pageIndex}>
{(pageIndex) => <span>Page {pageIndex + 1}</span>}
</table.Subscribe>
.get() returns the current value without subscribing.
MEDIUM Passing the same slice via atoms AND state
Wrong:
useTable({
,
atoms: { pagination: paginationAtom },
state: { pagination },
onPaginationChange: setPagination,
})
Correct: pick exactly one ownership path per slice.
See Also
tanstack-table/preact/table-state — atoms / Subscribe / FlexRender reference.
tanstack-table/preact/compose-with-tanstack-query — server-side flow keyed by atoms.
tanstack-table/preact/production-readiness — when to reach for narrow subscriptions.