CtrlK
BlogDocsLog inGet started
Tessl Logo

getting-started

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

Quality

87%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

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.

Setup

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.

Ember-Specific Patterns

Preserve method receivers in templates

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.

Render definitions through the matching component

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.

Common Mistakes

HIGH Passing an options object instead of a thunk

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.

HIGH Passing an unbound prototype method to a template modifier

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.

HIGH Recreating model inputs inside the options thunk

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.

MEDIUM Using a visibility-aware API without its feature

With tableFeatures({}), render row.getAllCells(). Add columnVisibilityFeature before using row.getVisibleCells().

API Discovery

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.

Repository
TanStack/table
Last updated
First committed

Is this your skill?

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.