| name | lit/lit-table-controller |
| description | The `TableController` ReactiveController pattern for hosting a TanStack Table instance inside a LitElement. One controller per host (constructed in a class field); `.table(options, selector?)` called from `render()`. The controller installs the Lit `coreReativityFeature`, subscribes the host to `table.store` and `table.optionsStore`, and tears those subscriptions down on `hostDisconnected`. Routing keywords: TableController, ReactiveController, ReactiveControllerHost, hostConnected, hostDisconnected, lit-table.
|
| type | framework |
| library | tanstack-table |
| framework | lit |
| library_version | 9.0.0-alpha.48 |
| requires | ["lit/table-state"] |
| sources | ["TanStack/table:docs/framework/lit/lit-table.md","TanStack/table:docs/framework/lit/guide/table-state.md","TanStack/table:packages/lit-table/src/TableController.ts","TanStack/table:packages/lit-table/src/reactivity.ts","TanStack/table:examples/lit/basic-table-controller/src/main.ts"] |
Maintainer note: the Lit adapter is scheduled for a rewrite alongside TanStack Lit Store during the v9 beta cycle. TableController's invalidation model and Subscribe mode may change in a future beta. The patterns below match 9.0.0-alpha.48.
TableController is the Lit-specific entry point for @tanstack/lit-table. It implements the Lit ReactiveController interface, hosts the underlying core Table instance, and bridges TanStack Store atom changes to host.requestUpdate() calls. This skill explains the lifecycle in detail.
What TableController actually does
export class TableController<TFeatures, TData> implements ReactiveController {
constructor(host: ReactiveControllerHost) {
;(this.host = host).addController(this)
}
public table(tableOptions, selector?) {
if (!this._table) {
this._table = constructTable({
...tableOptions,
_features: {
coreReativityFeature: litReactivity(),
...tableOptions._features,
},
mergeOptions: (def, next) => ({ ...def, ...next }),
})
this._setupSubscriptions()
}
this._table.setOptions((prev) => ({ ...prev, ...tableOptions }))
return {
}
}
private _setupSubscriptions() {
this._storeSubscription = this._table.store.subscribe(() =>
this.host.requestUpdate(),
)
this._optionsSubscription = this._table.optionsStore!.subscribe(() =>
this.host.requestUpdate(),
)
}
hostConnected() {
this._setupSubscriptions()
}
hostDisconnected() {
this._storeSubscription?.unsubscribe()
this._optionsSubscription?.unsubscribe()
}
}
Source: packages/lit-table/src/TableController.ts (full file).
Key points:
- One core table per controller. The first
.table(options) call constructs it; later calls merge options into the same instance.
- Two subscriptions:
table.store (state) and table.optionsStore (options). Both call host.requestUpdate().
- Subscriptions are torn down on
hostDisconnected and reset on hostConnected.
Subscribe is whole-store. The current adapter does not split host invalidation by source; table.Subscribe reads its source at render time, but the host still re-renders on any store change.
Lifecycle Diagram
constructor render() hostDisconnected
│ │ │
▼ ▼ ▼
host.addController(this) this.tableController.table(opts) unsubscribe(store)
│ unsubscribe(options)
▼
(first call) constructTable(opts) + _setupSubscriptions
(later calls) table.setOptions(prev => ({ ...prev, ...opts }))
│
▼
returns { ...table, Subscribe, FlexRender, state }
Canonical Setup
import { LitElement, html } from 'lit'
import { customElement, state } from 'lit/decorators.js'
import { repeat } from 'lit/directives/repeat.js'
import {
FlexRender,
TableController,
tableFeatures,
type ColumnDef,
} from '@tanstack/lit-table'
const _features = tableFeatures({})
const columns: Array<ColumnDef<typeof _features, Person>> = [
]
@customElement('people-table')
class PeopleTable extends LitElement {
private tableController = new TableController<typeof _features, Person>(this)
@state()
private data: Person[] = []
protected render() {
const table = this.tableController.table(
{
_features,
_rowModels: {},
columns,
data: this.data,
},
() => ({}),
)
return html`
<table>
<thead>
${repeat(
table.getHeaderGroups(),
(hg) => hg.id,
(hg) => html`
<tr>
${repeat(
hg.headers,
(h) => h.id,
(h) => html`<th>${FlexRender({ header: h })}</th>`,
)}
</tr>
`,
)}
</thead>
<tbody>
${repeat(
table.getRowModel().rows,
(r) => r.id,
(row) => html`
<tr>
${repeat(
row.getAllCells(),
(c) => c.id,
(cell) => html`<td>${FlexRender({ cell })}</td>`,
)}
</tr>
`,
)}
</tbody>
</table>
`
}
}
Source: examples/lit/basic-table-controller/src/main.ts.
Multiple Tables in One Host
Each table needs its own controller. Don't try to share one across instances.
class DashboardElement extends LitElement {
private peopleController = new TableController<
typeof _peopleFeatures,
Person
>(this)
private projectsController = new TableController<
typeof _projectsFeatures,
Project
>(this)
protected render() {
const people = this.peopleController.table({
})
const projects = this.projectsController.table({
})
return html`
<people-section .data=${people}></people-section>
<projects-section .data=${projects}></projects-section>
`
}
}
Reading State Off the Controller
The controller's .table(...) return value carries everything you usually need: feature methods, FlexRender, Subscribe, and the state projection. Direct reads off table.atoms.<slice>.get() and table.state.<slice> are current-value reads; reactivity comes from the host invalidation subscriptions the controller already wires up.
const pagination = table.atoms.pagination.get()
const snapshot = table.state
const selected = table.state
Common Mistakes
CRITICAL Creating a new TableController per render
Wrong:
protected render() {
const controller = new TableController<typeof _features, Person>(this)
const table = controller.table({ })
}
Correct: construct the controller once as a class field.
private tableController = new TableController<typeof _features, Person>(this)
protected render() {
const table = this.tableController.table({ })
}
Each new controller installs another subscription on the host; old controller state is discarded; table state resets every frame.
Source: packages/lit-table/src/TableController.ts.
CRITICAL Calling .table() outside render() and caching the return value
Wrong:
connectedCallback() {
super.connectedCallback()
this.cachedTable = this.tableController.table({ _features, _rowModels: {}, columns, data: this.data })
}
protected render() {
return html`${this.cachedTable.getRowModel().rows.map(/* … */)}`
}
Correct: call .table() each render(). The options are merged into the same logical table on each call, and the returned object carries fresh state/projections.
protected render() {
const table = this.tableController.table({ _features, _rowModels: {}, columns, data: this.data })
return html`${table.getRowModel().rows.map(/* … */)}`
}
Source: packages/lit-table/src/TableController.ts.
HIGH Forgetting that Subscribe re-renders the host on any store change
Wrong: assuming table.Subscribe({ source: table.atoms.rowSelection, … }) makes the host invalidate only on selection changes.
Correct: in the current adapter, the host's requestUpdate() is wired to the full table.store and table.optionsStore. Subscribe is a render-time projection convenience; it does not narrow host invalidation. Plan accordingly for large lists.
Source: packages/lit-table/src/TableController.ts (lines 200–218 + _setupSubscriptions).
HIGH Building _features inside render()
Wrong:
protected render() {
const _features = tableFeatures({ rowSortingFeature })
const table = this.tableController.table({ _features, })
}
Correct: declare _features at module scope (or once on the class, frozen). Identity drives internal memos.
const _features = tableFeatures({ rowSortingFeature })
Source: docs/framework/lit/guide/table-state.md (FAQ #1).
MEDIUM Calling .table() from a non-host context (e.g. a child component)
Wrong: passing the controller down and calling .table() from a different LitElement. The subscriptions belong to the host that constructed the controller — calling from elsewhere is undefined behavior.
Correct: each LitElement that needs its own table builds its own controller. Use createTableHook's useTableContext / useCellContext / useHeaderContext (@lit/context) to access a table from descendant elements.
Source: packages/lit-table/src/createTableHook.ts.
See Also
tanstack-table/lit/table-state — atoms, Subscribe, FlexRender, createTableHook.
tanstack-table/lit/getting-started — first-table walkthrough.
tanstack-table/lit/compose-with-tanstack-virtual — pairing with @tanstack/lit-virtual.