Create a TanStack Ember Table v9 table with useTable, a tracked options thunk, stable tableFeatures and columns, .gts templates, FlexRenderCell/Header/Footer, and correctly bound template helpers. Load for first-table setup, Glimmer reactivity, component cell renderers, or adapting another framework's example to Ember.
70
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
This skill builds on @tanstack/table-core#core and @tanstack/table-core#table-features. Ember Table is headless: it supplies reactive table models and renderer helpers, while the application owns semantic markup, CSS, accessibility, and design-system components.
The v9 addon requires Ember 5.8 or newer with Embroider or ember-auto-import v2. Prefer .gts/.gjs template-tag components with Glint.
import Component from '@glimmer/component'
import { tracked } from '@glimmer/tracking'
import { on } from '@ember/modifier'
import {
FlexRenderCell,
FlexRenderHeader,
createColumnHelper,
tableFeatures,
useTable,
type Cell,
type Row,
} from '@tanstack/ember-table'
type Person = { id: string; name: string }
const features = tableFeatures({})
const columnHelper = createColumnHelper<typeof features, Person>()
const columns = columnHelper.columns([
columnHelper.accessor('name', { header: 'Name' }),
])
const initialData: Person[] = [{ id: '1', name: 'Ada' }]
const getRowId = (row: Person) => row.id
const getAllCells = (
row: Row<typeof features, Person>,
): Array<Cell<typeof features, Person>> => row.getAllCells()
export default class PeopleTable extends Component {
@tracked data = initialData
table = useTable(() => ({
features,
columns,
data: this.data,
getRowId,
}))
get headerGroups() {
return this.table.getHeaderGroups()
}
get rows() {
return this.table.getRowModel().rows
}
addPerson = () => {
this.data = [...this.data, { id: '2', name: 'Grace' }]
}
<template>
<button type='button' {{on 'click' this.addPerson}}>Add person</button>
<table>
<thead>
{{#each this.headerGroups as |group|}}
<tr>
{{#each group.headers as |header|}}
<th colspan={{header.colSpan}}>
{{#unless header.isPlaceholder}}
<FlexRenderHeader @header={{header}} />
{{/unless}}
</th>
{{/each}}
</tr>
{{/each}}
</thead>
<tbody>
{{#each this.rows as |row|}}
<tr>
{{#each (getAllCells row) as |cell|}}
<td><FlexRenderCell @cell={{cell}} /></td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</template>
}useTable takes an options thunk. Tracked properties read by that thunk update table options, and table API reads inside getters/templates participate in Glimmer tracking. Keep features and columns at stable module or component-lifetime scope; replace @tracked data only when data meaningfully changes.
V9 table, column, row, cell, and header methods live on prototypes and require their receiver. Ember templates extract function references, so call methods in a getter or a small module-level helper:
const getCanSort = (column: Column<typeof features, Person>) =>
column.getCanSort()
const toggleSort =
(column: Column<typeof features, Person>) => (event: Event) =>
column.getToggleSortingHandler()?.(event)Pass toggleSort header.column to {{on}}; do not pass an extracted Table method directly.
Use FlexRenderCell, FlexRenderHeader, and FlexRenderFooter with their matching object. A definition may return a primitive or flexRenderComponent(Component, options). The rendered component receives @ctx and optional @options; Table does not instantiate arbitrary component-library markup for you.
Wrong: useTable({ features, columns, data: this.data }).
Correct: useTable(() => ({ features, columns, data: this.data })).
The thunk is how tracked option reads are connected to the table.
Wrong: {{on 'click' header.column.getToggleSortingHandler}}.
Correct: wrap the call in a helper that invokes the method on header.column, as shown above.
An extracted v9 prototype method loses this and can throw or silently target the wrong receiver.
Wrong: columns: columnHelper.columns(...) or data: this.data.slice() inside useTable(() => ...).
Correct: keep columns stable and pass the tracked data reference directly. The thunk may rerun; it should not manufacture new model inputs on every tracked update.
With tableFeatures({}), render row.getAllCells(). Add columnVisibilityFeature before using row.getVisibleCells().
Inspect node_modules/@tanstack/ember-table/declarations/index.d.ts for exports, use-table.d.ts for options/reactivity behavior, FlexRender.d.ts and flex-render.d.ts for renderer contracts, and node_modules/@tanstack/table-core/dist/features/<feature>/ for feature-gated APIs. Do not substitute React hooks, subscriptions, or component signatures.
9e523bc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.