CtrlK
BlogDocsLog inGet started
Tessl Logo

table-state

Use Svelte 5 rune-aware table atoms and stores, $derived projections, reactive option getters, controlled $state or createTableState slices, external atoms, and auto-reset behavior without broad invalidation or snapshot mismatches.

60

Quality

71%

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

Fix and improve this skill with Tessl

tessl review fix ./packages/svelte-table/skills/table-state/SKILL.md
SKILL.md
Quality
Evals
Security

This skill builds on @tanstack/table-core#core and getting-started. Read them first for table ownership and Svelte construction.

State Mental Model

TanStack Table is primarily a state coordinator. Keep state internal unless another system must read, persist, or drive it. Without initialState, atoms, state, or on[State]Change, the table owns all registered slices.

  • table.baseAtoms are internal writable atoms initialized from resolved initial state.
  • table.atoms are readonly derived atoms for the active owner of each registered slice.
  • table.store is the readonly flat store assembled from those atoms.

The Svelte adapter bridges TanStack Store dependency tracking into Svelte runes. table.atoms.<slice>.get(), table.store.get(), and table APIs become reactive when called in a template, $derived, $derived.by, or $effect. Outside those contexts they return current snapshots.

Only registered features create state and types. If pagination is missing, register rowPaginationFeature; do not add a cast or ad hoc state field. Keep features and columns stable and pass changing data through a getter.

Setup

Keep state internal unless another subsystem needs to own it. Read only the state slices a component needs.

<script lang="ts">
  import {
    createTable,
    rowPaginationFeature,
    tableFeatures,
  } from '@tanstack/svelte-table'

  const features = tableFeatures({ rowPaginationFeature })
  const columns = [{ accessorKey: 'name' }]
  let data = $state([{ name: 'Ada' }])

  const table = createTable({
    features,
    columns,
    get data() {
      return data
    },
  })

  const pagination = $derived(table.atoms.pagination.get())
</script>

<button onclick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
  Page {pagination.pageIndex + 1}
</button>

Core Patterns

Read narrow or complete state

const pagination = $derived(table.atoms.pagination.get())
const pageIndex = $derived(table.atoms.pagination.get().pageIndex)
const rows = $derived(table.getRowModel().rows)
const stateJson = $derived(JSON.stringify(table.store.get(), null, 2))

Use atom reads for normal UI. A table.store.get() read intentionally re-runs for any registered state change, so reserve it for debug output, persistence, or computations that need the whole state.

Control a slice with value-or-updater semantics

import type { PaginationState, Updater } from '@tanstack/svelte-table'

let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 20 })
const updatePagination = (next: Updater<PaginationState>) => {
  pagination = typeof next === 'function' ? next(pagination) : next
}

Pass a getter-backed state.pagination and onPaginationChange: updatePagination to createTable.

Reduce boilerplate with createTableState

import {
  createTable,
  createTableState,
  rowPaginationFeature,
  tableFeatures,
  type PaginationState,
} from '@tanstack/svelte-table'

const features = tableFeatures({ rowPaginationFeature })
const columns = [{ accessorKey: 'name' }]
const data = [{ name: 'Ada' }]
const [pagination, setPagination] = createTableState<PaginationState>({
  pageIndex: 0,
  pageSize: 20,
})

const table = createTable({
  features,
  columns,
  data,
  state: {
    get pagination() {
      return pagination()
    },
  },
  onPaginationChange: setPagination,
})

For better or worse, this resembles a small React useState hook: pagination() reads the current rune-backed value, while setPagination accepts either a value or a functional updater and can be passed directly to onPaginationChange.

Choose State Ownership

Use one owner per slice:

  • Prefer internal state plus feature APIs for table-local interaction.
  • Use initialState for starting/reset values; changing it later does not reset state.
  • Use Svelte $state, a getter-backed state entry, and the matching callback for normal Svelte-owned controlled state.
  • Use a stable external atom in atoms for state shared as a raw TanStack Store atom. Do not also add its change callback.

External atoms win over controlled state, which syncs into the internal base atom. Avoid multiple owners. The global v8 onStateChange option is gone; subscribe to table.store if every state change must be observed imperatively.

When code outside the table consumes a raw external atom, use useSelector from @tanstack/svelte-store. Inside table-driven UI, read the rune-aware table.atoms.<slice>.get() wrapper.

Initialize, Update, and Reset

Prefer setSorting, nextPage, toggleVisibility, toggleSelected, and other feature APIs over direct state writes. Write a base atom only for rare internal-state needs; write the external atom when atoms.<slice> owns it.

table.resetSorting()
table.resetPagination()
table.resetPagination(true)

Feature resets use table.initialState unless true requests the feature default and can flow to external owners. Core table.reset() resets internal base atoms only. Use feature types such as PaginationState for a slice and TableState<typeof features> for the complete registered state.

Common Mistakes

HIGH Keeping removed adapter selectors

Wrong:

const table = createTable(options, (state) => state.pagination)
const pageIndex = table.state.pageIndex

Correct:

const table = createTable(options)
const pagination = $derived(table.atoms.pagination.get())

Starting in beta.59, createTable and createAppTable take only options, table.state is absent, and subscribeTable and SubscribeSource are no longer exported. Use native tracked Svelte reads and $derived projections.

Source: docs/framework/svelte/guide/migrating.md

HIGH Controlling without writing back

Wrong:

const options = { state: { pagination }, onPaginationChange: console.log }

Correct:

const options = {
  state: {
    get pagination() {
      return pagination
    },
  },
  onPaginationChange: updatePagination,
}

A controlled slice is frozen unless every updater is resolved into the owning rune.

Source: docs/framework/svelte/guide/table-state.md

HIGH Snapshotting outside tracking

Wrong:

const pageIndex = table.store.get().pagination.pageIndex

Correct inside a component:

const pageIndex = $derived(table.atoms.pagination.get().pageIndex)

The first line is only a current snapshot when it runs outside a template or rune. The second line is a narrow native Svelte derivation.

Source: packages/svelte-table/src/createTable.svelte.ts

MEDIUM Declaring one slice in two owners

Wrong:

const options = { initialState: { pagination: start }, state: { pagination } }

Correct:

const options = {
  state: {
    get pagination() {
      return pagination
    },
  },
}

Controlled atoms or state wins over initialState; choose one owner per slice.

Source: docs/framework/svelte/guide/table-state.md

MEDIUM Fighting automatic page reset

Wrong:

table.setPageIndex(4)
data = filteredData

Correct:

const options = { autoResetPageIndex: false }

Client row-model changes reset the page by default; disable it only when the application handles invalid empty pages.

Source: docs/framework/svelte/guide/pagination.md

API Discovery

Inspect node_modules/@tanstack/svelte-table/dist/createTable.svelte.d.ts, createTableHook.svelte.d.ts, and createTableState.svelte.d.ts; inspect registered state slices in the matching core feature source.

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.