From 10b7484f23ab51b81750ff477b055c9e97cb78c3 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Fri, 10 Jul 2026 19:54:05 -0500 Subject: [PATCH 1/4] feat: regenerate skills --- .github/workflows/check-skills.yml | 17 +- .github/workflows/release.yml | 10 +- _artifacts/domain_map.yaml | 8433 +++-------------- .../scratch/cluster-a-foundational.yaml | 672 -- .../scratch/cluster-b-row-model-features.yaml | 1188 --- .../scratch/cluster-c-ui-state-features.yaml | 1110 --- .../scratch/cluster-d-framework-adapters.yaml | 990 -- .../cluster-e-lifecycle-composition.yaml | 1396 --- .../cluster-f-issues-failure-modes.yaml | 1173 --- _artifacts/skill_spec.md | 555 +- _artifacts/skill_tree.yaml | 2572 ++--- .../framework/alpine/guide/column-resizing.md | 4 +- .../angular/guide/column-resizing.md | 4 +- docs/framework/angular/guide/migrating.md | 12 +- docs/framework/lit/guide/column-resizing.md | 4 +- .../framework/preact/guide/column-resizing.md | 4 +- docs/framework/react/guide/column-resizing.md | 4 +- docs/framework/react/guide/migrating.md | 12 +- docs/framework/solid/guide/column-resizing.md | 4 +- docs/framework/solid/guide/migrating.md | 2 +- .../framework/svelte/guide/column-resizing.md | 4 +- docs/framework/vue/guide/column-resizing.md | 4 +- docs/framework/vue/guide/migrating.md | 2 +- package.json | 5 + packages/alpine-table/package.json | 6 +- .../skills/create-table-hook/SKILL.md | 98 + .../skills/getting-started/SKILL.md | 108 + .../alpine-table/skills/table-state/SKILL.md | 136 + packages/angular-table-devtools/package.json | 6 +- .../skills/devtools/SKILL.md | 87 + .../angular-rendering-directives/SKILL.md | 415 - .../references/content-shapes.md | 142 - .../create-table-hook-registries.md | 89 - .../references/di-tokens.md | 171 - .../flex-render-component-options.md | 64 - .../skills/angular/client-to-server/SKILL.md | 468 - .../compose-with-tanstack-query/SKILL.md | 486 - .../compose-with-tanstack-store/SKILL.md | 405 - .../compose-with-tanstack-virtual/SKILL.md | 399 - .../skills/angular/getting-started/SKILL.md | 487 - .../references/feature-row-model-mapping.md | 51 - .../skills/angular/migrate-v8-to-v9/SKILL.md | 422 - .../references/v8-to-v9-mapping.md | 268 - .../angular/production-readiness/SKILL.md | 472 - .../skills/angular/table-state/SKILL.md | 422 - .../references/external-atoms-and-app-hook.md | 153 - .../skills/create-table-hook/SKILL.md | 148 + .../skills/getting-started/SKILL.md | 138 + .../skills/migrate-v8-to-v9/SKILL.md | 194 + .../angular-table/skills/table-state/SKILL.md | 197 + .../skills/with-tanstack-query/SKILL.md | 142 + .../skills/with-tanstack-virtual/SKILL.md | 126 + .../skills/create-table-hook/SKILL.md | 94 + .../lit-table/skills/getting-started/SKILL.md | 154 + .../compose-with-tanstack-virtual/SKILL.md | 267 - .../skills/lit/getting-started/SKILL.md | 343 - .../skills/lit/lit-table-controller/SKILL.md | 311 - .../skills/lit/migrate-v8-to-v9/SKILL.md | 309 - .../lit-table/skills/lit/table-state/SKILL.md | 475 - .../skills/migrate-v8-to-v9/SKILL.md | 198 + .../lit-table/skills/table-state/SKILL.md | 155 + .../skills/with-tanstack-virtual/SKILL.md | 147 + packages/match-sorter-utils/package.json | 6 +- .../skills/fuzzy-ranking/SKILL.md | 94 + .../skills/devtools/SKILL.md | 85 + .../compose-with-tanstack-devtools/SKILL.md | 161 - .../skills/create-table-hook/SKILL.md | 176 + .../skills/getting-started/SKILL.md | 157 + .../skills/migrate-v8-to-v9/SKILL.md | 191 + .../skills/preact/client-to-server/SKILL.md | 287 - .../compose-with-tanstack-form/SKILL.md | 228 - .../compose-with-tanstack-pacer/SKILL.md | 186 - .../compose-with-tanstack-query/SKILL.md | 285 - .../compose-with-tanstack-store/SKILL.md | 259 - .../compose-with-tanstack-virtual/SKILL.md | 278 - .../skills/preact/getting-started/SKILL.md | 372 - .../skills/preact/migrate-v8-to-v9/SKILL.md | 309 - .../preact/production-readiness/SKILL.md | 274 - .../skills/preact/table-state/SKILL.md | 422 - .../references/advanced-state-patterns.md | 92 - .../preact-table/skills/table-state/SKILL.md | 155 + .../skills/with-tanstack-query/SKILL.md | 135 + .../skills/with-tanstack-virtual/SKILL.md | 118 + .../skills/devtools/SKILL.md | 85 + .../compose-with-tanstack-devtools/SKILL.md | 172 - .../skills/create-table-hook/SKILL.md | 182 + .../skills/getting-started/SKILL.md | 171 + .../skills/migrate-v8-to-v9/SKILL.md | 224 + .../skills/react/client-to-server/SKILL.md | 380 - .../react/compose-with-tanstack-form/SKILL.md | 353 - .../compose-with-tanstack-pacer/SKILL.md | 287 - .../compose-with-tanstack-query/SKILL.md | 469 - .../compose-with-tanstack-store/SKILL.md | 339 - .../compose-with-tanstack-virtual/SKILL.md | 392 - ...lumn-virtualization-and-infinite-scroll.md | 143 - .../skills/react/getting-started/SKILL.md | 394 - .../skills/react/migrate-v8-to-v9/SKILL.md | 498 - .../react/production-readiness/SKILL.md | 349 - .../react-subscribe-compiler-compat/SKILL.md | 269 - .../skills/react/table-state/SKILL.md | 425 - .../react-table/skills/table-state/SKILL.md | 255 + .../skills/with-tanstack-query/SKILL.md | 186 + .../skills/with-tanstack-virtual/SKILL.md | 197 + .../skills/devtools/SKILL.md | 83 + .../compose-with-tanstack-devtools/SKILL.md | 176 - .../skills/create-table-hook/SKILL.md | 122 + .../skills/getting-started/SKILL.md | 175 + .../skills/migrate-v8-to-v9/SKILL.md | 211 + .../skills/solid/client-to-server/SKILL.md | 241 - .../solid/compose-with-tanstack-form/SKILL.md | 320 - .../compose-with-tanstack-pacer/SKILL.md | 209 - .../compose-with-tanstack-query/SKILL.md | 289 - .../compose-with-tanstack-store/SKILL.md | 237 - .../compose-with-tanstack-virtual/SKILL.md | 288 - .../skills/solid/getting-started/SKILL.md | 276 - .../skills/solid/migrate-v8-to-v9/SKILL.md | 324 - .../solid/production-readiness/SKILL.md | 259 - .../skills/solid/table-state/SKILL.md | 426 - .../solid-table/skills/table-state/SKILL.md | 157 + .../skills/with-tanstack-query/SKILL.md | 149 + .../skills/with-tanstack-virtual/SKILL.md | 175 + .../skills/create-table-hook/SKILL.md | 168 + .../skills/getting-started/SKILL.md | 172 + .../skills/migrate-v8-to-v9/SKILL.md | 195 + .../skills/svelte/client-to-server/SKILL.md | 236 - .../compose-with-tanstack-form/SKILL.md | 294 - .../compose-with-tanstack-pacer/SKILL.md | 176 - .../compose-with-tanstack-query/SKILL.md | 296 - .../compose-with-tanstack-store/SKILL.md | 270 - .../compose-with-tanstack-virtual/SKILL.md | 289 - .../skills/svelte/getting-started/SKILL.md | 338 - .../skills/svelte/migrate-v8-to-v9/SKILL.md | 264 - .../svelte/production-readiness/SKILL.md | 258 - .../skills/svelte/table-state/SKILL.md | 437 - .../svelte-table/skills/table-state/SKILL.md | 205 + .../skills/with-tanstack-query/SKILL.md | 147 + .../skills/with-tanstack-virtual/SKILL.md | 150 + .../table-core/skills/api-not-found/SKILL.md | 113 + .../skills/client-vs-server/SKILL.md | 164 + .../skills/column-definitions/SKILL.md | 330 - .../skills/column-faceting/SKILL.md | 91 + .../skills/column-filtering/SKILL.md | 82 + .../table-core/skills/column-layout/SKILL.md | 326 - .../column-layout/references/subsystems.md | 220 - .../skills/column-ordering/SKILL.md | 75 + .../table-core/skills/column-pinning/SKILL.md | 89 + .../skills/column-resizing/SKILL.md | 91 + .../table-core/skills/column-sizing/SKILL.md | 72 + .../skills/column-visibility/SKILL.md | 75 + packages/table-core/skills/core/SKILL.md | 140 + .../skills/custom-features/SKILL.md | 207 + .../customizing-feature-behavior/SKILL.md | 423 - packages/table-core/skills/expanding/SKILL.md | 80 + packages/table-core/skills/filtering/SKILL.md | 375 - .../references/faceting-and-fuzzy.md | 218 - .../skills/global-filtering/SKILL.md | 84 + packages/table-core/skills/grouping/SKILL.md | 444 +- .../skills/migrate-v8-to-v9/SKILL.md | 620 +- .../table-core/skills/pagination/SKILL.md | 379 +- .../table-core/skills/row-expanding/SKILL.md | 356 - .../table-core/skills/row-pinning/SKILL.md | 285 +- .../table-core/skills/row-selection/SKILL.md | 390 +- packages/table-core/skills/setup/SKILL.md | 390 - packages/table-core/skills/sorting/SKILL.md | 334 +- .../skills/state-management/SKILL.md | 403 - .../table-core/skills/table-features/SKILL.md | 153 + .../table-core/skills/typescript/SKILL.md | 126 + .../compose-with-tanstack-devtools/SKILL.md | 103 - .../table-devtools/skills/devtools/SKILL.md | 90 + .../skills/devtools/SKILL.md | 81 + .../compose-with-tanstack-devtools/SKILL.md | 180 - .../skills/create-table-hook/SKILL.md | 146 + .../vue-table/skills/getting-started/SKILL.md | 144 + .../skills/migrate-v8-to-v9/SKILL.md | 185 + .../vue-table/skills/table-state/SKILL.md | 191 + .../skills/vue/client-to-server/SKILL.md | 360 - .../vue/compose-with-tanstack-form/SKILL.md | 369 - .../vue/compose-with-tanstack-pacer/SKILL.md | 321 - .../vue/compose-with-tanstack-query/SKILL.md | 383 - .../vue/compose-with-tanstack-store/SKILL.md | 302 - .../compose-with-tanstack-virtual/SKILL.md | 344 - .../skills/vue/getting-started/SKILL.md | 415 - .../skills/vue/migrate-v8-to-v9/SKILL.md | 393 - .../skills/vue/production-readiness/SKILL.md | 278 - .../vue-table/skills/vue/table-state/SKILL.md | 399 - .../skills/with-tanstack-query/SKILL.md | 127 + .../skills/with-tanstack-virtual/SKILL.md | 116 + scripts/sync-skill-versions.mjs | 122 + scripts/typecheck-skill-snippets.mjs | 131 + scripts/validate-skill-content.mjs | 173 + skills/github-issue-repro-and-fix/SKILL.md | 90 - 191 files changed, 13117 insertions(+), 44447 deletions(-) delete mode 100644 _artifacts/scratch/cluster-a-foundational.yaml delete mode 100644 _artifacts/scratch/cluster-b-row-model-features.yaml delete mode 100644 _artifacts/scratch/cluster-c-ui-state-features.yaml delete mode 100644 _artifacts/scratch/cluster-d-framework-adapters.yaml delete mode 100644 _artifacts/scratch/cluster-e-lifecycle-composition.yaml delete mode 100644 _artifacts/scratch/cluster-f-issues-failure-modes.yaml create mode 100644 packages/alpine-table/skills/create-table-hook/SKILL.md create mode 100644 packages/alpine-table/skills/getting-started/SKILL.md create mode 100644 packages/alpine-table/skills/table-state/SKILL.md create mode 100644 packages/angular-table-devtools/skills/devtools/SKILL.md delete mode 100644 packages/angular-table/skills/angular/angular-rendering-directives/SKILL.md delete mode 100644 packages/angular-table/skills/angular/angular-rendering-directives/references/content-shapes.md delete mode 100644 packages/angular-table/skills/angular/angular-rendering-directives/references/create-table-hook-registries.md delete mode 100644 packages/angular-table/skills/angular/angular-rendering-directives/references/di-tokens.md delete mode 100644 packages/angular-table/skills/angular/angular-rendering-directives/references/flex-render-component-options.md delete mode 100644 packages/angular-table/skills/angular/client-to-server/SKILL.md delete mode 100644 packages/angular-table/skills/angular/compose-with-tanstack-query/SKILL.md delete mode 100644 packages/angular-table/skills/angular/compose-with-tanstack-store/SKILL.md delete mode 100644 packages/angular-table/skills/angular/compose-with-tanstack-virtual/SKILL.md delete mode 100644 packages/angular-table/skills/angular/getting-started/SKILL.md delete mode 100644 packages/angular-table/skills/angular/getting-started/references/feature-row-model-mapping.md delete mode 100644 packages/angular-table/skills/angular/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/angular-table/skills/angular/migrate-v8-to-v9/references/v8-to-v9-mapping.md delete mode 100644 packages/angular-table/skills/angular/production-readiness/SKILL.md delete mode 100644 packages/angular-table/skills/angular/table-state/SKILL.md delete mode 100644 packages/angular-table/skills/angular/table-state/references/external-atoms-and-app-hook.md create mode 100644 packages/angular-table/skills/create-table-hook/SKILL.md create mode 100644 packages/angular-table/skills/getting-started/SKILL.md create mode 100644 packages/angular-table/skills/migrate-v8-to-v9/SKILL.md create mode 100644 packages/angular-table/skills/table-state/SKILL.md create mode 100644 packages/angular-table/skills/with-tanstack-query/SKILL.md create mode 100644 packages/angular-table/skills/with-tanstack-virtual/SKILL.md create mode 100644 packages/lit-table/skills/create-table-hook/SKILL.md create mode 100644 packages/lit-table/skills/getting-started/SKILL.md delete mode 100644 packages/lit-table/skills/lit/compose-with-tanstack-virtual/SKILL.md delete mode 100644 packages/lit-table/skills/lit/getting-started/SKILL.md delete mode 100644 packages/lit-table/skills/lit/lit-table-controller/SKILL.md delete mode 100644 packages/lit-table/skills/lit/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/lit-table/skills/lit/table-state/SKILL.md create mode 100644 packages/lit-table/skills/migrate-v8-to-v9/SKILL.md create mode 100644 packages/lit-table/skills/table-state/SKILL.md create mode 100644 packages/lit-table/skills/with-tanstack-virtual/SKILL.md create mode 100644 packages/match-sorter-utils/skills/fuzzy-ranking/SKILL.md create mode 100644 packages/preact-table-devtools/skills/devtools/SKILL.md delete mode 100644 packages/preact-table-devtools/skills/preact/compose-with-tanstack-devtools/SKILL.md create mode 100644 packages/preact-table/skills/create-table-hook/SKILL.md create mode 100644 packages/preact-table/skills/getting-started/SKILL.md create mode 100644 packages/preact-table/skills/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/preact-table/skills/preact/client-to-server/SKILL.md delete mode 100644 packages/preact-table/skills/preact/compose-with-tanstack-form/SKILL.md delete mode 100644 packages/preact-table/skills/preact/compose-with-tanstack-pacer/SKILL.md delete mode 100644 packages/preact-table/skills/preact/compose-with-tanstack-query/SKILL.md delete mode 100644 packages/preact-table/skills/preact/compose-with-tanstack-store/SKILL.md delete mode 100644 packages/preact-table/skills/preact/compose-with-tanstack-virtual/SKILL.md delete mode 100644 packages/preact-table/skills/preact/getting-started/SKILL.md delete mode 100644 packages/preact-table/skills/preact/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/preact-table/skills/preact/production-readiness/SKILL.md delete mode 100644 packages/preact-table/skills/preact/table-state/SKILL.md delete mode 100644 packages/preact-table/skills/preact/table-state/references/advanced-state-patterns.md create mode 100644 packages/preact-table/skills/table-state/SKILL.md create mode 100644 packages/preact-table/skills/with-tanstack-query/SKILL.md create mode 100644 packages/preact-table/skills/with-tanstack-virtual/SKILL.md create mode 100644 packages/react-table-devtools/skills/devtools/SKILL.md delete mode 100644 packages/react-table-devtools/skills/react/compose-with-tanstack-devtools/SKILL.md create mode 100644 packages/react-table/skills/create-table-hook/SKILL.md create mode 100644 packages/react-table/skills/getting-started/SKILL.md create mode 100644 packages/react-table/skills/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/react-table/skills/react/client-to-server/SKILL.md delete mode 100644 packages/react-table/skills/react/compose-with-tanstack-form/SKILL.md delete mode 100644 packages/react-table/skills/react/compose-with-tanstack-pacer/SKILL.md delete mode 100644 packages/react-table/skills/react/compose-with-tanstack-query/SKILL.md delete mode 100644 packages/react-table/skills/react/compose-with-tanstack-store/SKILL.md delete mode 100644 packages/react-table/skills/react/compose-with-tanstack-virtual/SKILL.md delete mode 100644 packages/react-table/skills/react/compose-with-tanstack-virtual/references/column-virtualization-and-infinite-scroll.md delete mode 100644 packages/react-table/skills/react/getting-started/SKILL.md delete mode 100644 packages/react-table/skills/react/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/react-table/skills/react/production-readiness/SKILL.md delete mode 100644 packages/react-table/skills/react/react-subscribe-compiler-compat/SKILL.md delete mode 100644 packages/react-table/skills/react/table-state/SKILL.md create mode 100644 packages/react-table/skills/table-state/SKILL.md create mode 100644 packages/react-table/skills/with-tanstack-query/SKILL.md create mode 100644 packages/react-table/skills/with-tanstack-virtual/SKILL.md create mode 100644 packages/solid-table-devtools/skills/devtools/SKILL.md delete mode 100644 packages/solid-table-devtools/skills/solid/compose-with-tanstack-devtools/SKILL.md create mode 100644 packages/solid-table/skills/create-table-hook/SKILL.md create mode 100644 packages/solid-table/skills/getting-started/SKILL.md create mode 100644 packages/solid-table/skills/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/solid-table/skills/solid/client-to-server/SKILL.md delete mode 100644 packages/solid-table/skills/solid/compose-with-tanstack-form/SKILL.md delete mode 100644 packages/solid-table/skills/solid/compose-with-tanstack-pacer/SKILL.md delete mode 100644 packages/solid-table/skills/solid/compose-with-tanstack-query/SKILL.md delete mode 100644 packages/solid-table/skills/solid/compose-with-tanstack-store/SKILL.md delete mode 100644 packages/solid-table/skills/solid/compose-with-tanstack-virtual/SKILL.md delete mode 100644 packages/solid-table/skills/solid/getting-started/SKILL.md delete mode 100644 packages/solid-table/skills/solid/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/solid-table/skills/solid/production-readiness/SKILL.md delete mode 100644 packages/solid-table/skills/solid/table-state/SKILL.md create mode 100644 packages/solid-table/skills/table-state/SKILL.md create mode 100644 packages/solid-table/skills/with-tanstack-query/SKILL.md create mode 100644 packages/solid-table/skills/with-tanstack-virtual/SKILL.md create mode 100644 packages/svelte-table/skills/create-table-hook/SKILL.md create mode 100644 packages/svelte-table/skills/getting-started/SKILL.md create mode 100644 packages/svelte-table/skills/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/client-to-server/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/compose-with-tanstack-form/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/compose-with-tanstack-pacer/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/compose-with-tanstack-query/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/compose-with-tanstack-store/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/compose-with-tanstack-virtual/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/getting-started/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/production-readiness/SKILL.md delete mode 100644 packages/svelte-table/skills/svelte/table-state/SKILL.md create mode 100644 packages/svelte-table/skills/table-state/SKILL.md create mode 100644 packages/svelte-table/skills/with-tanstack-query/SKILL.md create mode 100644 packages/svelte-table/skills/with-tanstack-virtual/SKILL.md create mode 100644 packages/table-core/skills/api-not-found/SKILL.md create mode 100644 packages/table-core/skills/client-vs-server/SKILL.md delete mode 100644 packages/table-core/skills/column-definitions/SKILL.md create mode 100644 packages/table-core/skills/column-faceting/SKILL.md create mode 100644 packages/table-core/skills/column-filtering/SKILL.md delete mode 100644 packages/table-core/skills/column-layout/SKILL.md delete mode 100644 packages/table-core/skills/column-layout/references/subsystems.md create mode 100644 packages/table-core/skills/column-ordering/SKILL.md create mode 100644 packages/table-core/skills/column-pinning/SKILL.md create mode 100644 packages/table-core/skills/column-resizing/SKILL.md create mode 100644 packages/table-core/skills/column-sizing/SKILL.md create mode 100644 packages/table-core/skills/column-visibility/SKILL.md create mode 100644 packages/table-core/skills/core/SKILL.md create mode 100644 packages/table-core/skills/custom-features/SKILL.md delete mode 100644 packages/table-core/skills/customizing-feature-behavior/SKILL.md create mode 100644 packages/table-core/skills/expanding/SKILL.md delete mode 100644 packages/table-core/skills/filtering/SKILL.md delete mode 100644 packages/table-core/skills/filtering/references/faceting-and-fuzzy.md create mode 100644 packages/table-core/skills/global-filtering/SKILL.md delete mode 100644 packages/table-core/skills/row-expanding/SKILL.md delete mode 100644 packages/table-core/skills/setup/SKILL.md delete mode 100644 packages/table-core/skills/state-management/SKILL.md create mode 100644 packages/table-core/skills/table-features/SKILL.md create mode 100644 packages/table-core/skills/typescript/SKILL.md delete mode 100644 packages/table-devtools/skills/compose-with-tanstack-devtools/SKILL.md create mode 100644 packages/table-devtools/skills/devtools/SKILL.md create mode 100644 packages/vue-table-devtools/skills/devtools/SKILL.md delete mode 100644 packages/vue-table-devtools/skills/vue/compose-with-tanstack-devtools/SKILL.md create mode 100644 packages/vue-table/skills/create-table-hook/SKILL.md create mode 100644 packages/vue-table/skills/getting-started/SKILL.md create mode 100644 packages/vue-table/skills/migrate-v8-to-v9/SKILL.md create mode 100644 packages/vue-table/skills/table-state/SKILL.md delete mode 100644 packages/vue-table/skills/vue/client-to-server/SKILL.md delete mode 100644 packages/vue-table/skills/vue/compose-with-tanstack-form/SKILL.md delete mode 100644 packages/vue-table/skills/vue/compose-with-tanstack-pacer/SKILL.md delete mode 100644 packages/vue-table/skills/vue/compose-with-tanstack-query/SKILL.md delete mode 100644 packages/vue-table/skills/vue/compose-with-tanstack-store/SKILL.md delete mode 100644 packages/vue-table/skills/vue/compose-with-tanstack-virtual/SKILL.md delete mode 100644 packages/vue-table/skills/vue/getting-started/SKILL.md delete mode 100644 packages/vue-table/skills/vue/migrate-v8-to-v9/SKILL.md delete mode 100644 packages/vue-table/skills/vue/production-readiness/SKILL.md delete mode 100644 packages/vue-table/skills/vue/table-state/SKILL.md create mode 100644 packages/vue-table/skills/with-tanstack-query/SKILL.md create mode 100644 packages/vue-table/skills/with-tanstack-virtual/SKILL.md create mode 100644 scripts/sync-skill-versions.mjs create mode 100644 scripts/typecheck-skill-snippets.mjs create mode 100644 scripts/validate-skill-content.mjs delete mode 100644 skills/github-issue-repro-and-fix/SKILL.md diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml index 8675617d1c..6f6fc410f7 100644 --- a/.github/workflows/check-skills.yml +++ b/.github/workflows/check-skills.yml @@ -16,6 +16,12 @@ on: - '**/skills/**' - '_artifacts/**' - '**/_artifacts/**' + - 'scripts/sync-skill-versions.mjs' + - 'scripts/typecheck-skill-snippets.mjs' + - 'scripts/validate-skill-content.mjs' + - 'package.json' + - 'pnpm-lock.yaml' + - '.github/workflows/check-skills.yml' workflow_dispatch: {} permissions: @@ -36,8 +42,11 @@ jobs: with: node-version: 20 - - name: Install intent - run: npm install -g @tanstack/intent + - name: Enable pnpm + run: corepack enable - - name: Validate skills - run: intent validate --github-summary + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Validate skills and examples + run: pnpm test:skills diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7e8603435..980dea47d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,10 +54,18 @@ jobs: git switch -c "$branch" git push --set-upstream origin "$branch" echo "branch=$branch" >> "$GITHUB_OUTPUT" - - name: Publish + - name: Prepare package versions run: | git config --global user.name 'github-actions[bot]' git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + env -u CI pnpm run cipublish + pnpm run skills:versions:fix + pnpm run skills:versions:check + env: + BRANCH: ${{ github.ref_name }} + TAG: ${{ inputs.tag }} + - name: Publish + run: | pnpm run cipublish env: BRANCH: ${{ github.ref_name }} diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml index 2efb06607a..562ec3d7d7 100644 --- a/_artifacts/domain_map.yaml +++ b/_artifacts/domain_map.yaml @@ -1,7110 +1,1571 @@ library: name: '@tanstack/table' - version: 9.0.0-alpha.48 - repository: https://github.com/TanStack/table - homepage: https://tanstack.com/table - description: - 'Headless data-grid library: features, state, and APIs for building powerful, type-safe - tables and datagrids while keeping full control of markup, styles, and behavior. Framework-agnostic - core (`@tanstack/table-core`) with adapters for React, Vue, Solid, Svelte, Angular, Lit, and Preact.' - primary_framework: framework-agnostic + version: '9.0.0-beta.38' + repository: 'https://github.com/TanStack/table' + homepage: 'https://tanstack.com/table' + description: 'Headless, framework-agnostic data-grid state and row-processing primitives with tree-shakeable v9 features and framework adapters.' + primary_framework: 'framework-agnostic' monorepo: true + package_version_overrides: + '@tanstack/angular-table-devtools': '9.0.0-beta.40' + '@tanstack/match-sorter-utils': '9.0.0-beta.37' + '@tanstack/preact-table-devtools': '9.0.0-beta.40' + '@tanstack/react-table-devtools': '9.0.0-beta.40' + '@tanstack/solid-table-devtools': '9.0.0-beta.40' + '@tanstack/table-devtools': '9.0.0-beta.40' + '@tanstack/vue-table': '9.0.0-beta.40' + '@tanstack/vue-table-devtools': '9.0.0-beta.40' + meta: - generated_by: '@tanstack/intent scaffold (Phase 3 deep-read merge)' - date: '2026-05-17' + generated_by: '@tanstack/intent scaffold domain discovery' + date: '2026-07-10' status: reviewed maintainer_review_pending: false - phase_4_date: '2026-05-17' + phase_4_date: '2026-07-10' + release_voice: 'Treat v9 as stable guidance while recording the exact prerelease package version in skill metadata.' + +scope: + goals: + - 'Route agents to the correct package and skill, then correct common foot-guns and misconceptions.' + - 'Use docs and examples for intended workflows, and direct exact API discovery to the installed package src directory.' + - 'Teach headless rendering, feature registration, row-model ownership, framework state, and v8-to-v9 changes as foundations.' + included: + - 'All 16 public workspace packages.' + - 'All 14 stock Table v9 features, custom features, TypeScript helpers, migrations, framework state, composable table hooks, Query, Virtual, Devtools, and fuzzy ranking.' + - 'Renderer-owned CSS edge cases for sticky positioning, column widths, resizing, layout, and virtualization.' + excluded: + - 'Component-library-specific integrations such as shadcn, Material UI, Mantine, and equivalent design systems.' + - 'Worker row models and experimental worker guidance.' + - 'A dedicated performance skill; performance guidance is placed where the relevant state, row model, or virtualization decision is made.' + - 'A promoted useLegacyTable workflow; it may only be identified as a deprecated temporary stopgap when encountered.' + +validation_contract: + - 'Every generated skill must pass Intent validation and exact package-version synchronization.' + - 'Load-bearing TypeScript examples marked with skill-snippet:check must compile against workspace source and their package tsconfig.' + - 'Content checks reject malformed Markdown tables, invented package imports, feature-gated APIs without their feature, unstable empty-array data getters, and adapter-specific corrected foot-guns.' + - 'Wrong/Correct pairs are reserved for code where Wrong actually misbehaves; valid defaults and supported alternatives must use decision-oriented prose instead.' + - 'Virtual integration patterns must follow the maintained adapter guide/example rather than borrowing another framework adapter API.' + - 'Release automation updates skill and artifact version metadata after package versions are calculated and before packages publish.' + +migration_depth_contract: + exception: 'Migration skills are intentionally comprehensive rather than bare-bones. Each adapter migration skill must stand alone and list every breaking change in its maintained v8-to-v9 guide, even when the same change also appears in the table-core migration skill.' + required_coverage: + - 'Framework package and construction entrypoint changes, including framework-version prerequisites.' + - 'Logical start/end column-pinning rename and the full old-to-new API mapping.' + - 'Prototype-bound row, cell, column, and header methods; object-spread/Object.keys/JSON implications.' + - 'Required tableFeatures registration, stockFeatures audit guidance, all 14 feature imports, and feature-gated state/APIs.' + - 'Core row model removal plus every get*RowModel to create*RowModel feature-slot mapping.' + - 'filterFns, sortFns, aggregationFns, and filterMeta registry-slot migration.' + - 'Adapter state access, selectors/subscriptions, controlled slices, external atoms, precedence, and onStateChange removal.' + - 'createColumnHelper TFeatures and columns() inference changes.' + - 'Framework rendering helper/component/directive changes.' + - 'tableOptions and createTableHook composability.' + - 'enablePinning split, columnSizing/columnResizing split and state/API renames.' + - 'Sorting option/API/type/built-in registry renames.' + - 'Removed underscore-prefixed internal APIs and public row API replacements.' + - 'Row-selection some-selected semantic changes and correct indeterminate checks.' + - 'TFeatures generic changes, StockFeatures typing, meta typing, function-registry augmentation replacement, and RowData restriction.' + - 'A final exhaustive migration checklist suitable for auditing an existing codebase.' + +table_state_depth_contract: + exception: 'Framework table-state skills are foundational and intentionally richer than ordinary feature skills. Preserve the guide mental model and adapter-specific reactivity; do not reduce them to a few controlled-state snippets.' + required_coverage: + - 'Table as a state coordinator and internal state as the default ownership choice.' + - 'Feature-gated state slices and the relationship between registered features, options, APIs, atoms, and inferred types.' + - 'The distinct baseAtoms, atoms, store, and adapter-selected state surfaces.' + - 'Current snapshot reads versus framework-reactive subscriptions or tracked reads.' + - 'Exactly one owner per slice: internal state, initialState, external atoms, or state plus on[State]Change.' + - 'External-atom precedence, controlled-state synchronization, updater-function handling, and global onStateChange removal.' + - 'Feature methods as the preferred write surface, baseAtoms as a low-level escape hatch, and external-atom writes when externally owned.' + - 'initialState timing, feature reset behavior, reset-to-default arguments, and core reset limitations with external atoms.' + - 'Feature-specific state types and TableState inference.' + - 'Adapter-specific selectors, subscriptions, compiler/reactivity boundaries, and option synchronization.' + +stable_model_input_contract: + rule: 'Every skill example must keep data and columns references stable between meaningful changes. Never recommend inline map/filter/slice chains, makeColumns calls, or fresh fallback arrays in repeatedly evaluated table options.' + accepted_patterns: + - 'Module or component-lifetime constants for static data and columns.' + - 'Framework memo/computed primitives for derived data, keyed to the actual derivation inputs.' + - 'Stable state, signal, ref, rune, resource, or Query result references for changing data.' + - 'A module-level stable empty fallback instead of data ?? [] in React-like render paths or reactive option initializers.' + +custom_feature_depth_contract: + exception: 'The custom-features skill must enumerate the complete extension surface in one authoritative example. Do not pair a minimal density example with separate map/API examples or add a later misconception that merely explains their relationship.' + required_feature_maps: + - 'TableState_FeatureMap' + - 'TableOptions_FeatureMap' + - 'Table_FeatureMap' + - 'ColumnDef_FeatureMap' + - 'Column_FeatureMap' + - 'Row_FeatureMap' + - 'Cell_FeatureMap' + - 'Header_FeatureMap' + - 'RowModelFns_FeatureMap' + - 'CachedRowModels_FeatureMap' + required_api_installation: + - 'assignTableAPIs inside constructTableAPIs for the singleton table.' + - 'assignPrototypeAPIs inside assignColumnPrototype for shared column methods.' + - 'assignPrototypeAPIs inside assignRowPrototype for shared row methods.' + - 'assignPrototypeAPIs inside assignCellPrototype for shared cell methods.' + - 'assignPrototypeAPIs inside assignHeaderPrototype for shared header methods.' + - 'initColumnInstanceData and initRowInstanceData for per-instance mutable data rather than shared methods.' + - 'The table_/column_/row_/cell_/header_ static-name prefixes, self argument difference, optional memoDeps, and absence of per-object assign*APIs utilities.' + +api_discovery_policy: + rule: 'For exact exports, option types, state shapes, and instance APIs, inspect src in the installed package before inventing an API or relying on memory.' + adapter_entrypoint: 'node_modules/@tanstack/-table/src/index.ts' + core_entrypoint: 'node_modules/@tanstack/table-core/src/index.ts' + feature_source: 'node_modules/@tanstack/table-core/src/features//' + fallback: 'If source is unavailable because the package manager omitted it, inspect the matching declarations and the installed package version; do not substitute v8 or another adapter API.' + +coverage: + ignored_packages: + - 'tanstack-solid-table-example-basic-app-table' + - 'tanstack-solid-table-example-basic-dynamic-columns' + - 'tanstack-solid-table-example-basic-external-atoms' + - 'tanstack-solid-table-example-basic-external-state' + - 'tanstack-solid-table-example-basic-use-table' + - 'tanstack-solid-table-example-column-groups' + - 'tanstack-solid-table-example-column-ordering' + - 'tanstack-solid-table-example-column-pinning' + - 'tanstack-solid-table-example-column-pinning-split' + - 'tanstack-solid-table-example-column-pinning-sticky' + - 'tanstack-solid-table-example-column-resizing' + - 'tanstack-solid-table-example-column-resizing-performant' + - 'tanstack-solid-table-example-column-sizing' + - 'tanstack-solid-table-example-column-visibility' + - 'tanstack-solid-table-example-composable-tables' + - 'tanstack-solid-table-example-expanding' + - 'tanstack-solid-table-example-filters' + - 'tanstack-solid-table-example-filters-faceted' + - 'tanstack-solid-table-example-filters-fuzzy' + - 'tanstack-solid-table-example-grouping' + - 'tanstack-solid-table-example-kitchen-sink' + - 'tanstack-solid-table-example-pagination' + - 'tanstack-solid-table-example-row-pinning' + - 'tanstack-solid-table-example-row-selection' + - 'tanstack-solid-table-example-sorting' + - 'tanstack-solid-table-example-sub-components' + - 'tanstack-solid-table-example-virtualized-columns' + - 'tanstack-solid-table-example-virtualized-infinite-scrolling' + - 'tanstack-solid-table-example-virtualized-rows' + - 'tanstack-solid-table-example-with-tanstack-form' + - 'tanstack-solid-table-example-with-tanstack-query' + - 'tanstack-solid-table-example-with-tanstack-router' + domains: - - name: Core foundations - slug: core-foundations - description: - Setup, column definitions, state management, and customization of built-in feature behavior. - State-management is the prerequisite for every other skill. - - name: Row-model features - slug: row-model-features - description: - Features driven by entries in `rowModels` — filtering (column+global+faceting+fuzzy), - sorting, pagination, grouping, expanding. All have `manual` opt-outs for server-side data. - - name: UI-state features - slug: ui-state-features - description: - Features that are pure UI state — no row model needed. Column layout (visibility/ordering/pinning/sizing/resizing), - row pinning, row selection. - - name: Framework adapters - slug: framework-adapters - description: - Per-framework reactivity bindings and rendering integration. Each adapter has its own `table-state` - skill; React adds Subscribe-for-React-Compiler, Angular adds structural directives, Lit adds the TableController - pattern. - - name: Lifecycle - slug: lifecycle - description: - 'End-to-end journeys that cross-cut features: getting-started, v8→v9 migration, client-to-server - conversion, production-readiness.' - - name: Composition - slug: composition - description: - Patterns for using TanStack Table with sibling TanStack libraries (Store, Query, Virtual, - Form, Pacer, Devtools). Per maintainer decision, no per-UI-library composition skills. + - slug: foundations + name: 'Foundations and migration' + description: 'Headless philosophy, feature registration, client/server ownership, TypeScript inference, API discovery, custom features, and v8 migration.' + - slug: feature-plugins + name: 'Feature plugins' + description: 'The 14 stock optional features, their prerequisites, state, row-model participation, and UI responsibilities.' + - slug: framework-adapters + name: 'Framework adapters' + description: 'Per-framework setup, reactive table state, v8 migration, reusable createTableHook patterns, and supported Query/Virtual composition.' + - slug: observability + name: 'Devtools' + description: 'Framework-neutral and adapter-specific Devtools registration, keys, production exports, and connection failures.' + - slug: utilities + name: 'Utilities' + description: 'Fuzzy ranking and comparison behavior shipped by match-sorter-utils.' + +tensions: + - name: 'Explicit tree-shaking vs kitchen-sink convenience' + skills: ['core', 'table-features', 'getting-started', 'migrate-v8-to-v9'] + agent_risk: 'Agents reach for stockFeatures or a kitchen-sink example as the default and silently erase the main v9 bundle-size benefit.' + - name: 'Client-side processing vs server-owned processing' + skills: + [ + 'client-vs-server', + 'column-filtering', + 'global-filtering', + 'grouping', + 'sorting', + 'pagination', + 'with-tanstack-query', + ] + agent_risk: 'Agents enable manual flags but still expect Table row models to transform the data, or mix server and client stages without defining the boundary.' + - name: 'Simple whole-table subscriptions vs fine-grained reactive state' + skills: ['table-state', 'create-table-hook', 'with-tanstack-virtual'] + agent_risk: 'Agents either over-optimize small tables or hide state reads behind stable objects so framework compilers and memoization miss updates.' + - name: 'Reactive options vs stable model inputs' + skills: + [ + 'core', + 'client-vs-server', + 'getting-started', + 'table-state', + 'with-tanstack-query', + ] + agent_risk: 'Agents derive data with map/filter/slice or recreate columns inside render/options callbacks, invalidating row and column models on unrelated updates or causing adapter render loops.' + - name: 'Headless flexibility vs renderer-owned correctness' + skills: + [ + 'core', + 'column-pinning', + 'column-sizing', + 'column-resizing', + 'with-tanstack-virtual', + ] + agent_risk: 'Agents expect feature state to supply semantic markup or CSS and blame Table for sticky, width, accessibility, or layout behavior owned by their renderer.' + skills: - - name: Setup - slug: setup - domain: core-foundations - description: - Install a table adapter and wire up a first working table with `features`, `rowModels`, - columns, and data. + - slug: core + package: '@tanstack/table-core' + domain: foundations type: core - packages: - - '@tanstack/table-core' - covers: - - tableFeatures - - features - - rowModels - - useTable - - constructTable - - _createTable - - stockFeatures - - coreFeatures - - storeReactivityBindings - - FlexRender - - flexRender - - table.getHeaderGroups - - table.getRowModel - tasks: - - Render a first read-only table from an array of objects - - Decide between an empty `tableFeatures({})` core-only setup vs `stockFeatures` (all features) vs hand-picked - feature imports - - Wire up the matching adapter (`useTable` / `injectTable` / `createTable` / `constructTable`) and render - markup with `` or `flexRender` - - Use `@tanstack/table-core` + `storeReactivityBindings()` directly (vanilla JS) when no framework adapter - exists + purpose: 'Establish the headless mental model, core row model, markup ownership, stable inputs, and feature-driven table architecture.' + sources: + [ + 'TanStack/table:docs/overview.md', + 'TanStack/table:docs/guide/tables.md', + 'TanStack/table:docs/guide/data.md', + 'TanStack/table:packages/table-core/src/index.ts', + ] failure_modes: - - mistake: Omits `features` and `rowModels` - mechanism: - v9 requires `features` (and an `rowModels` map, even if empty) at the top of `useTable`/`constructTable` - options. Without `features`, TypeScript loses feature-state inference and runtime construction - has no feature plugins to register. - wrong_pattern: | - // v8-flavoured, breaks in v9 - const table = useTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), // v8 option name - }) - correct_pattern: | - // v9 minimal table - const features = tableFeatures({}) // empty = core features only - const table = useTable({ - features, - rowModels: {}, // core row model auto-included - columns, - data, - }) - source: examples/react/basic-use-table/src/main.tsx:56-109; docs/guide/tables.md:33-47; docs/framework/react/guide/migrating.md:78-103 - priority: CRITICAL - status: active - version_context: - v9 alpha; affects every user upgrading from v8 — the hook is renamed (`useReactTable` - → `useTable`) AND new required options were introduced - - mistake: Calls `tableFeatures({})` inside the component body - mechanism: - A fresh `features` object on each render destroys the table's stable reference for feature - registration, the same way unstable `columns`/`data` cause infinite re-renders. `features` must - be hoisted to module scope or memoized. - wrong_pattern: | - function MyTable() { - // ❌ new object every render -> potential re-construct + state churn - const features = tableFeatures({ rowSortingFeature }) - const table = useTable({ features, rowModels: {}, columns, data }) - } - correct_pattern: | - // ✅ module-scoped, stable reference - const features = tableFeatures({ rowSortingFeature }) - - function MyTable() { - const table = useTable({ features, rowModels: {}, columns, data }) - } - source: - docs/guide/data.md:184-244; examples/react/basic-use-table/src/main.tsx:56 (defined outside - component) - priority: HIGH - status: active - version_context: v9 alpha; new in v9 because v8 had no `features` option - skills: - - state-management - - mistake: Reaches for `stockFeatures` by default - mechanism: - Reaching for `stockFeatures` re-introduces v8 bundle size (~15–20kb), silently negating - v9's opt-in tree-shaking. The v9 default is hand-picked features; `stockFeatures` exists only as - a v8 escape hatch. - wrong_pattern: | - // ❌ ships every feature, even unused ones - import { useTable, stockFeatures } from '@tanstack/react-table' - const table = useTable({ - features: stockFeatures, - rowModels: {}, - columns, - data, - }) - correct_pattern: | - // ✅ only the features you use - import { - tableFeatures, - useTable, - rowSortingFeature, - rowPaginationFeature, - } from '@tanstack/react-table' + - 'Treating Table as a component or design system instead of rendering semantic markup, styles, and accessibility in userland.' + - 'Recreating data or columns on every reactive pass and causing repeated row-model work or render loops.' + - 'Destructuring v9 row, cell, header, or column prototype methods and losing their instance this binding.' - const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - }) - source: docs/framework/react/guide/migrating.md:9-12,136-147; packages/table-core/src/features/stockFeatures.ts:38-53 - priority: MEDIUM - status: active - version_context: v9 alpha; `stockFeatures` is documented but discouraged for new code - - mistake: Adds a row model without registering its feature - mechanism: - Row model factories like `createSortedRowModel` only run if the matching feature (e.g. - `rowSortingFeature`) is also registered in `features`. TypeScript flags this when state slices/APIs - are accessed, but runtime silently degrades — `table.atoms.sorting` is undefined and sort handlers - do nothing. - wrong_pattern: | - // ❌ rowSortingFeature missing from features — sortedRowModel orphaned - const features = tableFeatures({ rowPaginationFeature }) - const table = useTable({ - features, - rowModels: { - sortedRowModel: createSortedRowModel(sortFns), // no-op - paginatedRowModel: createPaginatedRowModel(), - }, - columns, - data, - }) - correct_pattern: | - // ✅ feature + row model registered together - const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - }) - const table = useTable({ - features, - rowModels: { - sortedRowModel: createSortedRowModel(sortFns), - paginatedRowModel: createPaginatedRowModel(), - }, - columns, - data, - }) - source: docs/guide/row-models.md:46-78; examples/react/basic-external-atoms/src/main.tsx:27-30,81-92 - priority: HIGH - status: active - version_context: v9 alpha; new failure surface since v8 had no features/feature-row-model pairing - - mistake: Passing empty array literal to data causes infinite rerenders - mechanism: | - `const data = items ?? []` creates a new `[]` reference on every render. The - table sees a fresh `data` prop and rebuilds row models; if any callback in the - same render also reads from the table, you get an infinite re-render loop. - wrong_pattern: | - // ❌ Fresh [] each render — infinite loop when items is undefined - const table = useReactTable({ - data: items ?? [], - columns, - getCoreRowModel: getCoreRowModel(), - }) - correct_pattern: | - // ✅ Hoist the empty fallback OR memoize - const EMPTY: MyRow[] = [] - const table = useReactTable({ - data: items ?? EMPTY, - columns, - getCoreRowModel: getCoreRowModel(), - }) - // OR - const data = useMemo(() => items ?? [], [items]) - source: - https://github.com/TanStack/table/issues/4566 (Empty data causes infinite looping), https://github.com/TanStack/table/issues/6002 - (if data is empty array, then rendering table is causing infinite rerenders) - priority: CRITICAL - status: active - version_context: - Affects every version. Top recurring beginner issue. Maintainer (KevinVandy) explicitly - says docs need to make stability obvious. - skills: - - setup - - getting-started - - state-management - - mistake: Defining columns inside the render body without useMemo - mechanism: | - Defining `columns` inline inside the component body creates a fresh column array - every render. Table internals see new column references and rebuild header - groups, recalculate sizes, and lose memo caches — leading to slow renders and - cells/components remounting on every parent state change. - wrong_pattern: | - // ❌ Columns recreated every render - function MyTable({ data }) { - const columns = [ - columnHelper.accessor('name', { header: 'Name' }), - columnHelper.accessor('email', { header: 'Email' }), - ] - const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) - ... - } - correct_pattern: | - // ✅ Hoist outside, or useMemo with stable deps - const columns = [ - columnHelper.accessor('name', { header: 'Name' }), - columnHelper.accessor('email', { header: 'Email' }), - ] - function MyTable({ data }) { - const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) - ... - } - source: - https://github.com/TanStack/table/issues/5141 (What props passed to useReactTable hook have - to be stable?), https://github.com/TanStack/table/issues/4794 (unnecessary rerenders) - priority: CRITICAL - status: active - version_context: - 'Top-priority pattern across v7, v8, v9. v9 makes columns/data readonly (PR #6183) - to nudge users.' - skills: - - setup - - state-management - - production-readiness - - mistake: Hallucinating react-table v7 / pre-v9 @tanstack/[framework]-table APIs - mechanism: - Every major release of TanStack Table (formerly react-table) has been a substantial upgrade. - Agents trained on older data confidently produce v7 or v8 API shapes that no longer exist in v9 - (e.g. `useReactTable`, inline `getCoreRowModel()` as an option, `useTable` from react-table v7). - wrong_pattern: |- - // ❌ v7 / v8 patterns the agent invents - import { useTable, useSortBy } from 'react-table' // v7 - const table = useTable({ columns, data }, useSortBy) - - // or v8 - import { useReactTable, getCoreRowModel } from '@tanstack/react-table' - const table = useReactTable({ columns, data, getCoreRowModel: getCoreRowModel() }) - correct_pattern: |- - // ✅ v9 pattern — `features` + `rowModels` - import { useTable, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/react-table' - const features = tableFeatures({ rowSortingFeature }) - const table = useTable({ - features, - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - priority: CRITICAL - version_context: - v7→v8 and v8→v9 both shifted the API substantially; agents trained on any pre-v9 - data will produce wrong shapes. - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - mistake: API or state slice "missing" because the feature was not registered in `features` - mechanism: - In v9, `features` is a tree-shakeable registry. If a feature is not in `features`, TypeScript - hides its APIs and the runtime atom is not created. Agents who copy a snippet for `table.setColumnFilters(...)` - without registering `columnFilteringFeature` see a TS error or `table.atoms.columnFilters` is undefined - — and may incorrectly conclude the feature is broken or removed in v9. - wrong_pattern: |- - // ❌ rowSortingFeature missing — table.setSorting / state.sorting unavailable - const features = tableFeatures({}) // empty - const table = useTable({ features, rowModels: {}, columns, data }) - table.setSorting([{ id: 'age', desc: true }]) // ❌ does not exist on this table type - correct_pattern: |- - // ✅ Register every feature you intend to use; pair with its row model when applicable - const features = tableFeatures({ rowSortingFeature, rowPaginationFeature }) - const table = useTable({ - features, - rowModels: { - sortedRowModel: createSortedRowModel(sortFns), - paginatedRowModel: createPaginatedRowModel(), - }, - columns, data, - }) - priority: CRITICAL - version_context: - v9-specific. This is the first version of TanStack Table where features must be declared - for TypeScript and runtime to expose them. Expect heavy confusion from devs and agents trained on - v8. - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - mistake: Bundling stockFeatures or all features when only a few are used - mechanism: - TanStack Table v9 is tree-shakeable specifically so you only pay for features you use. - Registering `stockFeatures` (or every feature "just in case") forfeits the bundle benefit that motivated - the v9 redesign. - wrong_pattern: |- - // ❌ Pulls in every feature even though only sorting+pagination are used - import { stockFeatures, tableFeatures } from '@tanstack/react-table' - const features = tableFeatures(stockFeatures) - correct_pattern: |- - // ✅ Register only what this table uses - import { tableFeatures, rowSortingFeature, rowPaginationFeature } from '@tanstack/react-table' - const features = tableFeatures({ rowSortingFeature, rowPaginationFeature }) - priority: HIGH - version_context: v9. Tree-shaking via `features` is one of the headline reasons for the rewrite. - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - name: Column Definitions - slug: column-definitions - domain: core-foundations - description: - Define columns with `createColumnHelper()` to extract data and - render headers/cells/footers. + - slug: table-features + package: '@tanstack/table-core' + domain: foundations type: core - packages: - - '@tanstack/table-core' - covers: - - createColumnHelper - - columnHelper.accessor - - columnHelper.display - - columnHelper.group - - columnHelper.columns - - ColumnDef - - AccessorKeyColumnDef - - AccessorFnColumnDef - - DisplayColumnDef - - GroupColumnDef - - accessorKey - - accessorFn - - header - - cell - - footer - - aggregatedCell - - id - - getRowId - - DeepKeys - - flexRender - tasks: - - Create a typed `columnHelper` and define accessor/display/group columns with strong inference - - 'Pick between `accessorKey: "name.first"` (deep key with dots) and `accessorFn: row => row.name.first` - for nested / computed values' - - Use `getRowId` to derive stable row identifiers from data (instead of array index) so selection/expansion - survive data updates - - Render headers/cells/footers with `` (or `flexRender`) so string / JSX / function - forms all work + purpose: 'Register only the features, row-model factories, and function registries a table actually uses.' + sources: + [ + 'TanStack/table:docs/guide/row-models.md', + 'TanStack/table:packages/table-core/src/types/TableFeatures.ts', + 'TanStack/table:packages/table-core/src/features/stockFeatures.ts', + 'TanStack/table:packages/table-core/src/core/table/constructTable.ts', + ] failure_modes: - - mistake: Passes only TData to `createColumnHelper` - mechanism: - 'v9 changed the generic order: `createColumnHelper`. v8 code passed only - ``, which now binds `TData` into the `TFeatures` slot and breaks every column type. The compiler - error is noisy and not obvious.' - wrong_pattern: | - // ❌ v8 signature — TData ends up in the TFeatures slot - const columnHelper = createColumnHelper() - correct_pattern: | - // ✅ v9 — TFeatures first, TData second; use typeof features - const features = tableFeatures({ rowSortingFeature }) - const columnHelper = createColumnHelper() - source: - packages/table-core/src/helpers/columnHelper.ts:99-103; docs/framework/react/guide/migrating.md:484-499; - examples/react/basic-external-atoms/src/main.tsx:32 - priority: CRITICAL - status: active - version_context: v9 alpha; breaking change vs v8 generic signature - - mistake: Accessor function returns an object/array - mechanism: - The accessed value is what the table uses for sorting, filtering, faceting, and grouping. - Returning a non-primitive value means the built-in sort/filter/aggregation functions silently misbehave - or throw — only `displayCell` ever sees the value formatted. A primitive `string`/`number`/`Date` - is expected unless you supply a matching `sortFn`/`filterFn`/`aggregationFn`. - wrong_pattern: | - // ❌ returns an object — built-in alphanumeric/text sort and includesString filter break - columnHelper.accessor((row) => row.name, { - id: 'name', - cell: (info) => `${info.getValue().first} ${info.getValue().last}`, - }) - correct_pattern: | - // ✅ accessor returns a primitive; cell can still format it - columnHelper.accessor((row) => `${row.name.first} ${row.name.last}`, { - id: 'fullName', - cell: (info) => info.getValue(), - }) - source: docs/guide/column-defs.md:231; examples/react/basic-subscribe/src/main.tsx:103-108 - priority: HIGH - status: active - version_context: always present - skills: - - customizing-feature-behavior - - mistake: Omits `id` on an accessorFn column - mechanism: - When a column uses `accessorFn` (not `accessorKey`), there is nothing to derive an id from. - The constructor throws "coreColumnsFeature require an id when using an accessorFn" in development. - The same applies to non-string `header` values — without `id` or a string header, construction fails. - wrong_pattern: | - // ❌ accessorFn + JSX header => no id can be derived - columnHelper.accessor((row) => row.lastName, { - header: () => Last Name, - cell: (info) => info.getValue(), - }) - correct_pattern: | - // ✅ explicit id whenever you use accessorFn - columnHelper.accessor((row) => row.lastName, { - id: 'lastName', - header: () => Last Name, - cell: (info) => info.getValue(), - }) - source: - packages/table-core/src/core/columns/constructColumn.ts:91-100; examples/react/basic-use-table/src/main.tsx:65-70; - docs/guide/column-defs.md:233-243 - priority: CRITICAL - status: active - version_context: always present (same rule as v8) - - mistake: Defines `columns` inside the component without `useMemo` - mechanism: - 'TanStack Table compares `columns` and `data` by reference. Re-creating either array on - each render triggers internal recomputation that, in React, causes an infinite loop (state change - -> render -> new `columns` -> state change). This is the #1 FAQ.' - wrong_pattern: | - function MyTable() { - // ❌ new array reference every render -> infinite render loop - const columns = [ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('lastName', { header: 'Last' }), - ] - const table = useTable({ features, rowModels: {}, columns, data }) - } - correct_pattern: | - // ✅ memoized columns or module-scoped via columnHelper.columns([...]) - function MyTable() { - const columns = React.useMemo( - () => - columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('lastName', { header: 'Last' }), - ]), - [], - ) - const table = useTable({ features, rowModels: {}, columns, data }) - } - source: docs/faq.md:5-128; examples/react/basic-subscribe/src/main.tsx:51-127 - priority: CRITICAL - status: active - version_context: 'always present; #1 reason for infinite re-renders' - skills: - - setup - - state-management - - mistake: Uses array index `row.id` and updates data - mechanism: - By default, `row.id` is the row's index in `data`. When `data` is reordered, filtered, - or items are removed, row-keyed state (selection, expansion, pinning) attaches to the wrong row. - `getRowId` derives a stable id from the row's own data. - wrong_pattern: | - // ❌ no getRowId -> rowSelection survives data updates but maps to wrong rows - const table = useTable({ - features, - rowModels: {}, - columns, - data, - enableRowSelection: true, - }) - correct_pattern: | - // ✅ stable id from the row's own identifier - const table = useTable({ - features, - rowModels: {}, - columns, - data, - getRowId: (row) => row.id, - enableRowSelection: true, - }) - source: docs/guide/rows.md:48-59; examples/react/basic-subscribe/src/main.tsx:148; packages/table-core/src/core/rows/coreRowsFeature.utils.ts:215-228 - priority: HIGH - status: active - version_context: always present - - mistake: Column visibility toggle on column groups produces incorrect state - mechanism: | - Calling `column.toggleVisibility()` on a column GROUP writes `false` to - `columnVisibility[groupId]`, but `getIsVisible()` for a group always returns - `true` regardless. Users wire up a checkbox to the group header thinking it - hides children — it doesn't. - wrong_pattern: | - // ❌ Group-level toggle silently no-ops on visibility - - correct_pattern: | - // ✅ Iterate leaves explicitly - function toggleGroupVisibility(group) { - const targetVisible = !group.getLeafColumns().every(c => c.getIsVisible()) - group.getLeafColumns().forEach(c => c.toggleVisibility(targetVisible)) - } - source: - https://github.com/TanStack/table/issues/5497 (Column visibility APIs do not work with column - groups, 5 comments) - priority: MEDIUM - status: active - version_context: v8 / v9 - skills: - - column-definitions - - column-layout - - mistake: accessor with optional path strips undefined from getValue type - mechanism: | - The `DeepValue` generic in table-core walks a dotted path as `infer TBranch`/ - `infer TDeepProp`, but doesn't propagate `undefined` when an intermediate key - is optional. `getValue()` returns `number` when it should return `number | undefined`, - hiding real runtime errors. - wrong_pattern: | - // ❌ amount is inferred as `number` even though salary is optional - columnHelper.accessor('user.salary.amount', { - cell: (row) => { - const amount = row.getValue() // type: number (WRONG) - return amount.toFixed(2) // crashes when salary is undefined - }, - }) - correct_pattern: | - // ✅ Use accessorFn for paths with optional segments — type follows expression - columnHelper.accessor((row) => row.user.salary?.amount, { - id: 'salary', - cell: (info) => { - const amount = info.getValue() // type: number | undefined - return amount?.toFixed(2) ?? '-' - }, - }) - source: - https://github.com/TanStack/table/issues/6238 (accessor getValue() loses undefined for key - paths with optional keys) - priority: MEDIUM - status: active - version_context: v8 + v9 type bug - - mistake: columnHelper.accessor inside columnHelper.group loses getValue type inference - mechanism: | - When an accessor is nested inside a `columnHelper.group()`, the `info.getValue()` - return type degrades to `unknown` because the group helper's overloads don't - thread the row-data generic through correctly. - wrong_pattern: | - // ❌ info.getValue() inferred as unknown - columnHelper.group({ - id: 'name', - columns: [ - columnHelper.accessor('firstName', { - cell: (info) => info.getValue(), // unknown - }), - ], - }) - correct_pattern: | - // ✅ Hoist accessor definitions out of the group - const firstNameCol = columnHelper.accessor('firstName', { - cell: (info) => info.getValue(), // string - }) - columnHelper.group({ id: 'name', columns: [firstNameCol] }) - source: - https://github.com/TanStack/table/issues/5860 (getValue fails to infer correct type when columnHelper.accessor - defined within columnHelper.group), https://github.com/TanStack/table/issues/5065 - priority: MEDIUM - status: active - version_context: v8 / v9 - - mistake: getValue cache not invalidating when accessorFn changes - mechanism: | - `getValue` caches per-column by ID. If you swap `accessorFn` in response to a - state change (e.g. "Last, First" ↔ "First Last" toggle) without changing the - column ID, cached values are returned. The new accessor never runs. - wrong_pattern: | - // ❌ Stale values shown after accessor switch - const columns = useMemo(() => [ - columnHelper.accessor( - firstFormat ? (row) => row.firstName + ' ' + row.lastName : (row) => row.lastName + ', ' + row.firstName, - { id: 'name' } - ), - ], [firstFormat]) - correct_pattern: | - // ✅ Either change the column ID (loses sort/filter state) OR move the logic - // into cell() which is not cached: - columnHelper.accessor((row) => ({ first: row.firstName, last: row.lastName }), { - id: 'name', - cell: (info) => { - const { first, last } = info.getValue() - return firstFormat ? `${first} ${last}` : `${last}, ${first}` - }, - }) - source: - https://github.com/TanStack/table/issues/5363 (getValue cache not invalidating when accessorFn - is updated) - priority: MEDIUM - status: active - version_context: v8 / v9 — caching is fundamental, not a bug - skills: - - column-definitions - - column-layout - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - maintainer_notes: - - accessorKey + deep keys may be simplified in v10 (accessorFn + id is the maintainer's preferred long-term - shape). For v9, accessorKey remains fully supported and the more concise option for simple keys. - - name: State Management - slug: state-management - domain: core-foundations - description: - Coordinate table state slices (sorting, pagination, filters, selection, etc.) across `initialState`, - `state`+`on*Change`, and external `atoms`, with awareness of v9 atom precedence. + - 'Calling a feature API without registering its feature, so its state slice and runtime API do not exist.' + - 'Registering a row-model or function-registry slot without its prerequisite feature, or placing prerequisites after dependent slots.' + - 'Defaulting to stockFeatures for new tables and bundling every optional feature instead of preserving v9 tree-shaking.' + + - slug: client-vs-server + package: '@tanstack/table-core' + domain: foundations type: core - packages: - - '@tanstack/table-core' - covers: - - baseAtoms - - atoms - - store - - state - - initialState - - on[State]Change - - setOptions - - reset - - resetSorting - - resetPagination - - resetColumnFilters - - resetGlobalFilter - - resetRowSelection - - manualFiltering - - manualSorting - - manualPagination - - manualExpanding - - manualGrouping - - autoResetPageIndex - - autoResetAll - - TableState - - SortingState - - PaginationState - - RowSelectionState - - ColumnFiltersState - - GroupingState - - storeReactivityBindings - - createAtom - - useCreateAtom - - useSelector - - Subscribe - - table.Subscribe - - useTable selector (second argument) - tasks: - - Decide which state ownership pattern fits — internal (default) vs `state`+`on*Change` (v8 controlled) - vs external `atoms` (v9 preferred) vs `initialState` (starting value only) - - Promote a slice to external state when the app needs to share it with a server query, persistence, - or another component - - Toggle a feature to a "manual" server-side mode (`manualSorting`, `manualFiltering`, `manualPagination`) - — sort/filter/paginate happen server-side and the table just renders the page - - Reset state with feature reset APIs (`resetSorting()`, `resetPagination(true)`) and understand why - `table.reset()` is unsafe when slices are externally owned + purpose: 'Choose which filtering, grouping, sorting, expanding, and pagination stages Table owns and which a backend owns.' + sources: + [ + 'TanStack/table:docs/guide/row-models.md', + 'TanStack/table:packages/table-core/src/core/row-models/coreRowModelsFeature.utils.ts', + 'TanStack/table:examples/react/with-tanstack-query', + ] failure_modes: - - mistake: Passes both `state.pagination` and `atoms.pagination` - mechanism: - When both are supplied for the same slice, the external atom wins silently. `state.pagination` - is then dead config — UI values written to React state never reach the table, leading to "I called - setPagination but nothing updated" bugs. - wrong_pattern: | - // ❌ both ownership paths for the same slice - const paginationAtom = useCreateAtom({ pageIndex: 0, pageSize: 10 }) - const [pagination, setPagination] = React.useState(...) - - const table = useTable({ - features, rowModels: {...}, columns, data, - state: { pagination }, // ignored - onPaginationChange: setPagination, - atoms: { pagination: paginationAtom }, // wins - }) - correct_pattern: | - // ✅ pick one ownership path per slice — here, external atoms - const paginationAtom = useCreateAtom({ pageIndex: 0, pageSize: 10 }) + - 'Assuming manualPagination, manualSorting, manualFiltering, manualGrouping, or manualExpanding performs server work; each flag only bypasses that client row-model stage.' + - 'Passing a full dataset while manual pagination is enabled and expecting Table to slice it, or passing one page while a client row model expects the full dataset.' + - 'Mixing client and server stages without defining their order, producing sorting or filtering that only applies to the currently loaded page.' - const table = useTable({ - features, rowModels: {...}, columns, data, - atoms: { pagination: paginationAtom }, - // no state.pagination, no onPaginationChange needed - }) - source: - docs/framework/react/guide/table-state.md:315-316; docs/framework/react/guide/migrating.md:465-481; - packages/table-core/src/core/table/constructTable.ts:93-103 (atom precedence) - priority: CRITICAL - status: active - version_context: v9 alpha; new ownership-conflict surface introduced by the `atoms` option - - mistake: Uses external `state` without the matching `on*Change` callback - mechanism: - External `state.sorting` syncs into the table's base atom, but without `onSortingChange` - the table has no way to update React state — every sort toggle appears to do nothing. v9 silently - keeps reading from `state` so the UI looks "stuck". - wrong_pattern: | - // ❌ state without callback — sort toggles never reach setSorting - const [sorting, setSorting] = React.useState([]) - const table = useTable({ - features, rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - state: { sorting }, // no onSortingChange - }) - correct_pattern: | - // ✅ state + on*Change must be paired - const [sorting, setSorting] = React.useState([]) - const table = useTable({ - features, rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - state: { sorting }, - onSortingChange: setSorting, - }) - source: docs/framework/react/guide/table-state.md:415-435; examples/react/basic-external-state/src/main.tsx:64-94 - priority: CRITICAL - status: active - version_context: always present (same v8 rule) - - mistake: Uses `initialState` to control or update state - mechanism: - '`initialState` is only read once at construction time to build base atoms; mutating it - later does NOT update table state. Developers expect a React-state-like contract and watch state - freeze in place.' - wrong_pattern: | - // ❌ updates to initialState are ignored after first render - function MyTable({ defaultSort }: { defaultSort: SortingState }) { - const table = useTable({ - features, rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - initialState: { sorting: defaultSort }, // ❌ later changes to defaultSort never sync - }) - } - correct_pattern: | - // ✅ control with state + on*Change (or an external atom) - function MyTable({ defaultSort }: { defaultSort: SortingState }) { - const [sorting, setSorting] = React.useState(defaultSort) - const table = useTable({ - features, rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - state: { sorting }, - onSortingChange: setSorting, - }) - } - source: docs/framework/vanilla/guide/table-state.md:142-175; docs/framework/react/guide/table-state.md:285-315 - priority: HIGH - status: active - version_context: always present (same v8 rule) - - mistake: Writes to `table.baseAtoms.x.set(...)` while `atoms.x` owns the slice - mechanism: - When an external atom is supplied for a slice, `table.atoms.x` derives from it, not from - `baseAtoms.x`. Writes to the base atom silently no-op for reads; the UI shows the external atom's - value, the base atom drifts, and reset behavior gets weird. - wrong_pattern: | - // ❌ direct baseAtoms write while external atom owns the slice - const paginationAtom = useCreateAtom({ pageIndex: 0, pageSize: 10 }) - const table = useTable({ features, rowModels: {...}, columns, data, atoms: { pagination: paginationAtom } }) - - // later, somewhere - table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 })) - // ❌ baseAtom updated, but table.atoms.pagination still reads from paginationAtom - correct_pattern: | - // ✅ write to the external atom (or use the feature's setter API) - paginationAtom.set((old) => ({ ...old, pageIndex: 0 })) - // or - table.setPageIndex(0) - source: - docs/framework/vanilla/guide/table-state.md:138-141; docs/framework/react/guide/table-state.md:280-283; - packages/table-core/src/core/table/constructTable.ts:93-103 - priority: HIGH - status: active - version_context: v9 alpha; new pitfall introduced by atom architecture - - mistake: Forgets `manualSorting`/`manualFiltering`/`manualPagination` when serving server-side data - mechanism: - 'Without the `manual*` flag, the table re-applies its client-side row models on top of - already-sorted/filtered/paginated server data — rows get re-sorted, re-filtered, or sliced into - another page. Symptoms: wrong rows shown, broken page math, blank pages.' - wrong_pattern: | - // ❌ data is already paginated server-side, but table still slices it - const dataQuery = useQuery({ queryKey: ['data', pagination], queryFn: fetchPage }) - const table = useTable({ - features, rowModels: { paginatedRowModel: createPaginatedRowModel() }, - columns, - data: dataQuery.data?.rows ?? [], - rowCount: dataQuery.data?.rowCount, - atoms: { pagination: paginationAtom }, - // ❌ missing manualPagination: true - }) - correct_pattern: | - // ✅ tell the table the server handles pagination - const table = useTable({ - features, rowModels: {}, // can drop paginatedRowModel if fully server-side - columns, - data: dataQuery.data?.rows ?? [], - rowCount: dataQuery.data?.rowCount, - atoms: { pagination: paginationAtom }, - manualPagination: true, - }) - source: - docs/framework/vanilla/guide/table-state.md:197-228; docs/framework/react/guide/table-state.md:336-378; - packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts:20-23 - priority: CRITICAL - status: active - version_context: always present (same v8 rule) - - mistake: Uses `table.reset()` to clear externally owned state - mechanism: - '`table.reset()` only resets the internal `baseAtoms` to `initialState`; slices owned by - external atoms or external `state` are untouched. Developers expect "reset everything" and end up - with half-reset state (visible state from external atom, hidden state in baseAtom drifts).' - wrong_pattern: | - // ❌ external atom keeps its current value; only baseAtoms reset - const sortingAtom = useCreateAtom([]) - const table = useTable({ - features, rowModels: {...}, columns, data, - atoms: { sorting: sortingAtom }, - }) - // ... - table.reset() // sortingAtom is NOT cleared - correct_pattern: | - // ✅ feature-specific reset writes through the slice's updater (atom-aware) - table.resetSorting() // works whether sorting is internal or external - // or, if you specifically want to clear the external atom: - sortingAtom.set([]) - source: - docs/framework/vanilla/guide/table-state.md:177-188; docs/framework/react/guide/table-state.md:317-328; - packages/table-core/src/core/table/coreTablesFeature.utils.ts:42-65 - priority: HIGH - status: active - version_context: v9 alpha; the atom split makes `reset()` less safe than v8 - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - mistake: API or state slice "missing" because the feature was not registered in `features` - mechanism: - In v9, `features` is a tree-shakeable registry. If a feature is not in `features`, TypeScript - hides its APIs and the runtime atom is not created. Agents who copy a snippet for `table.setColumnFilters(...)` - without registering `columnFilteringFeature` see a TS error or `table.atoms.columnFilters` is undefined - — and may incorrectly conclude the feature is broken or removed in v9. - wrong_pattern: |- - // ❌ rowSortingFeature missing — table.setSorting / state.sorting unavailable - const features = tableFeatures({}) // empty - const table = useTable({ features, rowModels: {}, columns, data }) - table.setSorting([{ id: 'age', desc: true }]) // ❌ does not exist on this table type - correct_pattern: |- - // ✅ Register every feature you intend to use; pair with its row model when applicable - const features = tableFeatures({ rowSortingFeature, rowPaginationFeature }) - const table = useTable({ - features, - rowModels: { - sortedRowModel: createSortedRowModel(sortFns), - paginatedRowModel: createPaginatedRowModel(), - }, - columns, data, - }) - priority: CRITICAL - version_context: - v9-specific. This is the first version of TanStack Table where features must be declared - for TypeScript and runtime to expose them. Expect heavy confusion from devs and agents trained on - v8. - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - name: Customizing Feature Behavior - slug: customizing-feature-behavior - domain: core-foundations - description: - Override per-column `sortFn`, `filterFn`, `aggregationFn`, and table-level `globalFilterFn` - — and chain filter→sort metadata via `addMeta`. + - slug: typescript + package: '@tanstack/table-core' + domain: foundations type: core - packages: - - '@tanstack/table-core' - covers: - - filterFn - - sortFn - - aggregationFn - - globalFilterFn - - FilterFn - - SortFn - - AggregationFn - - addMeta - - autoRemove - - resolveFilterValue - - columnFiltersMeta - - filterFns - - sortFns - - aggregationFns - - FilterFnOption - - SortFnOption - - AggregationFnOption - - BuiltInFilterFn - - BuiltInSortFn - - BuiltInAggregationFn - - invertSorting - - sortUndefined - - sortDescFirst - tasks: - - Author a custom `filterFn` (e.g. fuzzy filter) and reference it by name from a column's `filterFn` - option after registering it via `createFilteredRowModel({ ...filterFns, custom })` - - 'Use the `addMeta` argument inside a `filterFn` to stash ranking info on the row, then read it from - a custom `sortFn` via `row.columnFiltersMeta[columnId]` for filter→sort handoff (canonical: match-sorter-utils - fuzzy)' - - Pick a built-in `sortFn` by string name (`alphanumeric`, `text`, `datetime`, `basic`) for a column, - then layer `invertSorting`/`sortDescFirst`/`sortUndefined` for direction control - - Register a custom `aggregationFn` for a grouped column (e.g. weighted average) by passing it through - `createGroupedRowModel({ ...aggregationFns, custom })` + purpose: 'Preserve userland inference with columnHelper, meta helpers, tableOptions, and feature-derived types instead of manually threading generics.' + sources: + [ + 'TanStack/table:docs/guide/helpers.md', + 'TanStack/table:docs/guide/column-defs.md', + 'TanStack/table:docs/guide/table-and-column-meta.md', + 'TanStack/table:packages/table-core/src/helpers', + ] failure_modes: - - mistake: References a custom filterFn by string without registering it - mechanism: - "String values for `filterFn` are looked up in `table._rowModelFns.filterFns`. If the custom - fn is not passed to `createFilteredRowModel({ ...filterFns, custom: myFn })`, the lookup misses - and `column_getFilterFn` logs `Could not find a valid 'column.filterFn' for column with the ID: - X`. The column then falls back to a no-op and the filter silently fails." - wrong_pattern: | - // ❌ "fuzzy" string never registered - const table = useTable({ - features, columns: [ - columnHelper.accessor('fullName', { filterFn: 'fuzzy' }), - ], - rowModels: { - filteredRowModel: createFilteredRowModel(filterFns), // ❌ no fuzzy in here - }, - data, - }) - correct_pattern: | - // ✅ register the custom fn AND module-augment for type safety - declare module '@tanstack/react-table' { - interface FilterFns { - fuzzy: FilterFn - } - } + - 'Annotating heterogeneous helper-built columns as ColumnDef[] and erasing each accessor value type.' + - 'Manually supplying deep Table feature generics rather than deriving them from typeof features or letting helpers infer them.' + - 'Using v8 global declaration merging for per-table meta when v9 tableMeta, columnMeta, filterMeta, and meta helpers can scope the types.' - const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { - const itemRank = rankItem(row.getValue(columnId), value) - addMeta?.({ itemRank }) - return itemRank.passed - } - - const table = useTable({ - features, - columns: [columnHelper.accessor('fullName', { filterFn: 'fuzzy' })], - rowModels: { - filteredRowModel: createFilteredRowModel({ ...filterFns, fuzzy: fuzzyFilter }), - }, - data, - }) - source: examples/react/filters-fuzzy/src/main.tsx:37-80,117-127; packages/table-core/src/features/column-filtering/columnFilteringFeature.utils.ts:85-108 - priority: CRITICAL - status: active - version_context: - v9 alpha; v9 made built-in fns into explicit arguments to row-model factories so - unregistered string names fail loudly in dev but quietly in prod - - mistake: Uses v8 `sortingFn` / `sortingFns` names in v9 - mechanism: - 'v9 renamed every sorting API: `sortingFn` → `sortFn`, `sortingFns` → `sortFns`, `SortingFn` - type → `SortFn`, `column.getSortingFn()` → `column.getSortFn()`. The old names typecheck only if - you accidentally pull from the legacy export and silently ignore your config.' - wrong_pattern: | - // ❌ v8 names — ignored in v9 column defs - columnHelper.accessor('age', { - sortingFn: 'alphanumeric', - }) - correct_pattern: | - // ✅ v9 names - columnHelper.accessor('age', { - sortFn: 'alphanumeric', - }) - source: docs/framework/react/guide/migrating.md:877-908; packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts:71-77 - priority: HIGH - status: active - version_context: v9 alpha; affects users upgrading from v8 - - mistake: Custom sortFn reads meta from a different column-filter id - mechanism: - '`row.columnFiltersMeta` is keyed by the column id that produced the meta (or `"__global__"` - for the global filter). A sortFn must look up the SAME column id the filterFn used. The fuzzy filter→sort - pattern only works when both reference the same column.' - wrong_pattern: | - // ❌ filter on 'fullName', sort reads meta from 'firstName' - const fuzzySort: SortFn = (a, b, columnId) => { - const meta = a.columnFiltersMeta['firstName'] // ❌ wrong key - return meta ? compareItems(meta.itemRank, b.columnFiltersMeta['firstName'].itemRank) : 0 - } - columnHelper.accessor('fullName', { filterFn: 'fuzzy', sortFn: fuzzySort }) - correct_pattern: | - // ✅ sortFn uses the same columnId arg that the row's filterFn ran on - const fuzzySort: SortFn = (rowA, rowB, columnId) => { - let dir = 0 - if (rowA.columnFiltersMeta[columnId]) { - dir = compareItems( - rowA.columnFiltersMeta[columnId].itemRank!, - rowB.columnFiltersMeta[columnId].itemRank!, - ) - } - return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir - } - source: - examples/react/filters-fuzzy/src/main.tsx:56-70; packages/table-core/src/features/column-filtering/createFilteredRowModel.ts:127-156 - (addMeta wiring) - priority: HIGH - status: active - version_context: always present - - mistake: Returns a complex value from accessor and uses a built-in sortFn - mechanism: - Built-in sortFns (`alphanumeric`, `text`, `basic`) coerce values via `toString()`/comparison - operators. An accessor that returns `{first, last}` will compare `"[object Object]"` strings — every - row ties and the original index ordering leaks through. The fix is to either return a primitive - from the accessor OR supply a sortFn that handles the shape. - wrong_pattern: | - // ❌ accessor returns object; alphanumeric sort sees "[object Object]" - columnHelper.accessor((row) => row.name, { - id: 'name', - sortFn: 'alphanumeric', - }) - correct_pattern: | - // ✅ Option A: return a primitive - columnHelper.accessor((row) => `${row.name.first} ${row.name.last}`, { - id: 'fullName', - sortFn: 'alphanumeric', - }) - // ✅ Option B: custom sortFn that knows the shape - columnHelper.accessor((row) => row.name, { - id: 'name', - sortFn: (a, b, id) => { - const av = a.getValue<{ first: string }>(id).first - const bv = b.getValue<{ first: string }>(id).first - return av === bv ? 0 : av > bv ? 1 : -1 - }, - }) - source: packages/table-core/src/fns/sortFns.ts:121-149; docs/guide/column-defs.md:231 - priority: MEDIUM - status: active - version_context: always present - skills: - - column-definitions - - mistake: Confuses `aggregationFn` with `aggregatedCell` - mechanism: - '`aggregationFn` produces the value for a grouped row (e.g. sum, mean). `aggregatedCell` - renders that value. New users put rendering logic inside `aggregationFn`, breaking re-renders and - string output.' - wrong_pattern: | - // ❌ rendering JSX inside the aggregation function - columnHelper.accessor('revenue', { - aggregationFn: (id, leaves) => ${leaves.reduce((a, r) => a + r.getValue(id), 0)}, - }) - correct_pattern: | - // ✅ aggregationFn returns a value; aggregatedCell renders it - columnHelper.accessor('revenue', { - aggregationFn: 'sum', // or a custom (id, leaves, children) => number - aggregatedCell: (info) => ${info.getValue().toLocaleString()}, - }) - source: packages/table-core/src/features/column-grouping/columnGroupingFeature.types.ts:53-78; packages/table-core/src/fns/aggregationFns.ts:1-242 - priority: MEDIUM - status: active - version_context: always present - - slug: filtering + - slug: api-not-found + package: '@tanstack/table-core' + domain: foundations type: core - packages: - - '@tanstack/table-core' - domain: row-model-features - description: - Filter rows in TanStack Table v9 — column filters, a global filter, faceted facet values - for filter UIs, and fuzzy ranking — using the `filteredRowModel` row-model pipeline stage. - subsystems: - - column-filtering - - global-filtering - - column-faceting - - global-faceting - - fuzzy-filtering - covers: - - columnFilteringFeature - - globalFilteringFeature - - columnFacetingFeature - - createFilteredRowModel(filterFns) - - createFacetedRowModel() - - createFacetedUniqueValues() - - createFacetedMinMaxValues() - - state.columnFilters (ColumnFiltersState = Array<{ id, value }>) - - state.globalFilter (any) - - onColumnFiltersChange - - onGlobalFilterChange - - columnDef.filterFn (string name | function | "auto") - - columnDef.enableColumnFilter - - columnDef.enableGlobalFilter - - manualFiltering - - enableFilters - - enableColumnFilters - - enableGlobalFilter - - globalFilterFn - - filterFromLeafRows - - maxLeafRowFilterDepth - - getColumnCanGlobalFilter - - getFacetedUniqueValues (server-side override) - - getFacetedMinMaxValues (server-side override) - - getGlobalFacetedUniqueValues (server-side override) - - getGlobalFacetedMinMaxValues (server-side override) - - table.setColumnFilters - - table.resetColumnFilters - - table.setGlobalFilter - - table.resetGlobalFilter - - table.getGlobalAutoFilterFn - - table.getGlobalFilterFn - - column.getFilterValue / setFilterValue / getCanFilter / getIsFiltered / getFilterIndex / getAutoFilterFn - / getFilterFn - - column.getCanGlobalFilter - - column.getFacetedRowModel / getFacetedUniqueValues / getFacetedMinMaxValues - - table.getGlobalFacetedRowModel / getGlobalFacetedUniqueValues / getGlobalFacetedMinMaxValues - - filterFns built-in registry - - filterFn.autoRemove - - filterFn.resolveFilterValue - - '@tanstack/match-sorter-utils (rankItem, compareItems, RankingInfo)' - - row.columnFiltersMeta (FilterMeta extended via declare module) - - addMeta callback (4th argument to FilterFn) - tasks: - - Wire up text/range/select per-column filters with debounced inputs (see examples/react/filters/src/main.tsx). - - 'Add a faceted filter UI: autocomplete from `column.getFacetedUniqueValues()` and a range slider from - `column.getFacetedMinMaxValues()` (requires `createFacetedRowModel()` PLUS the unique/minMax row models).' - - Implement fuzzy global search with `@tanstack/match-sorter-utils` and a custom `fuzzySort` that reads - `row.columnFiltersMeta[columnId].itemRank`. - - 'Switch a table to manual server-side filtering: set `manualFiltering: true`, omit the `filteredRowModel` - from `rowModels`, and pass pre-filtered `data` from the server.' - - 'Make a tree table filter sub-rows by enabling `filterFromLeafRows: true` so a parent stays visible - whenever any child matches.' + purpose: 'Diagnose missing exports, options, state, and instance methods before inventing replacement code.' + sources: + [ + 'TanStack/table:packages/table-core/src/index.ts', + 'TanStack/table:packages/table-core/src/types/TableFeatures.ts', + 'TanStack/table:docs/framework/react/guide/migrating.md', + ] failure_modes: - - mistake: - Forgetting `createFacetedRowModel()` while still registering `createFacetedUniqueValues()` - / `createFacetedMinMaxValues()`. - mechanism: | - `createFacetedUniqueValues` and `createFacetedMinMaxValues` both compute their results from `column.getFacetedRowModel().flatRows`. Without the base `facetedRowModel` factory in `rowModels.facetedRowModel`, `column_getFacetedRowModel` falls back to `table.getPreFilteredRowModel()` (see column-faceting/columnFacetingFeature.utils.ts lines 44-56), so facet values are still computed but **don't exclude the column's own active filter** — meaning a select dropdown showing options for a column will collapse to only the currently selected value once the user picks one. - wrong_pattern: | - ```tsx - const table = useTable({ - features: tableFeatures({ columnFacetingFeature, columnFilteringFeature }), - rowModels: { - filteredRowModel: createFilteredRowModel(filterFns), - // BUG: missing facetedRowModel - the unique/minMax helpers will use - // the pre-filtered model so they include every filter EXCEPT none - facetedUniqueValues: createFacetedUniqueValues(), - facetedMinMaxValues: createFacetedMinMaxValues(), - }, - columns, data, - }) - ``` - correct_pattern: | - ```tsx - // From examples/react/filters-faceted/src/main.tsx - const table = useTable({ - features: tableFeatures({ - columnFacetingFeature, - columnFilteringFeature, - rowPaginationFeature, - }), - rowModels: { - filteredRowModel: createFilteredRowModel(filterFns), - paginatedRowModel: createPaginatedRowModel(), - facetedRowModel: createFacetedRowModel(), // REQUIRED base - facetedMinMaxValues: createFacetedMinMaxValues(), - facetedUniqueValues: createFacetedUniqueValues(), - }, - columns, data, - }) - ``` - source: packages/table-core/src/features/column-faceting/columnFacetingFeature.utils.ts - priority: high - status: active - version_context: v9.0.0-alpha.48 - skills: - - filtering - - mistake: - 'Using `manualFiltering: true` without removing the `filteredRowModel` factory (works), or - expecting `manualFiltering` to filter the row model anyway.' - mechanism: | - When `manualFiltering: true`, `table_getFilteredRowModel` short-circuits and returns `getPreFilteredRowModel()` (the core row model). The filter state still exists and `onColumnFiltersChange` still fires — but rows are NOT filtered client-side. Agents commonly add the filter UI, hook up `setFilterValue`, and forget to refetch server data, leaving filter UI changes invisible. - wrong_pattern: | - ```tsx - // BUG: manualFiltering: true means the filteredRowModel is BYPASSED. - // Filter state changes do nothing visible unless `data` is refetched. - const table = useTable({ - features: tableFeatures({ columnFilteringFeature }), - rowModels: { - filteredRowModel: createFilteredRowModel(filterFns), - }, - data, - columns, - manualFiltering: true, - // ...but no useEffect to refetch when columnFilters changes - }) - ``` - correct_pattern: | - ```tsx - // From docs/guide/column-filtering.md - const [columnFilters, setColumnFilters] = useState([]) - const { data } = useQuery(['rows', columnFilters], () => - fetch('/api/rows?' + serialize(columnFilters)).then(r => r.json()) - ) - const table = useTable({ - features: tableFeatures({ columnFilteringFeature }), - rowModels: {}, // no filteredRowModel needed for manual filtering - data, - columns, - manualFiltering: true, - state: { columnFilters }, - onColumnFiltersChange: setColumnFilters, - }) - ``` - source: packages/table-core/src/core/row-models/coreRowModelsFeature.utils.ts - priority: high - status: active - version_context: v9.0.0-alpha.48 - skills: - - filtering - - mistake: - 'Custom fuzzy filter without merging into `filterFns`: passing only `{ fuzzy: fuzzyFilter - }` to `createFilteredRowModel`, dropping the 10 built-in filters.' - mechanism: | - `createFilteredRowModel(filterFns)` accepts a `Record`. The argument is stored AS-IS at `table._rowModelFns.filterFns`, replacing the registry. If a developer writes `createFilteredRowModel({ fuzzy: fuzzyFilter })`, then any column with `filterFn: 'includesString'` will trigger `Could not find a valid 'column.filterFn' for column with the ID: ...` (warning at columnFilteringFeature.utils.ts:102) and the column won't filter. - wrong_pattern: | - ```tsx - // BUG: drops the built-in registry - filteredRowModel: createFilteredRowModel({ - fuzzy: fuzzyFilter, - }), - // Column with filterFn: 'includesString' now warns and never filters - ``` - correct_pattern: | - ```tsx - // From examples/react/filters-fuzzy/src/main.tsx - import { filterFns, sortFns } from '@tanstack/react-table' - import { rankItem, compareItems } from '@tanstack/match-sorter-utils' - - declare module '@tanstack/react-table' { - interface FilterFns { fuzzy: FilterFn } - interface FilterMeta { itemRank?: RankingInfo } - } + - 'Searching v8 docs or recalling v7 APIs instead of checking the installed v9 package src and exact package version.' + - 'Assuming an API was removed when its feature was simply omitted from tableFeatures.' + - 'Using object spread, Object.keys, or JSON serialization to discover v9 prototype methods and concluding those methods do not exist.' - const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { - const itemRank = rankItem(row.getValue(columnId), value) - addMeta?.({ itemRank }) - return itemRank.passed - } - - const table = useTable({ - features, - rowModels: { - filteredRowModel: createFilteredRowModel({ - ...filterFns, // KEEP built-ins - fuzzy: fuzzyFilter, // ADD custom - }), - sortedRowModel: createSortedRowModel(sortFns), - }, - globalFilterFn: 'fuzzy', - columns, data, - }) - ``` - source: examples/react/filters-fuzzy/src/main.tsx - priority: high - status: active - version_context: v9.0.0-alpha.48 - skills: - - filtering - - sorting - - mistake: - Using `state.columnFilters` AND `initialState.columnFilters` at the same time (or same for - globalFilter). - mechanism: | - When both are set, the controlled `state.columnFilters` value overrides the `initialState.columnFilters` on every render. The initial state is effectively ignored. This pattern usually signals confusion between controlled and uncontrolled state ownership — and developers often expect `initialState` to seed `state` on first render, which TanStack Table does NOT do. - wrong_pattern: | - ```tsx - const [columnFilters, setColumnFilters] = useState([]) - // ^^ empty - const table = useTable({ - features, rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, - columns, data, - initialState: { - columnFilters: [{ id: 'name', value: 'John' }], // IGNORED - }, - state: { columnFilters }, // wins, starts empty - onColumnFiltersChange: setColumnFilters, - }) - ``` - correct_pattern: | - ```tsx - // Seed the controlled state at useState time, NOT in initialState. - // From docs/guide/column-filtering.md - const [columnFilters, setColumnFilters] = useState([ - { id: 'name', value: 'John' }, // seeded here - ]) - const table = useTable({ - features, rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, - columns, data, - state: { columnFilters }, - onColumnFiltersChange: setColumnFilters, - }) - ``` - source: docs/guide/column-filtering.md - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - filtering - - mistake: - Globally filtering on columns whose values aren't strings or numbers and getting silently-ignored - search. - mechanism: | - `globalFilteringFeature` defaults `getColumnCanGlobalFilter` to a function that returns `typeof value === 'string' || typeof value === 'number'` from sampling the first row's cell value. Objects, arrays, booleans, dates, and undefined values fail this default check and the column is excluded from the global filter scan. There is no warning — the column just doesn't participate. - wrong_pattern: | - ```tsx - // BUG: createdAt is a Date object — global filter silently skips it. - const columns = [ - columnHelper.accessor('createdAt', { header: 'Created' }), - columnHelper.accessor('name', { header: 'Name' }), - ] - // table.setGlobalFilter('2024') will never find Date rows - ``` - correct_pattern: | - ```tsx - // Override getColumnCanGlobalFilter or set per-column. - const table = useTable({ - features: tableFeatures({ globalFilteringFeature }), - rowModels: { filteredRowModel: createFilteredRowModel(filterFns) }, - columns, data, - globalFilterFn: 'includesString', - getColumnCanGlobalFilter: (column) => true, // include every column - }) - // Or per-column: - columnHelper.accessor('createdAt', { - header: 'Created', - enableGlobalFilter: true, - }) - ``` - source: packages/table-core/src/features/global-filtering/globalFilteringFeature.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - filtering - - mistake: Expecting `filterFromLeafRows` to keep child rows visible when only the parent matches. - mechanism: | - `filterFromLeafRows: true` walks the tree bottom-up and keeps a parent if ANY descendant matches. The inverse — keeping all descendants when the parent matches — is the **default** root-down behavior (see filterRowsUtils.ts:103). The two modes are mutually exclusive. To preserve all sub-rows under a matching parent without checking children, use `maxLeafRowFilterDepth: 0` so only root-level rows are filtered. - wrong_pattern: | - ```tsx - // BUG: filterFromLeafRows hides ALL children that don't match, - // even though the parent does match. - const table = useTable({ - features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), - rowModels: { filteredRowModel: createFilteredRowModel(filterFns), expandedRowModel: createExpandedRowModel() }, - columns, data, - getSubRows: r => r.subRows, - filterFromLeafRows: true, - // expectation: parent matches "John" -> all children visible - // reality: only children that also match "John" stay visible - }) - ``` - correct_pattern: | - ```tsx - // Keep parent's whole sub-tree when parent matches: filter root-only. - const table = useTable({ - features: tableFeatures({ columnFilteringFeature, rowExpandingFeature }), - rowModels: { - filteredRowModel: createFilteredRowModel(filterFns), - expandedRowModel: createExpandedRowModel(), - }, - columns, data, - getSubRows: r => r.subRows, - maxLeafRowFilterDepth: 0, // only filter root-level rows - }) - ``` - source: packages/table-core/src/features/column-filtering/filterRowsUtils.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - filtering - - row-expanding - - mistake: Column filter returns no matches when first row has null in that column - mechanism: | - The default `auto` filter type detects column type from the first row's value. - If row 0 has `null`/`undefined` for the column, type detection picks the wrong - filter and never matches. - wrong_pattern: | - // ❌ First row has null; column filter returns 0 results - const data = [ - { id: 1, name: null }, - { id: 2, name: 'Alice' }, - ] - correct_pattern: | - // ✅ Explicitly set filterFn on the column instead of relying on auto - columnHelper.accessor('name', { - filterFn: 'includesString', // or a custom fn - }) - source: - https://github.com/TanStack/table/issues/4711 (Column filter not working when first value - in data is null), https://github.com/TanStack/table/issues/4919 (Filter not working if accessorFn - returns null) - priority: MEDIUM - status: active - version_context: v8 behavior - skills: - - filtering - - column-definitions - - mistake: getFilteredRowModel retains memory after filter removal - mechanism: | - Heap snapshots show `PerformanceMeasure`, `FiberNode`, and `Error` objects - accumulating across filter-apply/remove cycles. The internal caching mechanism - in `getFilteredRowModel` doesn't release filtered row arrays even after filter - state is cleared. - wrong_pattern: | - // ❌ Memory grows with each filter toggle - const table = useReactTable({ - data, columns, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - }) - correct_pattern: | - // ✅ Manual client-side filter as workaround - const filteredData = useMemo(() => { - if (!columnFilters.length) return data - return data.filter(row => - columnFilters.every(f => /* manual matching */ true) - ) - }, [data, columnFilters]) - const table = useReactTable({ data: filteredData, columns, getCoreRowModel: getCoreRowModel() }) - source: - https://github.com/TanStack/table/issues/6170 (Memory leak in getFilteredRowModel - heap not - released after filter removal) - priority: MEDIUM - status: active - version_context: v8.21.3 — relevant for long-running apps; v9 row-model API may behave differently - skills: - - filtering - - production-readiness - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - reference_candidates: - - name: built-in filterFns - kind: function-registry - count: 12 - source: packages/table-core/src/fns/filterFns.ts - documented_in_guide: - - includesString - - includesStringSensitive - - equalsString - - equalsStringSensitive - - arrIncludes - - arrIncludesAll - - arrIncludesSome - - equals - - weakEquals - - inNumberRange - also_exported: - - arrHas - - between - - betweenInclusive - - greaterThan - - greaterThanOrEqualTo - - lessThan - - lessThanOrEqualTo - note: - "Docs list the built-in filterFn registry but the registry actually exports 12 (adds `arrHas`, - `between`, `betweenInclusive`). `greaterThan`, `greaterThanOrEqualTo`, `lessThan`, `lessThanOrEqualTo` - are NOT included in the registry export but are used by the range filters. Expanding example uses - `filterFn: 'between'` — which IS in the registry." - gaps: - - Docs say the built-in filterFn registry but the actual `filterFns` registry exports 12. Need authoritative - reference for the full set. - - '`column.getCanGlobalFilter` returns `false` for columns with first-row non-string/non-number values; - this is the SILENT failure case. Worth a dedicated reference table for which value types pass auto-detection.' - - "`addMeta` is optional in the FilterFn signature — fuzzy example uses `addMeta?.()`. Need clear rule - for when meta is/isn't passed (it IS passed by `createFilteredRowModel`)." - - No documented invariant on what happens when `filterFn` resolves to undefined; the warning is dev-mode - only and the column silently uses no filter. - - slug: sorting + - slug: custom-features + package: '@tanstack/table-core' + domain: foundations type: core - packages: - - '@tanstack/table-core' - domain: row-model-features - description: - Sort rows in TanStack Table v9 with the `sortedRowModel` stage — built-in sortFns, custom - sortFns, multi-sort, sortUndefined placement, invertSorting for "lower-is-better" scales, and manual - server-side sorting. - covers: - - rowSortingFeature - - createSortedRowModel(sortFns) - - state.sorting (SortingState = Array<{ id, desc }>) - - onSortingChange - - 'columnDef.sortFn (string | function | "auto") # v9 renamed from sortingFn' - - columnDef.sortDescFirst - - columnDef.sortUndefined (false | -1 | 1 | "first" | "last") - - columnDef.invertSorting - - columnDef.enableSorting - - columnDef.enableMultiSort - - manualSorting - - enableSorting - - enableSortingRemoval - - enableMultiSort - - enableMultiRemove - - maxMultiSortColCount - - isMultiSortEvent - - sortDescFirst - - table.setSorting - - table.resetSorting - - column.getCanSort / getIsSorted / getSortIndex / getCanMultiSort / clearSorting - - column.getToggleSortingHandler / toggleSorting - - column.getFirstSortDir / getNextSortingOrder / getAutoSortDir / getAutoSortFn / getSortFn - - 'sortFns built-in registry # v9 renamed from sortingFns' - tasks: - - Add clickable column-header sorting with multi-sort on Shift+click (see examples/react/sorting/src/main.tsx). - - 'Wire a column-specific custom `sortFn` for an enum (e.g. status: single < complicated < relationship).' - - "Sort nullable values with explicit placement using `sortUndefined: 'last'` so undefined never confuses - the auto sortFn picker." - - "Use `invertSorting: true` on a `rank` column so 1st ranks above 2nd even when the user clicks 'descending'." - - 'Switch to server-side sorting: `manualSorting: true`, omit `sortedRowModel`, mirror `state.sorting` - to the fetch URL.' + purpose: 'Extend Table through every v9 FeatureMap and table/column/row/cell/header API lifecycle surface only when built-in options, meta, and APIs are insufficient.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/custom-features.md', + 'TanStack/table:packages/table-core/src/types', + 'TanStack/table:packages/table-core/src/types/TableFeatures.ts', + 'TanStack/table:packages/table-core/src/utils.ts', + 'TanStack/table:packages/table-core/src/features', + 'TanStack/table:examples/react/custom-plugin', + ] failure_modes: - - mistake: Using v8's `sortingFn` / `sortingFns` after upgrading to v9. - mechanism: | - v9 renamed `columnDef.sortingFn` -> `columnDef.sortFn`, `tableOptions.sortingFns` -> `tableOptions.sortFns`, and the exported registry from `sortingFns` -> `sortFns`. The new column option `sortFn` defaults to `'auto'` and looks up `column.table._rowModelFns.sortFns` (see rowSortingFeature.utils.ts:160-173). A column with the v8 `sortingFn: 'alphanumeric'` will silently fall through to `sortFn_basic` (via the `?? sortFn_basic` fallback at line 172). - wrong_pattern: | - ```tsx - // v8-style column def — works without type error if `as any`, sorts wrong - { - accessorKey: 'fullName', - sortingFn: 'alphanumeric', // v8 name - } - // useTable({ sortingFns: { ...sortingFns, myFn } }) // v8 option name - ``` - correct_pattern: | - ```tsx - // v9 names. From examples/react/sorting/src/main.tsx - import { sortFns, createSortedRowModel } from '@tanstack/react-table' - - columnHelper.accessor('firstName', { - sortFn: 'alphanumeric', // v9 name - }) - - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { - sortedRowModel: createSortedRowModel({ // pass registry here - ...sortFns, - myCustom: (a, b, id) => a.original[id] - b.original[id], - }), - }, - columns, data, - }) - ``` - source: packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts - priority: high - status: active - version_context: v9.0.0-alpha.48 - skills: - - sorting - - mistake: "Expecting `sortUndefined: 'first' | 'last'` to work in v8 (they only exist in v9)." - mechanism: | - v8 only had `sortUndefined: false | -1 | 1`. v9 added the `'first'` and `'last'` literal aliases. The createSortedRowModel switch at lines 100-110 has explicit branches: `if (sortUndefined === 'first') return aUndefined ? -1 : 1`. With `1` (the v9 default), undefined sorts ascending toward the END (lower priority); with `-1`, it sorts toward the START. The semantic difference: numeric `1`/`-1` flip when `desc: true`; `'first'`/`'last'` are absolute. - wrong_pattern: | - ```tsx - // BUG: agent assumes 'first' = ascending-start. But with desc: true, - // numeric 1 flips to "first" anyway. The literal forms are ABSOLUTE. - { accessorKey: 'lastName', sortUndefined: -1 } // ascending-first, descending-LAST - ``` - correct_pattern: | - ```tsx - // From examples/react/sorting/src/main.tsx - columnHelper.accessor((row) => row.lastName, { - id: 'lastName', - sortUndefined: 'last', // ABSOLUTE: always at end regardless of asc/desc - sortDescFirst: false, // nullable values can mess up auto detection - }), - ``` - source: packages/table-core/src/features/row-sorting/createSortedRowModel.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - sorting - - mistake: Custom `sortFn` returns `desc`-aware values (i.e. negating inside the function). - mechanism: | - From sorting.md: "The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take of that logic." `createSortedRowModel` multiplies the return by `-1` when `isDesc`, then again by `-1` if `invertSorting` is true (lines 119-126). A custom sortFn that returns -1 for "A before B" when descending will get doubly-flipped. - wrong_pattern: | - ```tsx - // BUG: takes sort direction into account, breaks toggle - const customSort: SortFn = (a, b, id, desc) => { - // desc isn't even a parameter — but agents try to detect via state - const cmp = a.original[id] - b.original[id] - return desc ? -cmp : cmp - } - ``` - correct_pattern: | - ```tsx - // From examples/react/sorting/src/main.tsx - // Always return ascending-order comparison; the row model handles desc. - const sortStatusFn: SortFn = (rowA, rowB, _columnId) => { - const statusOrder = ['single', 'complicated', 'relationship'] - return statusOrder.indexOf(rowA.original.status) - - statusOrder.indexOf(rowB.original.status) - } - ``` - source: packages/table-core/src/features/row-sorting/createSortedRowModel.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - sorting - - mistake: - Using fuzzy filter on a column but not pairing it with a fuzzy-aware sortFn that reads `itemRank` - from `columnFiltersMeta`. - mechanism: | - The fuzzy filter writes `{ itemRank }` into `row.columnFiltersMeta[columnId]` via the `addMeta` callback (see createFilteredRowModel.ts lines 132-136). To rank results by match quality, a custom sortFn must read this meta and call `compareItems` from `@tanstack/match-sorter-utils`. If the column uses `sortFn: 'alphanumeric'` (the default), rows are sorted alphabetically, NOT by closest match — defeating the whole point of fuzzy search. - wrong_pattern: | - ```tsx - columnHelper.accessor(...{ - filterFn: 'fuzzy', - // BUG: missing sortFn — rows sort alphabetically, not by rank - }) - ``` - correct_pattern: | - ```tsx - // From examples/react/filters-fuzzy/src/main.tsx - import { compareItems } from '@tanstack/match-sorter-utils' - import { sortFns } from '@tanstack/react-table' + - 'Building a custom feature for behavior already covered by column meta, table meta, or a built-in feature API.' + - 'Adding types without registering matching initial state, default options, state updaters, and prototype/table APIs in the feature lifecycle.' + - 'Mutating table instances ad hoc instead of registering a stable feature in tableFeatures, losing type inference and composition.' + - 'Assuming the density example exhausts the extension surface and overlooking column-def, column, row, cell, header, or advanced row-model FeatureMaps.' + - 'Looking for nonexistent assignColumnAPIs/assignRowAPIs helpers instead of using assignPrototypeAPIs inside the matching TableFeature prototype hook.' - const fuzzySort: SortFn = (rowA, rowB, columnId) => { - let dir = 0 - if (rowA.columnFiltersMeta[columnId]) { - dir = compareItems( - rowA.columnFiltersMeta[columnId].itemRank!, - rowB.columnFiltersMeta[columnId].itemRank!, - ) - } - return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir - } - - columnHelper.accessor(..., { - filterFn: 'fuzzy', - sortFn: fuzzySort, - }) - ``` - source: examples/react/filters-fuzzy/src/main.tsx - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - sorting - - filtering - - mistake: getCanSort returns false with manualSorting when no accessor is set - mechanism: | - `getCanSort` checks for `accessorKey`/`accessorFn` even when `manualSorting: true`, - so display columns (no accessor) can't be sorted server-side. Users with virtual - "actions" columns or computed display-only columns hit this immediately. - wrong_pattern: | - // ❌ getCanSort() returns false even though manualSorting is true - const table = useReactTable({ - manualSorting: true, - columns: [ - { id: 'computed', header: 'Computed', cell: (info) => row.x + row.y }, - ], - ... - }) - correct_pattern: | - // ✅ Provide an accessorFn even if value is unused, OR force enableSorting on the column - columnHelper.display({ - id: 'computed', - header: 'Computed', - enableSorting: true, // force-enable for manualSorting - cell: (info) => info.row.original.x + info.row.original.y, - }) - source: - https://github.com/TanStack/table/issues/4136 (getCanSort returning false when manualSorting - enabled, 14 comments) - priority: MEDIUM - status: active - version_context: v8 / v9 - - mistake: Manual sorting bounces ascending→false→ascending when values tie on current page - mechanism: | - With `manualSorting: true`, when sort state cycles through - asc → desc → unsorted, the table compares `row.original` references for - determinism. If all visible values are equal (e.g. all empty strings), the - "desc" stage produces the same order as "asc" and looks like the sort isn't - changing — users see asc → unsorted → asc. - wrong_pattern: | - // ❌ User-visible: clicking sort goes asc → unsorted → asc when values tie - const table = useReactTable({ manualSorting: true, ... }) - correct_pattern: | - // ✅ Add a secondary stable tiebreaker server-side, and surface the actual sort - // direction from server-controlled state to the UI rather than relying on - // table.getState().sorting alone. - source: - https://github.com/TanStack/table/issues/5147 (Sorting direction not updating properly with - manual sorting, 10 comments) - priority: MEDIUM - status: active - version_context: v8 manual sorting - skills: - - sorting - - client-to-server - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - reference_candidates: - - name: built-in sortFns - kind: function-registry - count: 6 - source: packages/table-core/src/fns/sortFns.ts - entries: - - alphanumeric - - alphanumericCaseSensitive - - text - - textCaseSensitive - - datetime - - basic - note: - 'Default fallback when no sortFn matches is `sortFn_basic` (see rowSortingFeature.utils.ts:172). - `auto` infers from sampled values: date -> datetime, mixed alphanumeric -> alphanumeric, string - -> text, else basic.' - gaps: - - '`column_getAutoSortFn` samples `firstRows = getFilteredRowModel().flatRows.slice(10)` — this is `.slice(10)`, - NOT `.slice(0, 10)`! It skips the first 10 rows and uses everything after. Likely a bug; verify with - maintainer.' - - "`getSelectedRowModel`, `getFilteredSelectedRowModel`, and `getGroupedSelectedRowModel` in rowSelectionFeature.utils.ts - ALL currently call `selectRowsFn(rowModel)` against `table.getCoreRowModel()` — they appear identical. - Either they're stubs awaiting wiring or the dispatch is missing. Worth flagging." - - slug: pagination - type: core - packages: - - '@tanstack/table-core' - domain: row-model-features - description: - Paginate rows in TanStack Table v9 with the `paginatedRowModel` stage — page navigation - APIs, total row/page count, automatic page reset on data changes, and manual server-side pagination. - covers: - - rowPaginationFeature - - createPaginatedRowModel() - - 'state.pagination ({ pageIndex, pageSize }) # defaults: { pageIndex: 0, pageSize: 10 }' - - onPaginationChange - - manualPagination - - pageCount - - rowCount - - autoResetPageIndex - - paginateExpandedRows - - table.setPagination / resetPagination - - table.setPageIndex / resetPageIndex - - table.setPageSize / resetPageSize - - table.nextPage / previousPage / firstPage / lastPage - - table.getCanNextPage / getCanPreviousPage - - table.getPageCount / getRowCount / getPageOptions - - table.getPaginatedRowModel / getPrePaginatedRowModel - tasks: - - Add a pagination toolbar with `<<`, `<`, `>`, `>>` buttons, current page indicator, page size selector, - and 'go to page' input (see examples/react/pagination/src/main.tsx). - - 'Switch to server-side pagination: `manualPagination: true`, supply `rowCount` (or `pageCount`) from - the server, omit `paginatedRowModel`, refetch when `state.pagination` changes.' - - 'Stop the automatic page-index reset when filters change: `autoResetPageIndex: false` (e.g., to keep - page 3 visible while a user types into a search box).' - - 'Display total filtered rows even when paginated: `table.getPrePaginatedRowModel().rows.length` (see - filters example).' + - slug: migrate-v8-to-v9 + package: '@tanstack/table-core' + domain: foundations + type: migration + purpose: 'Route framework-specific migration while enforcing the shared v9 feature, row-model, instance-method, naming, and type changes.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/migrating.md', + 'TanStack/table:docs/framework/preact/guide/migrating.md', + 'TanStack/table:docs/framework/solid/guide/migrating.md', + 'TanStack/table:docs/framework/svelte/guide/migrating.md', + 'TanStack/table:docs/framework/vue/guide/migrating.md', + 'TanStack/table:docs/framework/angular/guide/migrating.md', + 'TanStack/table:docs/framework/lit/guide/migrating.md', + 'TanStack/table:packages/table-core/src/index.ts', + 'TanStack/table:packages/table-core/src/types/TableFeatures.ts', + 'TanStack/table:packages/table-core/src/features/column-pinning/columnPinningFeature.types.ts', + 'TanStack/table:packages/table-core/src/features/column-resizing/columnResizingFeature.types.ts', + 'TanStack/table:packages/react-table/src/legacy.ts', + ] + failure_modes: + - 'Promoting deprecated useLegacyTable as the migration target instead of a temporary stopgap for code already using it.' + - 'Leaving v8 getFilteredRowModel/getSortedRowModel/getPaginationRowModel table options in place instead of v9 create*RowModel slots on tableFeatures.' + - 'Missing shared breaking changes such as prototype-bound instance methods, sortFn naming, feature-gated APIs, or logical start/end pinning.' + + - slug: column-faceting + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Derive filter option counts and numeric ranges from the correct faceted row model.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-faceting.md', + 'TanStack/table:packages/table-core/src/features/column-faceting', + 'TanStack/table:examples/react/filters-faceted', + ] failure_modes: - - mistake: 'Setting `manualPagination: true` without supplying `rowCount` (or `pageCount`).' - mechanism: | - With `manualPagination` set, `table_getPageCount` returns `options.pageCount ?? Math.ceil(getRowCount() / pageSize)`. `table_getRowCount` returns `options.rowCount ?? getPrePaginatedRowModel().rows.length`. In manual mode, the pre-paginated row model contains only the CURRENT page's rows — so `getRowCount()` becomes the page size (e.g., 10), `getPageCount()` becomes 1, and `getCanNextPage()` returns `false`. Pagination is effectively broken. - wrong_pattern: | - ```tsx - // BUG: getPageCount returns 1, next/prev buttons are disabled - const table = useTable({ - features: tableFeatures({ rowPaginationFeature }), - rowModels: {}, - data, // only 10 rows for the current page - columns, - manualPagination: true, - state: { pagination }, - onPaginationChange: setPagination, - // missing: rowCount or pageCount - }) - ``` - correct_pattern: | - ```tsx - // From docs/guide/pagination.md - const { data: dataQuery } = useQuery(['rows', pagination], () => - fetchPage(pagination.pageIndex, pagination.pageSize), - ) - const table = useTable({ - features: tableFeatures({ rowPaginationFeature }), - rowModels: {}, - data: dataQuery.rows, - columns, - manualPagination: true, - rowCount: dataQuery.rowCount, // server tells the table the total - // OR: pageCount: dataQuery.pageCount - // OR: pageCount: -1 if unknown (next button always enabled) - state: { pagination }, - onPaginationChange: setPagination, - }) - ``` - source: packages/table-core/src/features/row-pagination/rowPaginationFeature.utils.ts - priority: high - status: active - version_context: v9.0.0-alpha.48 - skills: - - pagination - - mistake: Putting `pagination` in BOTH `state.pagination` and `initialState.pagination`. - mechanism: | - When both are set, the controlled `state.pagination` wins on every render. `initialState.pagination` is ignored. Agents often see "my pageSize default isn't working" because they wrote `useState({ pageIndex: 0, pageSize: 10 })` AND `initialState: { pagination: { pageSize: 25 } }`. The pageSize stays at 10. - wrong_pattern: | - ```tsx - // BUG: initialState ignored. - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }) - const table = useTable({ - initialState: { pagination: { pageSize: 25 } }, // IGNORED - state: { pagination }, // wins (pageSize 10) - onPaginationChange: setPagination, - // ... - }) - ``` - correct_pattern: | - ```tsx - // Seed in useState OR use initialState only — never both. - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }) - const table = useTable({ - features: tableFeatures({ rowPaginationFeature }), - rowModels: { paginatedRowModel: createPaginatedRowModel() }, - columns, data, - state: { pagination }, - onPaginationChange: setPagination, - }) - ``` - source: docs/guide/pagination.md - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - pagination - - mistake: - 'Disabling `autoResetPageIndex: false` and forgetting to clamp `pageIndex` when the data - shrinks below the current page.' - mechanism: | - `autoResetPageIndex` defaults to `!manualPagination` (so true client-side, false manual). When false and data changes, `pageIndex` stays put. `table_setPageIndex` only clamps against `options.pageCount` (max safe int when pageCount is unset/-1, see rowPaginationFeature.utils.ts:123-129) — it does NOT clamp against the current row model. Result: user sees an empty page after a destructive filter. - wrong_pattern: | - ```tsx - const table = useTable({ - features, rowModels: { paginatedRowModel: createPaginatedRowModel(), filteredRowModel: createFilteredRowModel(filterFns) }, - columns, data, - autoResetPageIndex: false, // user on page 5, then filters down to 2 pages - // -> page 5 is empty. No automatic clamp. - }) - ``` - correct_pattern: | - ```tsx - // Either leave autoResetPageIndex at default (true)... - const table = useTable({ /* ... */ }) // autoResetPageIndex undefined = on + - 'Registering columnFacetingFeature but omitting the facetedRowModel, facetedUniqueValues, or facetedMinMaxValues slot needed by the API being called.' + - 'Expecting a column facet to include that column own active filter; the faceted row model intentionally applies the other filters and excludes its own.' + - 'Computing large server-owned facet sets from the currently loaded client page and presenting incomplete counts as global results.' + + - slug: column-filtering + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Configure column filters, filter functions, metadata, nested-row direction, and client/manual ownership.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-filtering.md', + 'TanStack/table:packages/table-core/src/features/column-filtering', + 'TanStack/table:examples/react/filters', + ] + failure_modes: + - 'Setting manualFiltering and still expecting createFilteredRowModel to transform data; manual mode returns the pre-filtered model.' + - 'Using accessor values that are objects or renderer output with built-in filter functions that expect comparable primitive values.' + - 'Providing controlled columnFilters plus onColumnFiltersChange without applying both value and updater-function forms to the external source.' - // ...or clamp manually after any data-altering effect: - useEffect(() => { - const lastPage = Math.max(0, table.getPageCount() - 1) - if (table.atoms.pagination.get().pageIndex > lastPage) { - table.setPageIndex(lastPage) - } - }, [data, columnFilters]) - ``` - source: packages/table-core/src/features/row-pagination/rowPaginationFeature.utils.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - pagination - - mistake: - Computing `getRowCount()` and expecting it to match `data.length` when grouping/filtering/expansion - is active. - mechanism: | - `table_getRowCount` returns `options.rowCount ?? getPrePaginatedRowModel().rows.length`. The pre-paginated row model is the END of the pipeline before the page slice — so it reflects filtering, grouping, sorting, AND expansion. With `paginateExpandedRows: false`, expanded children are NOT in `getPrePaginatedRowModel().rows` (they're appended to the parent's page in `createPaginatedRowModel`'s expandRows call). With `paginateExpandedRows: true` (default), they ARE counted as their own paginated rows. - wrong_pattern: | - ```tsx - // BUG: agent expects getRowCount() === data.length - console.log(table.getRowCount()) // count after all transforms - console.log(data.length) // count of raw input - // These will diverge under filtering, grouping, OR a flat input != tree input. - ``` - correct_pattern: | - ```tsx - // Use the right model for the question being asked. - table.getCoreRowModel().rows.length // raw row count (flat) - table.getPreFilteredRowModel().rows.length // before filtering - table.getFilteredRowModel().rows.length // after filtering - table.getRowCount() // pre-paginated count (server count if rowCount option set) - table.getRowModel().rows.length // current page only - ``` - source: docs/guide/row-models.md - priority: low - status: active - version_context: v9.0.0-alpha.48 - skills: - - pagination - - filtering - - grouping - - mistake: getToggleAllRowsSelected behaves differently with server pagination + getRowId - mechanism: | - With server-side pagination, you must supply `getRowId` (so selections survive - page changes). But once `getRowId` is set, `getToggleAllRowsSelectedHandler` only - selects the current page's rows (because the table only knows about current-page - rows — it can't enumerate all server-side rows). Users expect "select all" to - mean truly all, identical to client-side mode. - wrong_pattern: | - // ❌ Mismatched expectations — header checkbox only affects current page - - correct_pattern: | - // ✅ Use page-aware APIs explicitly with server pagination - - // For "select all server-side rows", implement an explicit out-of-band - // selection mode — track a boolean "all rows mode" alongside the row map. - source: https://github.com/TanStack/table/issues/4781 (Pagination and Row Selection, 10 comments) - priority: HIGH - status: active - version_context: - Fundamental UX consequence of server pagination; not a bug but a recurring confusion - point - skills: - - pagination - - row-selection - - client-to-server - - mistake: autoResetPageIndex resets to initialState.pageIndex instead of 0 - mechanism: | - `_autoResetPageIndex` calls `table.resetPageIndex()` without `true`, so when data - shrinks (e.g. after applying a filter) the page resets to whatever - `initialState.pagination.pageIndex` was — typically a deep-linked page restored - from a URL — leaving the user on an invalid page. - wrong_pattern: | - // ❌ Filtering data resets to the deep-linked pageIndex, not 0 - const table = useReactTable({ - data, columns, - initialState: { pagination: { pageIndex: 5, pageSize: 10 } }, - autoResetPageIndex: true, - ... - }) - correct_pattern: | - // ✅ Explicitly reset to 0 when filters change - useEffect(() => { - table.setPageIndex(0) - }, [columnFilters, globalFilter]) - // Or: don't rely on autoResetPageIndex when deep-linking pageIndex - source: - https://github.com/TanStack/table/issues/6207 (autoResetPageIndex resets to initialState pageIndex - instead of 0) - priority: MEDIUM - status: active - version_context: v8.21.3 bug, likely inherited in v9 alpha - skills: - - pagination - - state-management - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - reference_candidates: [] - gaps: - - '`autoResetPageIndex` is silently `false` when `manualPagination` is true (see rowPaginationFeature.utils.ts:43-50). - This is documented but easy to miss. Worth a dedicated note.' - slug: grouping - type: core - packages: - - '@tanstack/table-core' - domain: row-model-features - description: - Group rows by column values in TanStack Table v9 with the `groupedRowModel` stage — the - built-in aggregationFn registry, custom aggregations, grouped-column placement modes, manual server-side - grouping, and expand-grouped-rows interaction with the expanding feature. - covers: - - columnGroupingFeature - - createGroupedRowModel(aggregationFns) - - state.grouping (GroupingState = Array) - - onGroupingChange - - columnDef.aggregationFn ("auto" | name | function) - - columnDef.aggregatedCell - - columnDef.getGroupingValue - - columnDef.enableGrouping - - manualGrouping - - enableGrouping - - 'groupedColumnMode (false | "reorder" | "remove") # default "reorder"' - - table.setGrouping / resetGrouping - - column.toggleGrouping / getCanGroup / getIsGrouped / getGroupedIndex - - column.getToggleGroupingHandler - - column.getAggregationFn / getAutoAggregationFn - - cell.getIsGrouped / getIsAggregated / getIsPlaceholder - - row.getIsGrouped / getGroupingValue / groupingColumnId / groupingValue / leafRows - - aggregationFns built-in registry (8 fns) - tasks: - - Add per-column 'group' toggles in headers and render grouped cells with subRow counts + expand icons - (see examples/react/grouping/src/main.tsx). - - 'Mix aggregation strategies across columns: `sum` for visits, `median` for age, `mean` for progress, - `count` for the implicit grouping column.' - - "Override a column's grouping key with `getGroupingValue: row => ${row.firstName} ${row.lastName}` - so rows group on a derived value, not the column accessor value." - - 'Pair grouping with expanding so users can drill into each group: register `rowExpandingFeature` + - `expandedRowModel` and call `row.getToggleExpandedHandler()` on grouped cells.' - - "Hide grouped columns from the visible column flow with `groupedColumnMode: 'remove'`, or leave - them in place with `groupedColumnMode: false`." + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Group rows, aggregate cells, and reason about grouped rows in expansion and pagination.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/grouping.md', + 'TanStack/table:packages/table-core/src/features/column-grouping', + 'TanStack/table:examples/react/grouping', + ] failure_modes: - - mistake: - Adding `columnGroupingFeature` without also registering `rowExpandingFeature` and expecting - grouped rows to expand. - mechanism: | - `createGroupedRowModel` produces grouped rows with `subRows`, but those rows aren't VISIBLE until the expanded row model flattens them in. Without `rowExpandingFeature` + `createExpandedRowModel()`, `row.getToggleExpandedHandler` doesn't exist (TypeScript error). Even if you skip the handler, the grouped row is collapsed by default and there's no way to drill down. Aggregated cells appear but child rows are unreachable. - wrong_pattern: | - ```tsx - // BUG: grouped rows show aggregates but can't be expanded. - const features = tableFeatures({ columnGroupingFeature }) - const table = useTable({ - features, - rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, - columns, data, - }) - // row.getToggleExpandedHandler() -> TS error or undefined - ``` - correct_pattern: | - ```tsx - // From examples/react/grouping/src/main.tsx - import { - aggregationFns, - columnGroupingFeature, - createExpandedRowModel, - createGroupedRowModel, - rowExpandingFeature, - } from '@tanstack/react-table' - - const features = tableFeatures({ - columnGroupingFeature, - rowExpandingFeature, - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - }) - - const table = useTable({ - features, - rowModels: { - groupedRowModel: createGroupedRowModel(aggregationFns), - expandedRowModel: createExpandedRowModel(), - // + sorted, filtered, paginated as needed - }, - columns, data, - }) - - // In cell renderer: - {cell.getIsGrouped() && ( - - )} - ``` - source: examples/react/grouping/src/main.tsx - priority: high - status: active - version_context: v9.0.0-alpha.48 - skills: - - grouping - - row-expanding - - mistake: - Customizing `aggregationFns` like the v8 `aggregationFns` option (passing the registry through - `tableOptions.aggregationFns` instead of `createGroupedRowModel`). - mechanism: | - In v9, the aggregation function registry is passed as the first argument to `createGroupedRowModel(aggregationFns)`, which stores it at `table._rowModelFns.aggregationFns`. There is NO top-level `tableOptions.aggregationFns` like sorting/filtering have via row model factory. Agents migrating from v8 commonly try `useTable({ aggregationFns: { ... } })` and it has no effect; columns with custom `aggregationFn: 'myCustom'` resolve to `undefined`. - wrong_pattern: | - ```tsx - // BUG: v8-style option that doesn't exist in v9 - const table = useTable({ - features: tableFeatures({ columnGroupingFeature }), - rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, - columns, data, - // @ts-ignore - this property doesn't exist on TableOptions in v9 - aggregationFns: { - myCustom: (id, leaf, child) => /* ... */, - }, - }) - ``` - correct_pattern: | - ```tsx - // From docs/guide/grouping.md - register via createGroupedRowModel - import { aggregationFns, createGroupedRowModel } from '@tanstack/react-table' - - const table = useTable({ - features: tableFeatures({ columnGroupingFeature, rowExpandingFeature }), - rowModels: { - groupedRowModel: createGroupedRowModel({ - ...aggregationFns, - myCustomAggregation: (columnId, leafRows, childRows) => { - return /* aggregated value */ - }, - }), - expandedRowModel: createExpandedRowModel(), - }, - columns, data, - }) - - // Then on a column: - { accessorKey: 'sales', aggregationFn: 'myCustomAggregation' } - ``` - source: packages/table-core/src/features/column-grouping/createGroupedRowModel.ts - priority: high - status: active - version_context: v9.0.0-alpha.48 - skills: - - grouping - - mistake: 'Confusing `aggregationFn` signature: `(columnId, leafRows, childRows)` vs filter/sort signatures.' - mechanism: | - Aggregation functions have a DIFFERENT signature than filterFns and sortFns. They receive `(columnId: string, leafRows: Array, childRows: Array)`. `leafRows` = ALL descendant non-grouped rows (recursive flatten). `childRows` = immediate children of the current grouped row (which may themselves be grouped sub-aggregates at deeper levels). Built-ins use `childRows` (sum, min, max, extent) for nested aggregation reuse, but `leafRows` (mean, median, unique, uniqueCount, count) when the math requires raw values. - wrong_pattern: | - ```tsx - // BUG: wrong arg names - first arg is columnId, not row - aggregationFn: (rowA, rowB, columnId) => /* ... */ - // BUG: averaging via childRows includes already-aggregated sub-group sums - aggregationFn: (id, leaf, child) => child.reduce((a, r) => a + r.getValue(id), 0) / child.length - ``` - correct_pattern: | - ```tsx - // From packages/table-core/src/fns/aggregationFns.ts - // For pure leaf averages, use leafRows: - const aggregationFn_mean: AggregationFn = (columnId, leafRows) => { - let count = 0, sum = 0 - leafRows.forEach((row) => { - const value = row.getValue(columnId) - if (typeof value === 'number') { count++; sum += value } - }) - return count ? sum / count : undefined - } - - // For nestable sums (reuse sub-aggregates), use childRows: - const aggregationFn_sum: AggregationFn = (columnId, _leafRows, childRows) => { - return childRows.reduce((acc, next) => { - const v = next.getValue(columnId) - return acc + (typeof v === 'number' ? v : 0) - }, 0) - } - ``` - source: packages/table-core/src/fns/aggregationFns.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - grouping - - mistake: - "Expecting grouped columns to appear in their original position — and missing the default - `groupedColumnMode: 'reorder'`." - mechanism: | - `columnGroupingFeature.getDefaultTableOptions` sets `groupedColumnMode: 'reorder'`. When grouping is active, the column ordering function in `columnOrderingFeature.utils.ts::orderColumns` moves grouped columns to the START of the leaf-column list. Agents who manually set `columnOrder` and group by 'status' will see 'status' jump to position 0 with no warning. Set `groupedColumnMode: false` to disable this, or `'remove'` to drop grouped columns from the visible flow entirely. - wrong_pattern: | - ```tsx - // BUG: column order is overridden by reorder mode - const table = useTable({ - features, - rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, - columns, data, - initialState: { - columnOrder: ['firstName', 'lastName', 'age', 'status'], // explicit - grouping: ['status'], - }, - // status jumps to position 0, columnOrder is "overridden" - }) - ``` - correct_pattern: | - ```tsx - const table = useTable({ - features, - rowModels: { groupedRowModel: createGroupedRowModel(aggregationFns) }, - columns, data, - initialState: { - columnOrder: ['firstName', 'lastName', 'age', 'status'], - grouping: ['status'], - }, - groupedColumnMode: false, // keep columnOrder intact - }) - - // Or accept the reorder and let grouped columns lead: - groupedColumnMode: 'reorder', // (default) - - // Or hide grouped columns entirely: - groupedColumnMode: 'remove', - ``` - source: packages/table-core/src/features/column-ordering/columnOrderingFeature.utils.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - grouping - - column-ordering - - mistake: Calling `getSelectedRowModel()` on a grouped table and expecting it to include grouped rows. - mechanism: | - Three selected-row APIs exist: `table.getSelectedRowModel()` (built off core), `table.getFilteredSelectedRowModel()` (built off filtered), `table.getGroupedSelectedRowModel()` (built off grouped). They are distinct in the type system. Agents commonly want "selected rows that are currently visible after grouping" and call the wrong one — `getSelectedRowModel()` walks the core (flat) row model and won't reflect grouping changes. - wrong_pattern: | - ```tsx - // BUG: returns selection from core model, not the grouped projection - const selectedRows = table.getSelectedRowModel().rows - // Doesn't reflect grouping — leaf rows only - ``` - correct_pattern: | - ```tsx - // Pick the right model for the question: - table.getSelectedRowModel() // selection from raw data - table.getFilteredSelectedRowModel() // selection within current filters - table.getGroupedSelectedRowModel() // selection within current groups - ``` - source: packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts - priority: low - status: warning - version_context: v9.0.0-alpha.48 - skills: - - grouping - - row-selection - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - reference_candidates: - - name: built-in aggregationFns - kind: function-registry - count: 8 - actual_count: 9 - source: packages/table-core/src/fns/aggregationFns.ts - entries: - - sum - - min - - max - - extent - - mean - - median - - unique - - uniqueCount - - count - note: - Docs say 'There are several built-in aggregation functions' and list 9. Phase 1+2 note said - 8 — actual count is 9 (count, extent both included). - gaps: - - Phase 1+2 anchor says 'the built-in aggregationFn registry' but the registry has 9 (count + 8 numeric). - Verify with maintainer. - - "`createGroupedRowModel` constructs new Row objects via `constructRow` for grouped rows; these rows - have a special `getValue` override that intercepts to either return cached grouping values or call - the column's aggregationFn (lines 139-175). The interaction with column accessors (which normally - return per-row data) is subtle. Worth a reference note." - - "`manualGrouping: true` exists but docs say 'There are not currently many known easy ways to do server-side - grouping with TanStack Table.' Real-world server-side grouping support is a gap." - - slug: row-expanding - type: core - packages: - - '@tanstack/table-core' - domain: row-model-features - description: - Expand and collapse rows in TanStack Table v9 with the `expandedRowModel` stage — tree - sub-rows via `getSubRows`, detail panels via `getRowCanExpand`, filter-from-leaf-rows interaction, - expanded rows in pagination, and manual server-side expansion. - covers: - - rowExpandingFeature - - createExpandedRowModel() - - state.expanded (ExpandedState = true | Record) - - onExpandedChange - - 'getSubRows # (row) => row.children | row.subRows | undefined' - - 'getRowCanExpand # override for detail-panel pattern' - - getIsRowExpanded - - 'enableExpanding # column/row gate' - - manualExpanding - - 'autoResetExpanded # auto reset when row structure changes' - - 'paginateExpandedRows # default true; false = expand on parent page' - - 'filterFromLeafRows # tree filtering interaction' - - 'maxLeafRowFilterDepth # default 100, set 0 to filter root-only' - - table.setExpanded / resetExpanded / toggleAllRowsExpanded - - table.getIsAllRowsExpanded / getIsSomeRowsExpanded / getCanSomeRowsExpand / getExpandedDepth - - table.getToggleAllRowsExpandedHandler - - row.toggleExpanded / getIsExpanded / getCanExpand / getIsAllParentsExpanded - - row.getToggleExpandedHandler - - 'row.depth # for paddingLeft indentation in tree UIs' - - 'row.subRows # populated via getSubRows' - tasks: - - 'Render a tree table: provide nested `subRows` data, set `getSubRows: row => row.subRows`, render - `paddingLeft: ${row.depth * 2}rem` on the first cell (see examples/react/expanding/src/main.tsx).' - - 'Render detail panels (sub-components) for flat data: set `getRowCanExpand: () => true`, render a - second `` with a `colSpan` cell when `row.getIsExpanded()` (see examples/react/sub-components/src/main.tsx).' - - 'Toggle ALL rows at once with a header button: `table.getToggleAllRowsExpandedHandler()`.' - - "Keep expanded children on their parent's page (so the page count doesn't expand): `paginateExpandedRows: - false`." - - 'Filter tree data and keep matching descendants visible under non-matching parents: `filterFromLeafRows: - true`.' + - 'Registering groupedRowModel or aggregationFns without columnGroupingFeature, or expecting grouping state alone to process rows.' + - 'Assuming pageSize counts only leaf data rows; group headers are rows in the model and affect built-in pagination.' + - 'Rendering every grouped cell as a normal value instead of handling grouped, placeholder, and aggregated cell states.' + + - slug: column-ordering + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Control leaf-column order while respecting pinning, visibility, and grouped-column precedence.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-ordering.md', + 'TanStack/table:packages/table-core/src/features/column-ordering', + 'TanStack/table:examples/react/column-dnd', + ] failure_modes: - - mistake: - 'Using `getRowCanExpand: () => true` with tree data and a custom expand button — but `row.getCanExpand()` - already auto-detects `subRows.length`, so the override may collide.' - mechanism: | - `row_getCanExpand` (rowExpandingFeature.utils.ts:319-327) returns `options.getRowCanExpand?.(row) ?? (enableExpanding ?? true) && !!row.subRows.length`. When `getRowCanExpand` is set, it WINS over the auto subRows check. If a developer wants both tree behavior AND detail panels (e.g., leaf rows expand to show extra info), they need `getRowCanExpand: row => true` AND `getSubRows: row => row.subRows`. But then EVERY row (including leaves with no subRows) will show an expand icon — the developer must check `row.subRows.length` themselves in the cell render. - wrong_pattern: | - ```tsx - // BUG: every row gets an expander icon, including leaves - const table = useTable({ - getRowCanExpand: () => true, - getSubRows: r => r.subRows, - // ... - }) - // In cell: row.getCanExpand() always true → leaf rows show 👉 with nothing to expand - ``` - correct_pattern: | - ```tsx - // For pure tree data, omit getRowCanExpand and let it auto-detect: - // From examples/react/expanding/src/main.tsx - const table = useTable({ - features, - rowModels: { expandedRowModel: createExpandedRowModel(), /* ... */ }, - columns, data, - getSubRows: row => row.subRows, - // no getRowCanExpand — row.getCanExpand() is true only when subRows.length > 0 - }) - - // For pure detail panels, override and skip getSubRows: - // From examples/react/sub-components/src/main.tsx - const table = useTable({ - features: tableFeatures({ rowExpandingFeature }), - rowModels: { expandedRowModel: createExpandedRowModel() }, - columns, data, - getRowCanExpand: (row) => true, // every row expands to show - }) - ``` - source: packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts - priority: high - status: active - version_context: v9.0.0-alpha.48 - skills: - - row-expanding - - mistake: - "Setting `paginateExpandedRows: false` and expecting the page size to stay 10 (it doesn't - — more rows render)." - mechanism: | - `paginateExpandedRows: true` (default) lets expanded children flow naturally into pagination — each child counts toward `pageSize`. `paginateExpandedRows: false` keeps expanded children stuck under their parent on whichever page the parent appears — meaning a page of 10 parents with 5 children each renders 60 rows. From the docs guide: "This also means more rows will be rendered than the set page size." `createPaginatedRowModel` calls `expandRows({ ... })` to inflate the page slice when `paginateExpandedRows: false` (see createPaginatedRowModel.ts:57-69). - wrong_pattern: | - ```tsx - // BUG: page renders 50 rows even though pageSize is 10 - const table = useTable({ - paginateExpandedRows: false, - initialState: { pagination: { pageSize: 10 } }, - // user expands all parents → 10 parents * 5 children = 60 visible rows - }) - ``` - correct_pattern: | - ```tsx - // From docs/guide/expanding.md - // Default behavior (children flow through pagination): - const table = useTable({ - features, - rowModels: { expandedRowModel: createExpandedRowModel(), paginatedRowModel: createPaginatedRowModel() }, - columns, data, - getSubRows: r => r.subRows, - // paginateExpandedRows defaults to true - }) - - // OR explicitly keep children with parent: - paginateExpandedRows: false, // accept that pageSize is a soft cap - ``` - source: packages/table-core/src/features/row-pagination/createPaginatedRowModel.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - row-expanding - - pagination - - mistake: Storing `expanded` as `true` then writing into it as if it were a Record. - mechanism: | - `ExpandedState = true | Record`. The `true` literal means "all rows expanded" — there's no need to enumerate row IDs. `row_toggleExpanded` (rowExpandingFeature.utils.ts:250-283) handles the transition: if `old === true`, it first MATERIALIZES the `true` into a real Record of all row IDs in the current row model, then applies the per-row toggle. Agents writing `setExpanded((old) => ({ ...old, [id]: true }))` against `old === true` get `{ 0: true, 1: true, ..., [id]: true }` which is a different state than they expected. - wrong_pattern: | - ```tsx - // BUG: spreading `true` (a boolean) into an object gives {} - all rows collapse except [id] - setExpanded((old) => ({ ...old, [row.id]: true })) - // When old === true, this becomes { [row.id]: true } -- everything else collapses! - ``` - correct_pattern: | - ```tsx - // Use row.toggleExpanded() / row.getToggleExpandedHandler() — they materialize properly: - // From examples/react/expanding/src/main.tsx - - - // Or handle the materialization yourself if you must: - table.setExpanded((old) => { - if (old === true) { - const map: Record = {} - Object.keys(table.getRowModel().rowsById).forEach(id => { map[id] = true }) - return { ...map, [row.id]: !map[row.id] } - } - return { ...old, [row.id]: !old[row.id] } - }) - ``` - source: packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - row-expanding - - mistake: - 'Setting `manualExpanding: true` and still passing `expandedRowModel: createExpandedRowModel()` - AND expecting client-side flattening.' - mechanism: | - With `manualExpanding: true`, `table_getExpandedRowModel` skips the registered `expandedRowModel` factory and returns `getPreExpandedRowModel()` (sorted rows). The expanded state still tracks which rows are "expanded" in terms of UI affordance, but the row model is NOT inflated. To render manually-expanded children, you must include them in the source `data` and `getSubRows` — at which point you can just NOT use manualExpanding. - wrong_pattern: | - ```tsx - // BUG: manualExpanding bypasses the expanded row model; - // sub-rows are never flattened into view. - const table = useTable({ - features: tableFeatures({ rowExpandingFeature }), - rowModels: { expandedRowModel: createExpandedRowModel() }, // ignored - columns, data, - getSubRows: r => r.subRows, - manualExpanding: true, - }) - ``` - correct_pattern: | - ```tsx - // Manual expanding is for server-side patterns where the server - // returns a pre-flattened view based on which rows are expanded. - // From docs/guide/expanding.md - const table = useTable({ - features: tableFeatures({ rowExpandingFeature }), - rowModels: {}, // no expandedRowModel for manual mode - columns, - data: dataQuery.data, // server returns flattened rows when expanded - manualExpanding: true, - state: { expanded }, - onExpandedChange: setExpanded, - }) - - // For client-side tree, omit manualExpanding: - const table = useTable({ - features: tableFeatures({ rowExpandingFeature }), - rowModels: { expandedRowModel: createExpandedRowModel() }, - columns, data, - getSubRows: r => r.subRows, - }) - ``` - source: packages/table-core/src/core/row-models/coreRowModelsFeature.utils.ts - priority: medium - status: active - version_context: v9.0.0-alpha.48 - skills: - - row-expanding - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - reference_candidates: [] - gaps: - - "`row.subRows` is initially populated by `getSubRows`; for grouped rows it's overwritten by `createGroupedRowModel`. - The interaction between user-provided `getSubRows` and grouping-generated subRows is subtle — what - happens if a user has tree data AND groups it?" - - "`autoResetExpanded` exists but isn't documented in the expanding guide. The implementation schedules - a reset (`table._reactivity.schedule`) when grouping changes (it's called from `createGroupedRowModel.onAfterUpdate`). - Worth a note on reset interactions." - - "`row.getIsAllParentsExpanded()` exists but isn't used in any example file I read. Its main use case - is in conditional rendering of deeply nested rows." - - slug: column-layout - type: core - domain: ui-state-features - packages: - - '@tanstack/table-core' - description: | - The five UI-state-only column features that shape how columns render: visibility (show/hide), ordering (manual sequence), pinning (left/right regions), sizing (px widths with min/max), and resizing (drag handle that commits to columnSizing). All are opt-in via `tableFeatures({ ... })`, share the consumer's state-management story (`initialState` vs hoisted `state` + `on*Change`), and combine through a fixed reorder pipeline: Column Pinning -> Manual Column Ordering -> Grouping. None of these features require a row model — they only affect layout. - covers: - - columnVisibilityFeature (state, getCanHide, getIsVisible, getToggleVisibilityHandler, table.getVisibleLeafColumns, - row.getVisibleCells, getIsAllColumnsVisible) - - columnOrderingFeature (columnOrder state, column.getIndex, getIsFirstColumn, getIsLastColumn, setColumnOrder, - the Pinning/Ordering/Grouping reorder pipeline) - - columnPinningFeature (left/right ColumnPinningState, column.pin, column.getIsPinned, column.getStart, - column.getAfter, column.getIsLastColumn/'left'|'right', split-table APIs getLeftHeaderGroups/getCenterHeaderGroups/getRightHeaderGroups, - row.getLeftVisibleCells/getCenterVisibleCells/getRightVisibleCells) - - 'columnSizingFeature (defaultColumnSizing {size: 150, minSize: 20, maxSize: MAX_SAFE_INTEGER}, columnDef.size/minSize/maxSize, - column.getSize, header.getSize, table.getTotalSize, table.getCenterTotalSize/getLeftTotalSize/getRightTotalSize)' - - columnResizingFeature (columnResizeMode 'onEnd' vs 'onChange', columnResizeDirection 'ltr'|'rtl', - header.getResizeHandler with onMouseDown + onTouchStart, column.getIsResizing, columnResizing.deltaOffset/isResizingColumn - for resize-indicator UI, CSS-variable + memoized-tbody perf pattern) - - Stable `columns` reference requirement (React FAQ pitfall) when these features are driving re-renders - tasks: - - Add a column visibility toggle panel that hides/shows columns without breaking header or cell render - (`getVisibleLeafColumns`, `row.getVisibleCells`). - - Pin specific columns left or right (e.g. a select column on the left, an actions column on the right) - via `initialState.columnPinning` or an interactive pin button using `column.pin('left' | 'right' | - false)`. - - Wire up drag-and-drop column reordering with `@dnd-kit/core` and `setColumnOrder` (the canonical 2026 - stack — react-dnd is incompatible with React 18+ Strict Mode). - - Build draggable column resize handles that stay 60fps with large tables (CSS variables + memoized - TableBody during resize, per the `column-resizing-performant` example). - - "Render a sticky-CSS pinning layout (one ``, pinned columns positioned with `column.getStart('left')` - / `getAfter('right')` and `position: sticky`)." - subsystems: - - name: visibility - description: | - `columnVisibility: Record` where missing or `true` means visible. Driven by `enableHiding` (table-level) and `columnDef.enableHiding` (column-level), both defaulting to `true`. Rendering MUST go through `getVisibleLeafColumns` / `row.getVisibleCells` — the `getAll*` variants do not respect this state. - key_apis: - - table.getVisibleLeafColumns() / row.getVisibleCells() - - column.getIsVisible / getCanHide / getToggleVisibilityHandler - - table.getIsAllColumnsVisible / getToggleAllColumnsVisibilityHandler - - name: ordering - description: | - `columnOrder: string[]` of leaf column ids. Empty array means definition order. Ordering is **scoped to unpinned (center) columns** when pinning is also active — pinned columns are sequenced inside `columnPinning.left/right` arrays, not `columnOrder`. The reorder pipeline executes in this fixed order: (1) Column Pinning splits into left/center/right, (2) `columnOrder` reorders the center, then (3) `groupedColumnMode: 'reorder' | 'remove'` may move grouped columns to the front. - key_apis: - - table.setColumnOrder / onColumnOrderChange - - column.getIndex(position?) / getIsFirstColumn / getIsLastColumn - - name: pinning - description: | - `columnPinning: { left: string[]; right: string[] }`. Pinning a group column pins every leaf. Two render strategies are supported: (1) **split tables** using `getLeft*/getCenter*/getRight*` header, column, and cell APIs (`column-pinning-split` example), and (2) **single table + sticky CSS** using `column.getStart('left')` / `column.getAfter('right')` + `position: sticky` plus `getIsLastColumn('left')` / `getIsFirstColumn('right')` for the shadow-edge indicator (`column-pinning-sticky` example). IMPORTANT: in v9 the table-level option is `enableColumnPinning`, not the v8 `enablePinning` — that name is now exclusively the per-column `columnDef.enablePinning`. - key_apis: - - column.pin('left' | 'right' | false) / column.getIsPinned / column.getCanPin - - column.getStart(position) / column.getAfter(position) — sticky CSS offsets - - table.getLeftHeaderGroups / getCenterHeaderGroups / getRightHeaderGroups - - row.getLeftVisibleCells / getCenterVisibleCells / getRightVisibleCells - - name: sizing - description: | - `columnSizing: Record` (pixels). Static defaults live in `defaultColumnSizing` (size 150, minSize 20, maxSize Number.MAX_SAFE_INTEGER) and can be overridden globally via `tableOptions.defaultColumn` or per-column via `columnDef.size/minSize/maxSize`. `column.getSize()` clamps the committed state value (or column-def fallback) between min and max. Three layout styles work: semantic `
` with `width`, block divs with strict widths, and absolutely positioned cells using `getStart()` (no resize mode tied to layout — this is purely consumer choice). - key_apis: - - column.getSize() / header.getSize() — size lookup - - table.getTotalSize / getCenterTotalSize / getLeftTotalSize / getRightTotalSize - - column.resetSize() — drop committed override, fall back to column def - - tableOptions.defaultColumn for cross-column defaults - - name: resizing - description: | - Layered on top of sizing. Two modes via `columnResizeMode`: `'onEnd'` (default — commits the new size to `columnSizing` only when the drag releases; safer for large React tables) and `'onChange'` (commits live during drag; needs the CSS variable + memoized TableBody pattern). `header.getResizeHandler()` returns a single function wired to BOTH `onMouseDown` and `onTouchStart` for mobile support. The transient `columnResizing` state carries `isResizingColumn` and `deltaOffset` — the latter drives the resize-indicator translation in `onEnd` mode. `columnResizeDirection: 'rtl'` inverts the delta sign. - key_apis: - - header.getResizeHandler() — single handler for mouse + touch - - column.getIsResizing / column.getCanResize - - table.atoms.columnResizing.get() — deltaOffset, isResizingColumn - - "columnResizeMode: 'onEnd' (default) | 'onChange'" - - "columnResizeDirection: 'ltr' (default) | 'rtl'" - - column.resetSize() (double-click handle) — provided by sizing subsystem + - 'Treating columnOrder as the final rendered order while pinning regions and groupedColumnMode also reorder columns.' + - 'Using headers, labels, or array indexes as drag identifiers instead of stable leaf column IDs.' + - 'Mutating the columnOrder array in place so the state owner does not observe a new value.' + + - slug: column-pinning + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Partition columns into logical start, center, and end regions and implement sticky layout correctly.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-pinning.md', + 'TanStack/table:packages/table-core/src/features/column-pinning', + 'TanStack/table:examples/react/column-pinning-sticky', + ] failure_modes: - - mistake: | - Rendering header or body cells with `table.getAllLeafColumns()` and `row.getAllCells()` while `columnVisibilityFeature` is registered. Hidden columns still render. - mechanism: | - The `getAll*` accessors return every column/cell unconditionally — they do not consult `columnVisibility` state. Only the `Visible` variants (and the header-group APIs) filter by visibility. Users who copy a v8-era body loop or a non-visibility example into a visibility-enabled table will see no effect on the rendered table regardless of how they toggle the checkbox panel. - wrong_pattern: | - // ❌ Toggling visibility has no effect on rendered cells - {table.getAllLeafColumns().map(column => ( - - ))} - {row.getAllCells().map(cell => ( - - ))} - correct_pattern: | - // ✅ From examples/react/column-visibility/src/main.tsx and the guide: - // Header groups already respect visibility; use them for headers. - // For body cells, swap getAllCells -> getVisibleCells. - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map(header => ( - - ))} - - ))} - - - {table.getRowModel().rows.map(row => ( - - {row.getVisibleCells().map(cell => ( // <-- the fix - - ))} - - ))} - - source: - - docs/guide/column-visibility.md (Column Visibility Aware Table APIs) - - examples/react/column-visibility/src/main.tsx - priority: high - status: active - version_context: | - v9 names unchanged from v8 (`getVisibleLeafColumns`, `getVisibleCells`). Still the #1 column-visibility footgun. - skills: - - column-layout - - mistake: | - Setting `columnResizeMode: 'onChange'` in a React table with many rows or expensive cell renders, then complaining about jittery / sub-30fps resize. Users typically call `column.getSize()` inside every ` - correct_pattern: | - // ✅ From examples/react/column-resizing-performant/src/main.tsx - // 1. Compute every column width once, keyed on resize state - const columnSizeVars = React.useMemo(() => { - const headers = table.getFlatHeaders() - const colSizes: { [key: string]: number } = {} - for (const header of headers) { - colSizes[`--header-${header.id}-size`] = header.getSize() - colSizes[`--col-${header.column.id}-size`] = header.column.getSize() - } - return colSizes - }, [table.state.columnResizing, table.state.columnSizing]) - - // 2. Spread widths onto the table element as CSS variables -
-
- {/* header cells read width from var(--header-${id}-size) */} -
- - {/* 3. Swap to memoized body only while a drag is in progress */} - {table.store.state.columnResizing.isResizingColumn && enableMemo - ? - : } -
- - // Body cells use the CSS variable (no per-cell getSize() call) -
- {cell.renderValue()} -
- - export const MemoizedTableBody = React.memo( - TableBody, - (prev, next) => prev.table.options.data === next.table.options.data, - ) - source: - - docs/guide/column-resizing.md (Advanced Column Resizing Performance) - - examples/react/column-resizing-performant/src/main.tsx - priority: high - status: active - version_context: | - v9 surface unchanged from v8 for resize APIs. The performant pattern is now the canonical reference — Svelte/Vue/Solid adapters generally don't need it, but React does. - skills: - - column-layout - - mistake: | - Trying to control the order of pinned columns by mutating `columnOrder` state. The pinned region keeps rendering in pin-insertion order regardless of what `columnOrder` says. - mechanism: | - The fixed reorder pipeline is (1) Column Pinning split, (2) `columnOrder` applied, (3) Grouping. After step (1) the pinned columns are read **directly from `state.columnPinning.left` and `.right`** by `table_getLeftHeaderGroups` / `_getRightHeaderGroups`. `columnOrder` is only consulted by the unpinned center partition. To reorder a pinned column you must edit the array inside `columnPinning` itself (or unpin -> reorder -> repin). - wrong_pattern: | - // ❌ Won't move 'actions' relative to 'firstName' while it's pinned right - const [columnPinning] = useState({ - left: ['select'], - right: ['actions'], - }) - table.setColumnOrder(['actions', 'select', 'firstName', 'lastName']) - correct_pattern: | - // ✅ Reorder the pinning state itself - table.setColumnPinning(old => ({ - left: ['select'], - right: ['summary', 'actions'], // move 'summary' to render before 'actions' - })) - - // Or for the unpinned center region, columnOrder works normally: - table.setColumnOrder(['firstName', 'lastName']) // only affects center columns - source: - - "docs/guide/column-ordering.md ('Note: columnOrder state will only affect unpinned columns')" - - docs/guide/column-pinning.md (How Column Pinning Affects Column Order) - - packages/table-core/src/features/column-pinning/columnPinningFeature.utils.ts (table_getLeftHeaderGroups - reads from columnPinning.left directly) - priority: medium - status: active - version_context: | - Behavior unchanged from v8. The 3-stage pipeline is documented identically in both column-ordering.md and column-pinning.md in v9. - skills: - - column-layout - - mistake: | - Using the v8 table option `enablePinning` to disable column pinning in v9 — option is ignored, columns remain pinnable. - mechanism: | - In v9 the umbrella `enablePinning` option was split into two distinct options: `enableColumnPinning` (table level) and `enableRowPinning` (table level). The string `enablePinning` is still valid but now refers ONLY to the per-column `columnDef.enablePinning` opt-out. Any agent or human ported from v8 that writes `enablePinning: false` at the table level will see no effect — `column_getCanPin` reads `column.table.options.enableColumnPinning ?? true`. - wrong_pattern: | - // ❌ v8 syntax — no longer disables pinning in v9 - const table = useTable({ - features: tableFeatures({ columnPinningFeature }), - enablePinning: false, // ignored at table level in v9 - //... - }) - correct_pattern: | - // ✅ v9: split into two table-level options - const table = useTable({ - features: tableFeatures({ columnPinningFeature, rowPinningFeature }), - enableColumnPinning: false, // turns off column pinning - enableRowPinning: false, // turns off row pinning (or row => row.original.locked) - //... - }) - - // Per-column opt-out is still spelled `enablePinning`: - columnHelper.accessor('id', { - enablePinning: false, // this column can't be pinned - }) - source: - - packages/table-core/src/features/column-pinning/columnPinningFeature.types.ts (TableOptions_ColumnPinning.enableColumnPinning) - - packages/table-core/src/features/column-pinning/columnPinningFeature.utils.ts (column_getCanPin - reads enableColumnPinning) - - packages/table-core/src/features/row-pinning/rowPinningFeature.types.ts (TableOptions_RowPinning.enableRowPinning) - priority: high - status: active - version_context: | - v9 breaking change vs v8. Agents trained on the v8 surface will consistently produce `enablePinning` at the table level. - skills: - - column-layout - - row-pinning - - mistake: | - Using `react-dnd` or `react-beautiful-dnd` for column reordering in a React 18+/Strict Mode app, or trying to wrap dnd-kit's `DndContext` directly inside a `
...
- {header.isPlaceholder ? null : ( - - )} -
` and re-render the whole body on every move event. - mechanism: | - In `'onChange'` mode, the resize handler commits a new `columnSizing` map on every pointer move. Each commit re-renders the entire body. With `column.getSize()` evaluated on every cell, you re-run an O(columns × rows) traversal per move event — far beyond a 16ms frame budget. The performant pattern (a) caches column widths once in a `useMemo` keyed on `columnResizing` + `columnSizing`, (b) passes those widths down as CSS variables on the table root so cells don't read from JS, and (c) wraps the TableBody in `React.memo` and uses the memoized version only while `state.columnResizing.isResizingColumn` is truthy. The guide also recommends `'onEnd'` as a sane default when this perf budget can't be met. - wrong_pattern: | - // ❌ Live resize + getSize() on every cell + un-memoized body - const table = useTable({ - features: tableFeatures({ columnSizingFeature, columnResizingFeature }), - columnResizeMode: 'onChange', - //... - }) - // ... - - {cell.renderValue()} -
` element. - mechanism: | - `react-dnd` has documented incompatibilities with React 18 Strict Mode (its Provider creates effects that re-fire unpredictably) and is no longer actively maintained against the latest React internals. `react-beautiful-dnd` is in maintenance mode and has known issues with `
` markup. The official TanStack examples for column-dnd and row-dnd both use `@dnd-kit/core` + `@dnd-kit/sortable` because dnd-kit's provider renders `
` wrappers — which violates HTML if nested inside `
` / ``. The `DndContext` MUST wrap the entire table from outside. - wrong_pattern: | - // ❌ react-dnd in React 18 Strict Mode — flicker and stale drags - import { DndProvider } from 'react-dnd' - import { HTML5Backend } from 'react-dnd-html5-backend' - // ... - - // ❌ Nesting DndContext inside
-
- - ... - -
- correct_pattern: | - // ✅ From examples/react/column-dnd/src/main.tsx - import { - DndContext, KeyboardSensor, MouseSensor, TouchSensor, - closestCenter, useSensor, useSensors, - } from '@dnd-kit/core' - import { restrictToHorizontalAxis } from '@dnd-kit/modifiers' - import { - SortableContext, arrayMove, - horizontalListSortingStrategy, useSortable, - } from '@dnd-kit/sortable' - - function handleDragEnd(event: DragEndEvent) { - const { active, over } = event - if (over && active.id !== over.id) { - table.setColumnOrder(prev => { - const oldIndex = prev.indexOf(active.id as string) - const newIndex = prev.indexOf(over.id as string) - return arrayMove(prev, oldIndex, newIndex) - }) - } - } + - 'Using v8 or early-beta left/right state and APIs instead of v9 logical start/end names, especially in RTL layouts.' + - 'Expecting pinning state to apply position: sticky, offsets, z-index, backgrounds, or overflow CSS automatically.' + - 'Allowing rendered widths to diverge from column.getSize, producing gaps or overlaps between adjacent pinned columns.' + + - slug: column-resizing + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Wire resize gestures and resize state onto the numeric sizing model without avoidable render cost.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-resizing.md', + 'TanStack/table:packages/table-core/src/features/column-resizing', + 'TanStack/table:examples/react/column-resizing-performant', + ] + failure_modes: + - 'Registering columnResizingFeature without its columnSizingFeature prerequisite.' + - 'Displaying a resize handle without attaching header.getResizeHandler to the correct pointer or touch events.' + - 'Reading getSize repeatedly in every cell during onChange resizing instead of caching sizes or using CSS variables for large tables.' + + - slug: column-sizing + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Use numeric min, max, and current sizes as state inputs to a renderer-owned CSS layout.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-sizing.md', + 'TanStack/table:packages/table-core/src/features/column-sizing', + 'TanStack/table:examples/react/column-sizing', + ] + failure_modes: + - 'Expecting the numeric sizing state to set DOM widths without applying it to th/td descendants, grid tracks, or flex styles.' + - 'Forcing auto or percentage strings through a number API instead of choosing and implementing an appropriate CSS layout strategy.' + - 'Forgetting the default size and rendered content can disagree, which also corrupts pinning offsets and total-size calculations.' + + - slug: column-visibility + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Hide leaf columns while rendering only visibility-aware header, column, and cell collections.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/column-visibility.md', + 'TanStack/table:packages/table-core/src/features/column-visibility', + 'TanStack/table:examples/react/column-visibility', + ] + failure_modes: + - 'Updating columnVisibility but rendering getAllLeafColumns or row.getAllCells, so hidden columns remain in the DOM.' + - 'Treating absent map entries as hidden; only an explicit false hides a column.' + - 'Expecting enableHiding to hide a column when it only controls whether the user or API may hide it.' + + - slug: global-filtering + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Apply one filter across eligible columns with explicit column eligibility and client/manual ownership.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/global-filtering.md', + 'TanStack/table:packages/table-core/src/features/global-filtering', + 'TanStack/table:examples/react/filters', + ] + failure_modes: + - 'Registering globalFilteringFeature without columnFilteringFeature and a filtered row-model stage when client processing is expected.' + - 'Assuming every column participates; the default eligibility checks the first core row value and only accepts strings or numbers.' + - 'Using manualFiltering while updating globalFilter state but never sending that value to the server query.' + + - slug: expanding + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Expand hierarchical subrows or custom detail panels and place expansion correctly relative to pagination.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/expanding.md', + 'TanStack/table:packages/table-core/src/features/row-expanding', + 'TanStack/table:examples/react/expanding', + ] + failure_modes: + - 'Expecting nested data to expand without getSubRows, or expecting a custom detail panel without getRowCanExpand and renderer markup.' + - 'Updating expanded state but never rendering subrows or the custom expanded UI because Table is headless.' + - 'Assuming paginateExpandedRows changes grouping or flattens visual descendants; it only controls the expanded-row pagination stage.' - // NOTE: DndContext renders divs, so it MUST wrap from outside - -
- - {table.getHeaderGroups().map(hg => ( - - - {hg.headers.map(h => )} - - - ))} - - {/* ... */} -
- - source: - - docs/guide/column-ordering.md (Drag and Drop Column Reordering Suggestions) - - "examples/react/column-dnd/src/main.tsx (the comment `// NOTE: This provider creates div elements, - so don't nest inside of elements`)" - priority: medium - status: active - version_context: | - The recommendation has been stable since v8.10. dnd-kit is the recommended stack in v9 docs. - skills: - - column-layout - - mistake: | - Forgetting to wire `header.getResizeHandler()` to BOTH `onMouseDown` AND `onTouchStart` — desktop drag resize works, mobile users tap-and-drag but the column never resizes. - mechanism: | - `header_getResizeHandler` returns a single function that internally branches on `isTouchStartEvent(event)`. The same function is meant to be installed on both DOM event types so the handler picks the correct branch. If a developer only wires `onMouseDown` (the common copy-paste from a desktop-only example), touch events never fire the handler at all — touch users can grab the resizer visually but no `mousedown` event is synthesized for a long drag on iOS Safari/Android Chrome. The handler installs `touchmove` / `touchend` listeners on `document` only after it receives the initial `touchstart`. - wrong_pattern: | - // ❌ Desktop only — mobile users can't resize -
header.column.resetSize()} - className={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`} - /> - correct_pattern: | - // ✅ From every column-resizing example: wire BOTH events -
header.column.resetSize()} - onMouseDown={header.getResizeHandler()} - onTouchStart={header.getResizeHandler()} - className={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`} - /> - source: - - docs/guide/column-resizing.md (Column Resize APIs section) - - examples/react/column-resizing/src/main.tsx - - examples/react/column-pinning-sticky/src/main.tsx - - packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts (header_getResizeHandler - branches on isTouchStartEvent) - priority: medium - status: active - version_context: | - v9 surface unchanged. - skills: - - column-layout - - mistake: | - Defining `columns` (or the column-helper array) inline in the render body instead of stabilizing with `useMemo`/`useState`/module scope. Triggers an infinite render loop once any of these layout features re-render on state change. - mechanism: | - TanStack Table treats `columns` (and `data` and `state`) as inputs that, when their reference changes, signal that the underlying table model must be rebuilt. With visibility/pinning/ ordering/sizing/resizing registered, every interaction commits a state slice; React re-runs the component; an inline `columns` literal gets a new identity; the table rebuilds; that triggers another render. The `column-pinning-split` and `column-dnd` examples both use `useMemo(() => columnHelper.columns([...]), [])` or define `defaultColumns` outside the component for exactly this reason. The FAQ flags this as Pitfall 1. - wrong_pattern: | - // ❌ Infinite loop once column visibility / pinning / resizing fires - function App() { - const columns = [ - columnHelper.accessor('firstName', { ... }), - columnHelper.accessor('lastName', { ... }), - ] - const table = useTable({ - features: tableFeatures({ columnPinningFeature, columnResizingFeature }), - columns, - data, - }) - } - correct_pattern: | - // ✅ Stable reference via module scope (column-pinning-split pattern) - const defaultColumns = columnHelper.columns([ - columnHelper.accessor('firstName', { ... }), - columnHelper.accessor('lastName', { ... }), - ]) - function App() { - const [columns] = React.useState(() => [...defaultColumns]) - //... - } + - slug: pagination + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Configure client slicing or manual pages, counts, navigation limits, and automatic page-index resets.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/pagination.md', + 'TanStack/table:packages/table-core/src/features/row-pagination', + 'TanStack/table:examples/react/pagination', + ] + failure_modes: + - 'Enabling manualPagination and passing all rows while expecting Table to slice them; manual data must already represent the intended page.' + - 'Omitting rowCount or pageCount for server pagination and then trusting next-page or last-page availability.' + - 'Misdiagnosing pageIndex jumping to zero as failed controlled state when autoResetPageIndex is reacting to client-side data or row-model changes.' - // ✅ Or useMemo (column-dnd pattern) - function App() { - const columns = React.useMemo( - () => columnHelper.columns([ - columnHelper.accessor('firstName', { ... }), - columnHelper.accessor('lastName', { ... }), - ]), - [], - ) - //... - } - source: - - 'docs/faq.md (Pitfall 1: Creating new columns or data on every render)' - - examples/react/column-pinning-split/src/main.tsx (defaultColumns + useState) - - examples/react/column-dnd/src/main.tsx (useMemo) - priority: high - status: active - version_context: | - React-specific. Same FAQ entry applies across all of cluster C — listed here because layout features are usually where users first hit it. - skills: - - column-layout - - row-selection - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - gaps: - - | - Is `column-layout` too broad as a single skill? Five subsystems with distinct config surfaces but a common pattern. Alternative: split into `column-visibility-ordering` (state-only) + `column-pinning` + `column-sizing-resizing` (interaction). Kevin approved combining — keep unless Phase 4 reveals retrieval issues. - - | - No Angular-specific failure mode for column resizing perf. The `angular/column-resizing-performant` example exists but I didn't deep-read it; if signals make the React pattern unnecessary, a clarifying note may belong on the resize subsystem. - - | - The `Table_ColumnResizing.setcolumnResizing` API name has a deliberately lowercase `c` ("matches the current generated v9 table API for the `columnResizing` state slice" per the JSDoc). This is an oddity that might warrant a typo-tolerance failure mode if it's stable enough to ship. - name: Column Layout Features - slug: row-pinning - type: core - domain: ui-state-features - packages: - - '@tanstack/table-core' - description: | - Pin specific rows to a top region or bottom region that stays visible across pagination and filtering. State shape is `{ top: string[]; bottom: string[] }` keyed by `row.id`. Simpler pipeline than column pinning — only one reorder step happens after row pinning (sorting) and pinned rows are sorted within their region. - covers: - - rowPinningFeature (RowPinningState, row.pin, row.getIsPinned, row.getPinnedIndex, row.getCanPin, table.getTopRows, - table.getBottomRows, table.getCenterRows, table.getIsSomeRowsPinned) - - enableRowPinning option (bool or row predicate) - - keepPinnedRows option (default true — pinned rows persist across pagination/filtering even if they'd - otherwise be filtered out; false hides them when filtered/paginated away) - - row.pin(position, includeLeafRows?, includeParentRows?) signature — the extra args matter for grouped/expanded - data - - The reorder pipeline: Row Pinning -> Sorting - tasks: - - Add pin-top / pin-bottom buttons to a row that persist the pinned rows even when the user paginates - or filters the data. - - Render pinned rows in a sticky `
` with calculated `top`/`bottom` offsets based on `row.getPinnedIndex()` - (see the example pattern). - - Choose between (a) splitting pinned vs center rows in render (`getTopRows + getCenterRows + getBottomRows`) - versus (b) leaving pinned rows duplicated in the main flow (the `copyPinnedRows` toggle in the example). + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Separate top, center, and bottom rows with stable IDs and explicit rendering/sticky behavior.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/row-pinning.md', + 'TanStack/table:packages/table-core/src/features/row-pinning', + 'TanStack/table:examples/react/row-pinning', + ] failure_modes: - - mistake: | - Setting `getRowId: (row) => row.id` (or omitting it), pinning a row, then refetching/reordering the data — the wrong row stays pinned because `row.id` defaulted to `row.index` and indices shifted. - mechanism: | - `row.id` defaults to the row's positional `index` in the data array. `rowPinning.top` and `rowPinning.bottom` are arrays of string row ids. After a refetch that reorders the data, index 3 might now be a different record entirely, but the pinning state still pins index 3. This is the same root cause as the row selection ID pitfall — fix by supplying a stable `getRowId: (row) => row.uuid` (or any unique primary key from the domain data). The row-pinning example deliberately uses `makeData(1_000, 2, 2)` with `subRows` and demonstrates the `includeLeafRows`/`includeParentRows` args of `row.pin` to handle grouped data. - wrong_pattern: | - // ❌ row.id defaults to row.index; pin survives wrong rows after refetch - const table = useTable({ - features: tableFeatures({ rowPinningFeature, rowPaginationFeature }), - data, // refetched periodically - //... - }) - correct_pattern: | - // ✅ Stable row identity - const table = useTable({ - features: tableFeatures({ rowPinningFeature, rowPaginationFeature }), - data, - getRowId: row => row.userId, // or row.uuid, row.id from API, etc. - //... - }) - - // For grouped/expanded data, pass the include flags too: - row.pin('top', includeLeafRows, includeParentRows) - source: - - docs/guide/row-selection.md (Useful Row Ids — same principle) - - examples/react/row-pinning/src/main.tsx (uses makeData with subRows, demonstrates include flags) - priority: high - status: active - version_context: | - v9 surface unchanged. - skills: - - row-pinning - - row-selection - - mistake: | - Leaving `keepPinnedRows: true` (the default) and being surprised that pinned rows appear on every page during pagination — or conversely, setting it to `false` and being surprised that pinned rows disappear when filtered out. - mechanism: | - `keepPinnedRows` controls whether `table.getTopRows()` / `table.getBottomRows()` use `table.getRow(id, true)` (which searches the full pre-pagination row set) or `visibleRows.find(...)` (which only finds rows currently in the row model). Default is `true`, so pinned rows stick across pagination and filtering and may render on every page. With `false`, pinned rows can disappear if their underlying row is filtered or paginated out. The row-pinning example exposes both via the "Keep/Persist Pinned Rows" checkbox to make the contrast visible. The behavior is deliberate — pick the one that matches the UX you want. - wrong_pattern: | - // ❌ Expecting pinned rows to vanish on filter, but they don't (default) - const table = useTable({ - features: tableFeatures({ rowPinningFeature, columnFilteringFeature }), - //... - // keepPinnedRows defaults to true; pinned rows survive filtering - }) - correct_pattern: | - // ✅ Be explicit about which UX you want - const table = useTable({ - features: tableFeatures({ rowPinningFeature, columnFilteringFeature }), - keepPinnedRows: false, // pinned rows disappear when filtered/paginated out - //... - }) + - 'Using index-derived row IDs for persistent pinning while sorting, filtering, pagination, or data insertion changes indexes.' + - 'Expecting row pinning to add sticky CSS or render top/center/bottom collections in the correct order automatically.' + - 'Ignoring keepPinnedRows, so pinned rows remain visible outside the center row model when the desired product behavior was to filter or paginate them away.' - // Or to keep the default but render pinned rows separately from the body: - - {table.getTopRows().map(row => )} - {table.getCenterRows().map(row => )} - {table.getBottomRows().map(row => )} - - source: - - docs/guide/row-pinning.md - - 'packages/table-core/src/features/row-pinning/rowPinningFeature.utils.ts (table_getPinnedRows: const - keepPinnedRows = table.options.keepPinnedRows ?? true)' - - examples/react/row-pinning/src/main.tsx (keepPinnedRows toggle) - priority: medium - status: active - version_context: | - v9 default matches v8 (`true`). Same option name. - skills: - - row-pinning - - mistake: | - Iterating `table.getRowModel().rows` for the main tbody AND rendering `getTopRows()`/`getBottomRows()` separately — pinned rows render twice (once at top/bottom, once where they originally appeared in the row model). - mechanism: | - `getRowModel()` returns the complete current row model with pinned rows still in it (pinning doesn't remove rows from the model, it just exposes a partition). The intended pattern is to render either (a) `getTopRows + getCenterRows + getBottomRows` (three loops, pinned rows appear once) or (b) `getTopRows + getRowModel + getBottomRows` only if you explicitly want pinned rows duplicated. The example toggles between these via `copyPinnedRows` to make the difference explicit. The default choice should be `getCenterRows`. - wrong_pattern: | - // ❌ Pinned rows render twice - - {table.getTopRows().map(row => )} - {table.getRowModel().rows.map(row => ( // <-- still includes pinned rows - ... - ))} - {table.getBottomRows().map(row => )} - - correct_pattern: | - // ✅ From examples/react/row-pinning/src/main.tsx - - {table.getTopRows().map(row => ( - - ))} - {table.getCenterRows().map(row => ( - - {row.getAllCells().map(cell => ( - - ))} - - ))} - {table.getBottomRows().map(row => ( - - ))} - - source: - - examples/react/row-pinning/src/main.tsx (the copyPinnedRows demo toggle) - priority: medium - status: active - version_context: | - v9 surface unchanged. - skills: - - row-pinning - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - gaps: - - | - Could fold into `column-layout` as a sixth subsystem given how small the dedicated guide is (the row-pinning.md guide is *2 sentences* long after the deprecated `pinning.md` redirect). Kept separate because Kevin approved it as its own bullet AND the failure modes are genuinely different from column-pinning (id-stability matters more, `keepPinnedRows` has no column-side analogue, render order differs). **Flag for Phase 4 review.** - - | - Sticky-CSS row pinning pattern (the `PinnedRow` component in the example with `position: sticky` + `top: row.getPinnedIndex() * 26 + 48px`) is non-trivial — magic numbers depend on row height + header height. Worth deciding if this becomes its own reference pattern or a callout in the existing failure mode. - slug: row-selection - type: core - domain: ui-state-features - packages: - - '@tanstack/table-core' - description: | - Track which rows are selected via `rowSelection: Record` and derive selected-row models scoped to the current data (`getSelectedRowModel`), to filtered data (`getFilteredSelectedRowModel`), or to grouped data (`getGroupedSelectedRowModel`). Supports single-select via `enableMultiRowSelection: false`, conditional row-by-row enabling via predicates, and parent->child propagation via `enableSubRowSelection`. The row id used in the selection map defaults to `row.index` — supplying a stable `getRowId` is essentially mandatory for any real-world use. - covers: - - rowSelectionFeature (RowSelectionState, table.getSelectedRowModel, getFilteredSelectedRowModel, getGroupedSelectedRowModel, - getPreSelectedRowModel, getIsAllRowsSelected, getIsSomeRowsSelected, getIsAllPageRowsSelected, getIsSomePageRowsSelected, - toggleAllRowsSelected, toggleAllPageRowsSelected) - - row.toggleSelected, row.getIsSelected, row.getIsSomeSelected (indeterminate), row.getIsAllSubRowsSelected, - row.getCanSelect, row.getCanMultiSelect, row.getCanSelectSubRows, row.getToggleSelectedHandler - - enableRowSelection (bool or row predicate) — default true - - enableMultiRowSelection (bool or row predicate) — default true; setting to false makes it radio-style - (single-row at a time) - - enableSubRowSelection (bool or row predicate) — default true; controls whether parent row toggle propagates - to subRows - - The interaction with `getRowId`, `manualPagination`, atoms (via `useCreateAtom`), and the indeterminate-checkbox - UI pattern - tasks: - - Add a select-column with header "select all" + per-row checkbox wired through `getToggleAllRowsSelectedHandler` - / `getToggleSelectedHandler`, including the indeterminate (`getIsSomeRowsSelected`) state. - - Hoist selection state outside the table (via `useState` or an atom from `useCreateAtom`) so the selected - ids can be sent to an API call or used in other components. - - 'Build single-select / radio-style selection by setting `enableMultiRowSelection: false`.' - - 'Restrict selection conditionally (e.g. only adult records, non-archived rows) via `enableRowSelection: - row => row.original.age > 18`.' + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Maintain selection IDs across current, filtered, grouped, and manually paginated data.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/row-selection.md', + 'TanStack/table:packages/table-core/src/features/row-selection', + 'TanStack/table:examples/react/row-selection', + ] failure_modes: - - mistake: | - Omitting `getRowId` and using `manualPagination` (server-side paging). The user pages forward, selects a row, pages back, and the wrong row appears selected — or selections evaporate on page navigation. - mechanism: | - With no `getRowId` provided, `row.id` defaults to the row's `index` within the data array. Under `manualPagination`, each page receives only that page's rows, so `data[0]` is always row.id "0", `data[1]` is always "1", etc. Selecting row 5 on page 1 and paging to page 2 sets `rowSelection["5"] = true`, but page 2's data also has a "5" — so a different record now appears selected. The fix is universal: supply `getRowId: row => row.someStablePrimaryKey`. The row-selection guide flags this as the "Useful Row Ids" section and a separate note covers the `manualPagination` interaction: `getSelectedRowModel` only returns rows in the current `data` array, so for cross-page totals you must read `rowSelection` state directly instead of relying on the row model. - wrong_pattern: | - // ❌ Server-side pagination + default row.id = row.index - const table = useTable({ - features: tableFeatures({ rowSelectionFeature, rowPaginationFeature }), - data, // only the current page from the server - manualPagination: true, - rowCount, // server-provided total - //... - }) - // After paging, `rowSelection: { '5': true }` is ambiguous — - // which page's row 5? Selection appears to move with the user. - correct_pattern: | - // ✅ Stable row id keyed to the primary key the server uses - const table = useTable({ - features: tableFeatures({ rowSelectionFeature, rowPaginationFeature }), - data, - manualPagination: true, - rowCount, - getRowId: row => row.uuid, // or row.id from your API - //... - }) - - // ✅ For "X of Y selected" with server-side pagination, read the - // rowSelection state directly — getSelectedRowModel only knows - // about rows in the current page's `data`: - const totalSelected = Object.keys(table.state.rowSelection).length - - // (The selection state can hold ids that aren't in `data` — - // that's by design.) - source: - - docs/guide/row-selection.md (Useful Row Ids section; manualPagination note above Access Row Selection - State) - - packages/table-core/src/features/row-selection/rowSelectionFeature.types.ts (RowSelectionState = - Record) - - 'examples/react/row-selection/src/main.tsx (uses getRowId: row => row.id)' - priority: high - status: active - version_context: | - v9 surface unchanged from v8. Still the #1 row-selection footgun, especially in any server-driven table. - skills: - - row-selection - - mistake: | - Setting `enableMultiRowSelection: false` to get radio-style single selection, but rendering checkboxes via `getToggleAllRowsSelectedHandler` and `getIsSomeRowsSelected` for an indeterminate header — leaves a non-functional "select all" UI that contradicts the single-select intent. - mechanism: | - When `enableMultiRowSelection` is false, the internal `mutateRowIsSelected` helper clears all other ids from the selection map before adding the new one — so "select all" is effectively no-op (it sets one row, deselects all others, then checks the next, in a fast loop that leaves only the last row selected). The header indeterminate state is meaningless because only zero or one rows can ever be selected. Use radio inputs instead, drop the toggle-all header, and treat the selection map as `{ [singleId]: true }`. - wrong_pattern: | - // ❌ Checkbox "select all" + single-select mode - const table = useTable({ - features: tableFeatures({ rowSelectionFeature }), - enableMultiRowSelection: false, // radio-like - //... - }) - - // Header still renders a checkbox + indeterminate - - correct_pattern: | - // ✅ Radio inputs, no toggle-all header - const table = useTable({ - features: tableFeatures({ rowSelectionFeature }), - enableMultiRowSelection: false, - getRowId: row => row.id, - //... - }) - - { - id: 'select', - header: '', // no "select all" makes sense in single-select mode - cell: ({ row }) => ( - - ), - } - - // Or conditional single-select for only the sub-rows of certain - // parents (the guide's predicate form): - enableMultiRowSelection: row => row.original.age > 18, - source: - - docs/guide/row-selection.md (Single Row Selection section) - - 'packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts (mutateRowIsSelected: - if (!row_getCanMultiSelect(row)) Object.keys(selectedRowIds).forEach(key => delete selectedRowIds[key]))' - priority: medium - status: active - version_context: | - v9 surface unchanged. - skills: - - row-selection - - mistake: | - Calling `table.getSelectedRowModel().flatRows` on a paginated server-side table to count or list "all selected rows" — gets only the selected rows visible on the current page, missing selections from previously visited pages. - mechanism: | - `getSelectedRowModel` is derived from `table.getCoreRowModel()` via `selectRowsFn`, which recurses the core row tree filtering by `isRowSelected`. Under `manualPagination`, the core row model only knows about the rows in the current `data` prop — previously-paged rows aren't materialized. The `rowSelection` state map, however, can hold ids that aren't in the current `data`. The guide says explicitly: "Row selection state, however, can contain row ids that are not present in the `data` array just fine." For server-side selection bookkeeping, read `state.rowSelection` directly; for a count UI, use `Object.keys(rowSelection).length` instead of `.getSelectedRowModel().rows.length`. - wrong_pattern: | - // ❌ Under manualPagination, only counts the visible page's selected rows - const selectedCount = table.getSelectedRowModel().flatRows.length - // ^ this stays small because flatRows is built from data, which - // only contains the current page. - - const handleBulkAction = () => { - const ids = table.getSelectedRowModel().flatRows - .map(row => row.original.id) - api.archive(ids) // missing all selections from other pages! - } - correct_pattern: | - // ✅ For counts and id lists under manualPagination, read state directly - const selectedCount = Object.keys(table.state.rowSelection).length - - const handleBulkAction = () => { - // rowSelection keys ARE the stable ids (you set getRowId: row => row.id) - const ids = Object.keys(table.state.rowSelection) - api.archive(ids) - } - - // (Client-side / no manualPagination: getSelectedRowModel is fine, - // because data contains every row.) - source: - - "docs/guide/row-selection.md ('Note: If you are using manualPagination ...')" - - packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts (selectRowsFn walks - the supplied rowModel, can only materialize rows present in core data) - - examples/react/row-selection/src/main.tsx (uses Object.keys(table.state.rowSelection).length for - the count UI) - priority: high - status: active - version_context: | - v9 surface unchanged from v8. This is the most common bug after shipping a working "select rows" feature to a server-paginated table. - skills: - - row-selection - - mistake: | - Toggling a parent row in a grouped/expanded table and being surprised that all subRows also flip — or conversely, disabling the propagation but having `getIsSomeSelected` return false when only some children are selected. - mechanism: | - `enableSubRowSelection` defaults to `true`. The internal `mutateRowIsSelected` recurses into `row.subRows` when this option is truthy, applying the same `value` to each. This enables the "select parent => select all children" UX. To opt out, set `enableSubRowSelection: false` (or pass a row predicate). The indeterminate state on a parent comes from `row.getIsSomeSelected()` which calls `isSubRowSelected(row)` and returns `'some'` when not all selectable descendants are selected — but this only looks at subRows, so if you've disabled propagation AND have separately selected mixed subRows, the parent will correctly read "some". The UX decision is yours; pick deliberately. - wrong_pattern: | - // ❌ Default behavior: clicking the parent selects all children - // (which is correct for most cases but surprising if you wanted - // parent-only selection for a "select this group as a whole" UX) - const table = useTable({ - features: tableFeatures({ rowSelectionFeature, rowExpandingFeature }), - getSubRows: row => row.subRows, - // enableSubRowSelection unset — defaults to true - }) - correct_pattern: | - // ✅ Opt out of parent->child propagation explicitly - const table = useTable({ - features: tableFeatures({ rowSelectionFeature, rowExpandingFeature }), - getSubRows: row => row.subRows, - enableSubRowSelection: false, // toggling parent doesn't touch subRows - }) + - 'Assuming selection mutates application data or is automatically deleted when rows disappear; rowSelection is independent ID state.' + - 'Using default index IDs for server pagination or mutable data instead of a stable getRowId.' + - 'Treating getSelectedRowModel as a database-wide selection lookup under manual pagination even though only loaded rows can appear in a row model.' - // ✅ Or selectively disable per row - enableSubRowSelection: row => row.depth > 0, // top-level rows still propagate + - slug: sorting + package: '@tanstack/table-core' + domain: feature-plugins + type: feature + purpose: 'Configure single/multi sorting, comparison functions, undefined placement, removal cycles, and client/manual ownership.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/sorting.md', + 'TanStack/table:packages/table-core/src/features/row-sorting', + 'TanStack/table:examples/react/sorting', + ] + failure_modes: + - 'Setting manualSorting and expecting createSortedRowModel to reorder rows; manual mode trusts incoming data.' + - 'Writing a custom sortFn that applies ascending/descending direction itself even though Table reverses the comparator result for descending order.' + - 'Assuming undefined values, multi-sort gestures, and sort-removal cycles match product expectations without configuring sortUndefined and multi-sort options.' - // ✅ Drive an indeterminate parent checkbox from descendant state - - source: - - docs/guide/row-selection.md (Sub-Row Selection section) - - packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts (mutateRowIsSelected - recursion gated by row_getCanSelectSubRows) - - "packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts (isSubRowSelected: - returns 'all' | 'some' | false)" - priority: medium - status: active - version_context: | - v9 surface unchanged. - skills: - - row-selection - - mistake: Parent row checkbox state stuck on "unchecked" with all sub-rows selected - mechanism: | - `getIsAllSubRowsSelected` on a grouped parent only counts direct children, not - nested descendants. With multi-level grouping, the parent reports "all selected" - based on a partial count. Combined with `enableSubRowSelection: true`, - `row.toggleSelected(false)` on a parent shortcuts to a no-op because - `getIsSelected()` returns false even when all descendants are checked. - wrong_pattern: | - // ❌ Returns false even when all leaf descendants are selected - const isParentChecked = row.getIsAllSubRowsSelected() - correct_pattern: | - // ✅ Walk leaves directly - const allLeafs = row.getLeafRows() - const allSelected = allLeafs.length > 0 && allLeafs.every(r => r.getIsSelected()) - const someSelected = allLeafs.some(r => r.getIsSelected()) - source: - https://github.com/TanStack/table/issues/4878 (row selection not working for nested rows, - 20 comments), https://github.com/TanStack/table/issues/4759 (Cannot toggle selection of grouped - row to deselect its children, 8 comments) - priority: HIGH - status: active - version_context: v8 / v9 — deeply confusing for tree data - skills: - - row-selection - - grouping - - mistake: Row selection state persists after data is removed/refreshed - mechanism: | - v8 removed v7's `autoResetSelectedRows`. With server-side data updates (websockets, - refetch), row IDs that no longer exist remain in `rowSelection` state and - `getIsAllRowsSelected()` returns true based on stale state. - wrong_pattern: | - // ❌ Selection state goes stale after data refresh - useEffect(() => { - refreshData() // selection still references deleted IDs - }, [trigger]) - correct_pattern: | - // ✅ Prune stale IDs on data change - useEffect(() => { - setRowSelection(prev => { - const validIds = new Set(data.map(row => row.id)) - const next = {} - for (const id in prev) if (validIds.has(id)) next[id] = prev[id] - return next - }) - }, [data]) - source: - https://github.com/TanStack/table/issues/5850 (Row selection is not cleaned up when table - data is removed), https://github.com/TanStack/table/issues/4498 (Row selection does not reset after - upgrading from v7 to v8) - priority: HIGH - status: active - version_context: v7 → v8 migration regression — autoResetSelectedRows was removed - skills: - - row-selection - - state-management - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - gaps: - - | - The `examples/angular/row-selection-signal/` example uses a signal-aware integration (`signal({})` + manual propagation through `onRowSelectionChange`). React's `useCreateAtom` (from `@tanstack/react-store`) is shown in the React example as the modern alternative to `useState` for selection state. Worth flagging in Phase 4 whether a per-adapter atom/signal pattern variant belongs in this skill or in a framework-specific overlay. - - | - `getPreSelectedRowModel` exists but is rarely used in practice — it returns the same thing as `getCoreRowModel` per the source. No failure mode currently flags this as a noise API; left in `covers` only. - - name: React Table State, Subscribe & createTableHook - slug: table-state + - slug: getting-started + package: '@tanstack/react-table' domain: framework-adapters - description: - Reactivity, atom subscription, useTable selector, table.Subscribe rendering, FlexRender, - and createTableHook composition for @tanstack/react-table. type: framework - packages: - - '@tanstack/react-table' - covers: - - useTable - - useTable selector second argument - - table.state (selected state) - - table.atoms (per-slice readonly derived atoms) - - table.baseAtoms (writable internal atoms) - - table.store (flat readonly store) - - table.Subscribe with selector - - table.Subscribe with source (atom or store) - - Subscribe (standalone) - - table.FlexRender - - useCreateAtom from @tanstack/react-store - - atoms option for external ownership - - state plus on[State]Change option - - initialState - - createTableHook - - createAppColumnHelper / useAppTable / useTableContext / useCellContext / useHeaderContext - - table.AppTable / table.AppHeader / table.AppCell / table.AppFooter - tasks: - - Wire up a React table with default reactivity and decide between initialState, atoms, and external - state. - - Optimize re-renders with a useTable selector or table.Subscribe at a specific subtree. - - Hoist a pagination or filter slice into an external atom that other parts of the app subscribe to. - - Build a reusable createTableHook with shared features, row models, and cellComponents / headerComponents - / tableComponents. + framework: react + purpose: 'Create and render a headless v9 React table with useTable and explicit features.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/migrating.md', + 'TanStack/table:examples/react/basic-use-table', + 'TanStack/table:packages/react-table/src/index.ts', + ] failure_modes: - - mistake: Reading state via table.atoms.pagination.get() inside a component and expecting it to re-render - mechanism: - Atom .get() and table.store.state are current-value reads, not React subscriptions. They - do not cause the component to re-render when the atom changes. - wrong_pattern: | - function Pager({ table }) { - const pagination = table.atoms.pagination.get() - return Page {pagination.pageIndex + 1} - } - correct_pattern: | - function Pager({ table }) { - return ( - p.pageIndex} - > - {(pageIndex) => Page {pageIndex + 1}} - - ) - } - source: docs/framework/react/guide/table-state.md (Reading State Without Subscribing); examples/react/basic-subscribe/src/main.tsx - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48 - - mistake: Passing both atoms.pagination and state.pagination for the same slice - mechanism: - When a slice is owned both ways, external atoms take precedence over external state. The - on[State]Change callback may stop firing or write into a base atom whose value is shadowed by the - external atom, producing surprising divergence. - wrong_pattern: | - const table = useTable({ - features, - rowModels: {}, - columns, - data, - atoms: { pagination: paginationAtom }, - state: { pagination }, - onPaginationChange: setPagination, - }) - correct_pattern: | - // Pick exactly one source of truth per slice. - const table = useTable({ - features, - rowModels: {}, - columns, - data, - atoms: { pagination: paginationAtom }, - }) - source: docs/framework/react/guide/table-state.md (Custom Initial State Note); examples/react/basic-external-atoms - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - mistake: Using table.Subscribe inside a column cell / header definition - mechanism: - In cell and header render contexts, the table field is typed as the core Table, not the React-extended ReactTable, so table.Subscribe is not on the object. Calling it crashes - or fails to compile. - wrong_pattern: | - columnHelper.display({ - id: 'select', - cell: ({ row, table }) => ( - s[row.id]}> - {(isSelected) => } - - ), - }) - correct_pattern: | - import { Subscribe } from '@tanstack/react-table' + - 'Copying a v8 useReactTable/getCoreRowModel setup instead of the v9 useTable plus tableFeatures shape.' + - 'Expecting useTable to render markup or styles rather than mapping headers, rows, and cells with FlexRender or flexRender.' + - 'Creating features, columns, or fallback data inside render without stable references.' - columnHelper.display({ - id: 'select', - cell: ({ row, table }) => ( - s[row.id]}> - {(isSelected) => ( - - )} - - ), - }) - source: docs/framework/react/guide/table-state.md (Tips section); examples/react/basic-subscribe/src/main.tsx - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - skills: - - react-subscribe-compiler-compat - - &id001 - mistake: Quadratic slowdown from nested mergeObjects on every Svelte reactive tick - mechanism: | - `@tanstack/svelte-table` adapter wraps `setOptions` in `$effect.pre`, calling - `setOptions(prev => mergeObjects(prev, mergedOptions))` on every reactive tick. - `mergeObjects` builds a getter chain referencing the previous result; after N - ticks `table.options` is N-deep. Reads of feature-default-only keys - (`onExpandedChange`, etc.) walk the entire chain on every row-model evaluation, - producing O(N²) work and visible freezes while typing into a filter input. - wrong_pattern: | - // ❌ Default svelte-table integration — the chain grows quadratically with keystrokes - const table = createSvelteTable({ - data, columns, state: { globalFilter }, - getCoreRowModel: getCoreRowModel(), - }) - correct_pattern: | - // ✅ Override mergeOptions to flatten on each tick (workaround until upstream fix) - const table = createSvelteTable({ - data, columns, - mergeOptions: (prev, next) => { - const result = {} - const allKeys = new Set([...Reflect.ownKeys(prev), ...Reflect.ownKeys(next)]) - for (const key of allKeys) { - const fromNext = next[key] - result[key] = fromNext !== undefined ? fromNext : prev[key] - } - return result - }, - }) - source: - 'https://github.com/TanStack/table/issues/6235 (svelte-table: nested mergeObjects in setOptions - produces quadratic slowdown)' - priority: CRITICAL - status: active - version_context: - v9.0.0-alpha.32 (svelte). Same structural risk exists in other adapters if they re-call - setOptions per tick. - skills: - - table-state - - production-readiness - - name: Vue Table State, Subscribe & createTableHook - slug: table-state + - slug: table-state + package: '@tanstack/react-table' domain: framework-adapters - description: - Vue reactivity binding, useTable selector, table.Subscribe, FlexRender, and createTableHook - composition for @tanstack/vue-table. type: framework - packages: - - '@tanstack/vue-table' - covers: - - useTable - - useTable selector second argument - - table.state (selected reactive state) - - table.atoms / table.baseAtoms / table.store - - table.Subscribe with selector - - table.Subscribe with source - - FlexRender component - - vueReactivity (shallowRef + computed + watch flush sync) - - createAtom from @tanstack/vue-store - - useSelector from @tanstack/vue-store - - reactive data / columns (ref or computed) - - atoms / state / on[State]Change options - - createTableHook - tasks: - - Pass a Vue ref or computed as data and let useTable re-sync via setOptions. - - Choose between atoms ownership and state-plus-on[State]Change with getter wrappers for Vue refs. - - Subscribe a small piece of the template to a single atom with table.Subscribe. + framework: react + purpose: 'Read, subscribe to, control, and optimize React table state with selectors, Subscribe, and external atoms.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/table-state.md', + 'TanStack/table:examples/react/basic-subscribe', + 'TanStack/table:packages/react-table/src/Subscribe.ts', + 'TanStack/table:packages/react-table/src/useTable.ts', + ] failure_modes: - - mistake: Passing a Vue ref directly to state.pagination without a getter - mechanism: - useTable reads state.pagination once when options resolve. Without a getter, the raw ref - object is captured and the table never sees subsequent ref.value changes. - wrong_pattern: | - const pagination = ref({ pageIndex: 0, pageSize: 10 }) - const table = useTable({ - features, - rowModels: {}, - columns, - data, - state: { pagination }, - onPaginationChange: (u) => { - pagination.value = u instanceof Function ? u(pagination.value) : u - }, - }) - correct_pattern: | - const pagination = ref({ pageIndex: 0, pageSize: 10 }) - const table = useTable({ - features, - rowModels: {}, - columns, - data, - state: { - get pagination() { return pagination.value }, - }, - onPaginationChange: (u) => { - pagination.value = u instanceof Function ? u(pagination.value) : u - }, - }) - source: docs/framework/vue/guide/table-state.md (External State); examples/vue/basic-external-state - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48 - - mistake: Reading table.atoms.pagination.get() outside a reactive context and expecting Vue updates - mechanism: - Atom .get() is a current-value read. It only participates in Vue tracking when called inside - a reactive scope such as a template, computed, or watch. From plain script setup top-level code - it is a one-shot read. - wrong_pattern: | - const pagination = table.atoms.pagination.get() // read once, never updates - correct_pattern: | - const pagination = computed(() => table.atoms.pagination.get()) - // or - // table.state.pagination via useTable selector - source: docs/framework/vue/guide/table-state.md (Reading State Without Subscribing) - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - mistake: Using the v8 useVueTable name - mechanism: - v9 renamed the adapter creator to useTable for every framework. useVueTable is no longer - exported. - wrong_pattern: | - import { useVueTable } from '@tanstack/vue-table' - correct_pattern: | - import { useTable } from '@tanstack/vue-table' - source: docs/framework/vue/vue-table.md; packages/vue-table/src/useTable.ts - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - *id001 - - name: Solid Table State, Subscribe & createTableHook - slug: table-state + - 'Reading table.atoms.x.get or table.store.state during render and assuming that snapshot read subscribes React to future changes.' + - 'Providing onSliceChange without state.slice, or failing to apply updater-function values when React owns the slice.' + - 'Hiding builder-method state reads in compiler-memoized children without a Subscribe boundary, or adding fine-grained Subscribe everywhere before measuring.' + + - slug: migrate-v8-to-v9 + package: '@tanstack/react-table' domain: framework-adapters - description: - Solid reactivity binding, createTable selector, table.state() accessor, table.Subscribe, - FlexRender, and createTableHook composition for @tanstack/solid-table. - type: framework - packages: - - '@tanstack/solid-table' - covers: - - createTable - - createTable selector second argument - - table.state() (Solid accessor) - - table.atoms / table.baseAtoms / table.store - - table.Subscribe with selector - - table.Subscribe with source (accessor children) - - FlexRender component - - solidReactivity (createSignal + createMemo) - - createAtom from @tanstack/solid-store - - useSelector from @tanstack/solid-store - - reactive get data() getter pattern - - atoms / state / on[State]Change options - - createTableHook - tasks: - - Wire reactive Solid data via a get data() getter on the options object. - - Read table.state() as an accessor inside JSX and computations. - - Subscribe per-row UI to a single atom with table.Subscribe and an accessor child. + type: migration + framework: react + purpose: 'Migrate React from useReactTable to the native v9 feature, state, rendering, helper, and composable APIs.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/migrating.md', + 'TanStack/table:packages/react-table/src/index.ts', + 'TanStack/table:packages/react-table/src/legacy.ts', + 'TanStack/table:examples/react/basic-use-table', + ] failure_modes: - - mistake: Reading table.state as a value instead of calling table.state() as an accessor - mechanism: - The Solid adapter returns table.state as a function accessor so reads can participate in - Solid tracking. table.state.pagination accesses a property on the function and is undefined. - wrong_pattern: | - const page = table.state.pagination.pageIndex // undefined - correct_pattern: | - const page = table.state().pagination.pageIndex - source: docs/framework/solid/guide/table-state.md (Reading Reactive State with createTable) - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48 - - mistake: Passing data as a plain reactive accessor value instead of a getter - mechanism: - Solid loses fine-grained reactivity once a signal value is destructured into a plain object. - Without a get data() getter, createTable never re-syncs setOptions when the underlying signal changes. - wrong_pattern: | - const [data] = createSignal(makeData(100)) - const table = createTable({ - features, - rowModels: {}, - columns, - data: data(), - }) - correct_pattern: | - const [data] = createSignal(makeData(100)) - const table = createTable({ - features, - rowModels: {}, - columns, - get data() { return data() }, - }) - source: docs/framework/solid/guide/table-state.md (Reading Reactive State with createTable); examples/solid/basic-use-table/src/App.tsx - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48 - - mistake: Using table.Subscribe child callback as a raw value - mechanism: - In Solid, table.Subscribe child receives an accessor function. Treating it as a value renders - the function reference, not the selected state. - wrong_pattern: | - s[row.id]}> - {(isSelected) => } - - correct_pattern: | - s[row.id]}> - {(isSelected) => ( - - )} - - source: docs/framework/solid/guide/table-state.md (Fine-grained Updates with table.Subscribe) - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - *id001 - maintainer_notes: - - 'Signal-based adapter: TanStack Store atoms and native framework state are equally supported. Most - developers prefer native state (Solid signals, Angular signals, Svelte runes). The skill should lead - with native-state patterns and treat atoms as the alternative for cross-app state sharing.' - - name: Svelte Table State, subscribeTable & createTableHook - slug: table-state + - 'Using useLegacyTable as the finished migration rather than converting to useTable and explicit features.' + - 'Keeping the removed global onStateChange or v8 row-model options instead of per-slice control and tableFeatures slots.' + - 'Missing React-specific FlexRender/Subscribe changes while only applying the core feature renames.' + + - slug: create-table-hook + package: '@tanstack/react-table' domain: framework-adapters - description: - Svelte 5 rune reactivity, createTable selector, table.state, subscribeTable .current pattern, - FlexRender, and createTableHook composition for @tanstack/svelte-table. type: framework - packages: - - '@tanstack/svelte-table' - covers: - - createTable - - createTable selector second argument - - table.state (selected rune-tracked state) - - table.atoms / table.baseAtoms / table.store - - subscribeTable (atom or store source) - - subscribeTable .current accessor - - FlexRender Svelte component - - svelteReactivity ($state + $derived.by + $effect.pre) - - createAtom / useSelector from @tanstack/svelte-store - - reactive get data() getter pattern - - atoms / state / on[State]Change options with $state getters - - createTableHook - tasks: - - Bind table data to a Svelte 5 $state variable through a get data() getter. - - Use subscribeTable to expose a per-slice rune-friendly reactive value with .current. - - Control sorting or pagination via $state + getter + on[State]Change updater pattern. + framework: react + purpose: 'Create a typed app-level React table hook with shared features, options, helpers, components, and contexts.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/composable-tables.md', + 'TanStack/table:docs/framework/react/guide/table-context.md', + 'TanStack/table:examples/react/composable-tables', + 'TanStack/table:packages/react-table/src/createTableHook.tsx', + 'TanStack/table:packages/react-table/src/createTableHookContexts.tsx', + ] failure_modes: - - mistake: Using subscribeTable result as a value instead of reading .current - mechanism: - subscribeTable returns a rune-friendly wrapper whose selected value lives behind .current. - Treating the wrapper itself as the value renders an object, not the page index. - wrong_pattern: | - const pageIndex = subscribeTable( - table.atoms.pagination, - (p) => p.pageIndex, - ) - // template - // Page {pageIndex + 1} - correct_pattern: | - const pageIndex = subscribeTable( - table.atoms.pagination, - (p) => p.pageIndex, - ) - // template - // Page {pageIndex.current + 1} - source: docs/framework/svelte/guide/table-state.md (Fine-grained Updates with subscribeTable) - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48; Svelte 5+ only - - mistake: Passing $state values directly to options.state without getters - mechanism: - createTable resolves options once and reads state.pagination as a snapshot. Without a getter, - runes do not surface the latest value to the table, and the controlled slice falls out of sync. - wrong_pattern: | - let pagination = $state({ pageIndex: 0, pageSize: 10 }) - const table = createTable({ - features, - rowModels: {}, - columns, - data, - state: { pagination }, - onPaginationChange: (u) => { - pagination = u instanceof Function ? u(pagination) : u - }, - }) - correct_pattern: | - let pagination = $state({ pageIndex: 0, pageSize: 10 }) - const table = createTable({ - features, - rowModels: {}, - columns, - get data() { return data }, - state: { - get pagination() { return pagination }, - }, - onPaginationChange: (u) => { - pagination = u instanceof Function ? u(pagination) : u - }, - }) - source: docs/framework/svelte/guide/table-state.md (External State); examples/svelte/basic-external-state - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48; Svelte 5+ only - - mistake: Using the legacy createSvelteTable name from Svelte 4 era - mechanism: - v9 is Svelte 5+ only and exports createTable. Older codebases referencing createSvelteTable - will fail to import. - wrong_pattern: | - import { createSvelteTable } from '@tanstack/svelte-table' - correct_pattern: | - import { createTable } from '@tanstack/svelte-table' - source: packages/svelte-table/src/createTable.svelte.ts; docs/framework/svelte/svelte-table.md - priority: HIGH - status: active - version_context: v9.0.0-alpha.48; Svelte 5+ only - - *id001 - maintainer_notes: - - 'Signal-based adapter: TanStack Store atoms and native framework state are equally supported. Most - developers prefer native state (Solid signals, Angular signals, Svelte runes). The skill should lead - with native-state patterns and treat atoms as the alternative for cross-app state sharing.' - - name: Angular Table State & injectTable - slug: table-state + - 'Using createTableHook for a one-off table or registering a full component system when only shared features/defaults are needed.' + - 'Prop-drilling table, cell, and header values through registered components instead of using the typed context hooks returned by the same factory.' + - 'Creating contexts inside a render path or introducing circular imports between the hook module and registered components, causing HMR or remount failures.' + + - slug: with-tanstack-query + package: '@tanstack/react-table' domain: framework-adapters - description: - Angular signal reactivity binding, injectTable lazy initializer, atom-as-signal reads, - computed selection with shallow equality, and createTableHook composition for @tanstack/angular-table. - type: framework - packages: - - '@tanstack/angular-table' - covers: - - injectTable - - injectTable initializer function form - - table.atoms / table.baseAtoms / table.store - - angularReactivity(injector) (signal + computed + toObservable) - - shallow equality helper - - computed selectors with equal option - - signal-based state ownership via state plus on[State]Change - - atoms option (core re-export, advanced) - - initialState - - createTableHook - tasks: - - Convert v8 createAngularTable usage to injectTable lazy initializer with signal reads. - - Select a state slice with computed(...) and the shallow equality helper to avoid extra change detection. - - Control sorting and pagination via signal + state + on[State]Change update/set pattern. + type: integration + framework: react + purpose: 'Use Query as the server-data source while Table owns data-grid state and explicit manual row-model boundaries.' + sources: + [ + 'TanStack/table:examples/react/with-tanstack-query', + 'TanStack/table:examples/react/virtualized-infinite-scrolling', + 'TanStack/table:docs/framework/react/guide/pagination.md', + ] failure_modes: - - mistake: Calling injectTable with a plain options object instead of an initializer function - mechanism: - injectTable expects a () => options factory and reruns it when read signals change. Passing - a plain object skips signal tracking and the table never resyncs when source signals update. - wrong_pattern: | - readonly table = injectTable({ - features, - rowModels: {}, - columns, - data: this.data(), - }) - correct_pattern: | - readonly table = injectTable(() => ({ - features, - rowModels: {}, - columns, - data: this.data(), - })) - source: - docs/framework/angular/guide/table-state.md (Feature-based State); packages/angular-table/src/injectTable.ts; - examples/angular/basic-inject-table - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48; Angular >=19 with signals - - mistake: Using the v8 createAngularTable name - mechanism: - v9 renamed the adapter creator to injectTable to match Angular DI conventions. createAngularTable - is no longer exported. - wrong_pattern: | - import { createAngularTable } from '@tanstack/angular-table' - correct_pattern: | - import { injectTable } from '@tanstack/angular-table' - source: docs/framework/angular/angular-table.md; packages/angular-table/src/injectTable.ts - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - mistake: Selecting object slices with computed without shallow equality - mechanism: - 'Object/array slice references change on every store update even when their contents are - unchanged. Without { equal: shallow }, downstream computeds and templates re-run unnecessarily, - defeating fine-grained signal performance.' - wrong_pattern: | - readonly pagination = computed(() => this.table.store.state.pagination) - correct_pattern: | - import { shallow } from '@tanstack/angular-table' + - 'Putting server response data into a second unsynchronized React state layer instead of deriving Table data from the query result.' + - 'Omitting pagination, sorting, or filter state from the query key and showing cached data for the wrong table state.' + - 'Enabling manual processing without passing server counts, resetting invalid pages, or preserving previous data intentionally.' - readonly pagination = computed( - () => this.table.store.state.pagination, - { equal: shallow }, - ) - source: docs/framework/angular/guide/table-state.md (Selecting State with Angular computed) - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - *id001 - maintainer_notes: - - 'Signal-based adapter: TanStack Store atoms and native framework state are equally supported. Most - developers prefer native state (Solid signals, Angular signals, Svelte runes). The skill should lead - with native-state patterns and treat atoms as the alternative for cross-app state sharing.' - - name: Lit Table State & createTableHook - slug: table-state + - slug: with-tanstack-virtual + package: '@tanstack/react-table' domain: framework-adapters - description: - Lit TableController reactive controller, atom subscription, table.state selector, table.Subscribe - render, FlexRender, and createTableHook composition for @tanstack/lit-table. - type: framework - packages: - - '@tanstack/lit-table' - covers: - - TableController - - tableController.table(options, selector?) - - table.state (selected state) - - table.atoms / table.baseAtoms / table.store - - table.Subscribe with selector or source - - table.FlexRender - - litReactivity - - createAtom from @tanstack/store - - atoms / state / on[State]Change options - - ReactiveControllerHost integration with requestUpdate - - createTableHook - tasks: - - Instantiate one TableController per LitElement host and call .table(...) inside render. - - Use table.state with a selector to keep render output focused on needed slices. - - Control sorting via Lit @state and pass state + onSortingChange updater. + type: integration + framework: react + purpose: 'Virtualize React rows or columns as a rendering concern layered over the final Table row model.' + sources: + [ + 'TanStack/table:docs/framework/react/guide/virtualization.md', + 'TanStack/table:examples/react/virtualized-rows', + 'TanStack/table:examples/react/virtualized-columns', + 'TanStack/table:examples/react/virtualized-infinite-scrolling', + ] failure_modes: - - mistake: Creating a new TableController inside render() on every update - mechanism: - TableController owns the underlying table instance and subscriptions. Constructing it in - render creates fresh subscriptions and discards prior state every cycle, causing data loss and double-subscribed - host updates. - wrong_pattern: | - protected render() { - const tableController = new TableController(this) - const table = tableController.table({ - features, - rowModels: {}, - columns, - data: this._data, - }) - return html`...` - } - correct_pattern: | - private tableController = new TableController(this) - - protected render() { - const table = this.tableController.table({ - features, - rowModels: {}, - columns, - data: this._data, - }) - return html`...` - } - source: packages/lit-table/src/TableController.ts; docs/framework/lit/guide/table-state.md; examples/lit/basic-table-controller/src/main.ts - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48 - - mistake: Expecting source-only host invalidation from table.Subscribe with a source prop - mechanism: - TableController currently wires host requestUpdate through the full table.store and optionsStore - subscriptions. table.Subscribe with a source is render-time selection sugar, not a per-source invalidation - guarantee. - wrong_pattern: | - // assuming this avoids host invalidation when other slices change - ${table.Subscribe({ - source: table.atoms.rowSelection, - selector: (s) => s[row.id], - children: (isSelected) => html``, - })} - correct_pattern: | - // accept that the host updates on any store change; use Subscribe for cleaner render code - ${table.Subscribe({ - selector: (state) => state.pagination, - children: (pagination) => html`Page ${pagination.pageIndex + 1}`, - })} - source: packages/lit-table/src/TableController.ts (LitTable Subscribe note); docs/framework/lit/guide/table-state.md - priority: MEDIUM - status: active - version_context: v9.0.0-alpha.48 - - mistake: Reading atom.get() in render and never seeing UI update - mechanism: - Atom .get() reads a value but does not register with the controller. Host invalidation - is wired through the full table.store subscription, so updates that change the read slice still - trigger render, but state from a non-controller-bound atom that is not part of table.store will - not invalidate the host. - wrong_pattern: | - // separate atom not passed via atoms option - const externalAtom = createAtom({ count: 0 }) - - protected render() { - return html`${externalAtom.get().count}` - } - correct_pattern: | - // wire external atoms through table.atoms or maintain a separate subscription - private tableController = new TableController(this) - private _externalAtom = createAtom({ count: 0 }) - private _unsub?: () => void - - connectedCallback() { - super.connectedCallback() - this._unsub = this._externalAtom.subscribe(() => this.requestUpdate()) - } + - 'Treating Virtual as a Table feature or row model instead of virtualizing table.getRowModel().rows in the renderer.' + - 'Measuring rows against the wrong scroll element or omitting stable keys and data-index values for dynamic measurement.' + - 'Combining semantic table layout, sticky headers/columns, transforms, and dynamic heights without copying the example CSS positioning contract.' - disconnectedCallback() { - super.disconnectedCallback() - this._unsub?.() - } - source: docs/framework/lit/guide/table-state.md (Reading State Without Subscribing); packages/lit-table/src/TableController.ts - priority: MEDIUM - status: active - version_context: v9.0.0-alpha.48 - - *id001 - maintainer_notes: - - Lit adapter is scheduled for a rewrite with TanStack Lit Store during beta. Treat the current API - as v9-alpha; expect refinements in beta. - - name: Preact Table State, Subscribe & createTableHook - slug: table-state + - slug: getting-started + package: '@tanstack/preact-table' domain: framework-adapters - description: - Preact reactivity binding mirroring React, useTable selector, table.Subscribe rendering, - FlexRender, and createTableHook composition for @tanstack/preact-table. type: framework - packages: - - '@tanstack/preact-table' - covers: - - useTable - - useTable selector second argument - - table.state (selected state) - - table.atoms / table.baseAtoms / table.store - - table.Subscribe with selector or source - - Subscribe (standalone) - - table.FlexRender - - useCreateAtom from @tanstack/preact-store - - useSelector from @tanstack/preact-store - - atoms / state / on[State]Change options - - initialState - - createTableHook (createAppColumnHelper, useAppTable, useTableContext, useCellContext, useHeaderContext) - tasks: - - Set up useTable in Preact with stable references for data, columns, and features. - - Optimize a large Preact table by passing () => null as the selector and subscribing lower in the tree. - - Build reusable component registries with createTableHook in Preact. + framework: preact + purpose: 'Create and render a native Preact v9 table without relying on the React adapter through preact/compat.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/migrating.md', + 'TanStack/table:examples/preact/basic-use-table', + 'TanStack/table:packages/preact-table/src/index.ts', + ] failure_modes: - - mistake: Recreating columns or features on every render - mechanism: - Preact relies on stable references for table options. New columns or features arrays each - render force useTable to call setOptions and rebuild derived state, undoing the work selectors and - Subscribe are meant to save. - wrong_pattern: | - function App({ data }) { - const columns = [ - { accessorKey: 'firstName', header: 'First name' }, - ] - const features = tableFeatures({}) - const table = useTable({ - features, - rowModels: {}, - columns, - data, - }) - return null - } - correct_pattern: | - const features = tableFeatures({}) - const columns = [ - { accessorKey: 'firstName', header: 'First name' }, - ] - - function App({ data }) { - const table = useTable({ - features, - rowModels: {}, - columns, - data, - }) - return null - } - source: docs/framework/preact/preact-table.md; examples/preact/basic-use-table/src/main.tsx - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - mistake: Using table.Subscribe inside a cell or header render context - mechanism: - The cell and header table field is typed as the core Table and does not - expose Subscribe. Import the standalone Subscribe and pass source={table.store}. - wrong_pattern: | - cell: ({ row, table }) => ( - s[row.id]}> - {(isSelected) => } - - ) - correct_pattern: | - import { Subscribe } from '@tanstack/preact-table' + - 'Importing @tanstack/react-table through preact/compat instead of the native @tanstack/preact-table v9 adapter.' + - 'Copying useReactTable or React-only rendering assumptions instead of Preact useTable and its exported helpers.' + - 'Recreating stable features, columns, or data on each render and causing unnecessary option and row-model work.' - cell: ({ row, table }) => ( - s[row.id]}> - {(isSelected) => ( - - )} - - ) - source: - docs/framework/preact/guide/table-state.md (Optimizing Re-renders with Selectors and table.Subscribe); - packages/preact-table/src/Subscribe.ts - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - mistake: Passing useTable both atoms.pagination and state.pagination for the same slice - mechanism: - External atoms take precedence over external state. Mixing them silently hides on[State]Change-driven - setState writes and leaves React-style state stale. - wrong_pattern: | - const table = useTable({ - features, - rowModels: {}, - columns, - data, - atoms: { pagination: paginationAtom }, - state: { pagination }, - onPaginationChange: setPagination, - }) - correct_pattern: | - // Pick exactly one source of truth per slice. - const table = useTable({ - features, - rowModels: {}, - columns, - data, - atoms: { pagination: paginationAtom }, - }) - source: docs/framework/preact/guide/table-state.md (Custom Initial State Note); examples/preact/basic-external-atoms - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - *id001 - - name: React Subscribe for React Compiler Compatibility - slug: react-subscribe-compiler-compat + - slug: table-state + package: '@tanstack/preact-table' domain: framework-adapters - description: - Use Subscribe / table.Subscribe to keep nested header and cell components correct under - React Compiler memoization. type: framework - packages: - - '@tanstack/react-table' - covers: - - Subscribe (standalone import) - - table.Subscribe (from useTable result) - - source prop (atom or store) - - selector prop - - render-prop children - - useSelector under the hood - - table.store - - table.atoms.rowSelection / columnPinning / etc. - tasks: - - Wrap row-selection or pin-button JSX inside nested header/cell components in Subscribe to prevent - React Compiler from memoizing stale state reads. - - Choose between subscribing to table.store with a selector vs subscribing to a single atom for per-row - UI. - - Audit a table for builder-pattern reads inside custom component boundaries that are likely memoization - risks. + framework: preact + purpose: 'Use selected table.state, Preact subscriptions, controlled slices, and external atoms correctly.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/table-state.md', + 'TanStack/table:examples/preact/basic-subscribe', + 'TanStack/table:packages/preact-table/src/useTable.ts', + ] failure_modes: - - mistake: - Reading column.getIsPinned() / row.getIsSelected() / cell.getIsAggregated() inside a custom - child component and not wrapping in Subscribe - mechanism: - React Compiler memoizes the child's JSX against the stable column / row / cell reference - it receives. The state-dependent builder method hides its atom dependency from the compiler, so - the memoized JSX never re-runs when the underlying atom changes. - wrong_pattern: | - function DraggableHeader({ header }) { - const isPinned = header.column.getIsPinned() - return - } - correct_pattern: | - import { Subscribe } from '@tanstack/react-table' + - 'Reading atom/store snapshots in render as though they create Preact subscriptions.' + - 'Pairing an onSliceChange callback with no controlled state.slice value or ignoring updater-function inputs.' + - 'Narrowing the useTable selector so far that rendered builder APIs depend on unselected state, or optimizing before a real render problem exists.' - function DraggableHeader({ header, table }) { - return ( - s.columnPinning} - > - {() => { - const isPinned = header.column.getIsPinned() - return - }} - - ) - } - source: - docs/framework/react/guide/table-state.md (Subscribe for React Compiler Compatibility); examples/react/basic-subscribe/src/main.tsx; - packages/react-table/src/Subscribe.ts - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48; React Compiler enabled - - mistake: - Using table.Subscribe from inside a cell or header definition where table is the core Table - type - mechanism: - Cell and header render contexts type table as Table, not ReactTable. - table.Subscribe is undefined. The fix is to import the standalone Subscribe and pass source={table.store} - or source={table.atoms.X}. - wrong_pattern: | - cell: ({ row, table }) => ( - s[row.id]}> - {(isSelected) => } - - ) - correct_pattern: | - import { Subscribe } from '@tanstack/react-table' + - slug: migrate-v8-to-v9 + package: '@tanstack/preact-table' + domain: framework-adapters + type: migration + framework: preact + purpose: 'Move a v8 React-adapter/preact-compat table onto the native v9 Preact adapter and architecture.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/migrating.md', + 'TanStack/table:packages/preact-table/src/index.ts', + 'TanStack/table:examples/preact/basic-use-table', + ] + failure_modes: + - 'Treating Preact as a simple hook rename while leaving the React package and compat aliases in place.' + - 'Keeping v8 get*RowModel options and globally included features instead of explicit tableFeatures slots.' + - 'Missing prototype-method, state, helper, pinning, and sorting renames shared by every v9 adapter.' - cell: ({ row, table }) => ( - s[row.id]}> - {(isSelected) => ( - - )} - - ) - source: docs/framework/react/guide/table-state.md (Tips); packages/react-table/src/Subscribe.ts - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - mistake: Wrapping every cell in Subscribe by default - mechanism: - Subscribe is overhead. For inline JSX in the parent component, the compiler always re-evaluates - when the parent re-renders, so wrapping is unnecessary. Over-wrapping adds subscription churn without - correctness benefit. - wrong_pattern: | - // Inline cell that already re-runs each parent render - {row.getVisibleCells().map((cell) => ( - s.rowSelection}> - {() => } - - ))} - correct_pattern: | - {row.getVisibleCells().map((cell) => ( - - ))} - source: docs/framework/react/guide/table-state.md (Subscribe for React Compiler Compatibility) - priority: MEDIUM - status: active - version_context: v9.0.0-alpha.48 - - mistake: Subscribing to the whole table.store for one row's selection checkbox - mechanism: - Every store change re-renders the Subscribe child. Subscribing to table.atoms.rowSelection - (or a per-row selector returning s[row.id]) limits re-renders to actual selection changes for that - row. - wrong_pattern: | - s.rowSelection[row.id]}> - {(isSelected) => } - - correct_pattern: | - s[row.id]}> - {(isSelected) => ( - - )} - - source: docs/framework/react/guide/table-state.md (Tips bullets); examples/react/basic-subscribe/src/main.tsx - priority: MEDIUM - status: active - version_context: v9.0.0-alpha.48 - - mistake: Reading raw v8 state on v9 ReactTable instead of using Subscribe - mechanism: | - v9 ReactTable returns an object whose state slices live on `table.atoms.*` / `table.store`. - Reading `table.getState().rowSelection` directly during render does not subscribe the - component — the table only re-renders when its top-level state ref changes, so checkbox - visuals, sort indicators, and pagination text go stale unless you use `` - or a state selector. - wrong_pattern: | - // ❌ Reads state during render — does NOT subscribe in v9 alpha - function Toolbar({ table }) { - const { rowSelection, sorting } = table.getState() - return
{Object.keys(rowSelection).length} selected
- } - correct_pattern: | - // ✅ Use table.Subscribe to opt into reactive updates - function Toolbar({ table }) { - return ( - ({ count: Object.keys(s.rowSelection).length })} - > - {({ count }) =>
{count} selected
} -
- ) - } - source: - 'PR #6246 "feat: change default state selectors to all"; PR #6244 (options store refactor); - useTable.ts JSDoc examples' - priority: CRITICAL - status: active - version_context: - v9 alpha pattern. Agents trained on v8 will reach for table.getState() and never - subscribe. Default selector now subscribes to ALL state slices since alpha.48, but selectors are - the documented pattern. - skills: - - react-subscribe-compiler-compat - - table-state - - customizing-feature-behavior - - mistake: setOptions in render body causing SubscribeBound warning - mechanism: | - Since alpha.20, `useTable` calls `table.setOptions()` during render instead of in - `useEffect`. That synchronously flushes store subscribers — including `SubscribeBound` - — so React warns "Cannot update a component (SubscribeBound) while rendering a - different component (Table)". The warning trips even on a single state change to a - parent prop because `optionsStore.setState() → atom.set() → propagate() → flush()` - all run synchronously inside the parent's render phase. - wrong_pattern: | - // ❌ Common v9 alpha setup — surfaces SubscribeBound warning on every parent rerender - function MyTable({ globalFilter }) { - const table = useTable({ features, data, columns, state: { globalFilter } }) - return ( - ({ globalFilter: s.globalFilter })}> - {() => ...} - - ) - } - correct_pattern: | - // ✅ Until the upstream fix lands, drive controlled state through onChange handlers - // so `state` doesn't change on every parent render, or wait for the post-alpha.48 fix. - // See PR #6244 / #6246 — KevinVandy's WIP refactor of the optionsStore for react/preact. - function MyTable() { - const [globalFilter, setGlobalFilter] = useState('') - const table = useTable({ - features, data, columns, - state: { globalFilter }, - onGlobalFilterChange: setGlobalFilter, - }) - return ({ globalFilter: s.globalFilter })}> - {() => ...} - - } - source: - 'https://github.com/TanStack/table/issues/6224 (alpha: useTable causes "Cannot update a component - (SubscribeBound)")' - priority: CRITICAL - status: active - version_context: - 'v9.0.0-alpha.20+ regression. Owner discussion in comments shows PR #6244 attempts - a fix by removing optionsStore from react/preact adapters.' - skills: - - react-subscribe-compiler-compat - - composable-tables - - mistake: Trying to subscribe to table.options.meta - mechanism: | - `table.Subscribe` subscribes to `table.store` / atoms — not to `table.options`. - Putting external state into `meta` and trying to subscribe to it via a selector - will never fire updates because options aren't part of the reactive state graph. - wrong_pattern: | - // ❌ meta lives on options, not store — Subscribe will never re-fire - const table = useTable({ ..., meta: { foo: someState } }) - s.options?.meta?.foo}> - {(foo) => {foo}} - - correct_pattern: | - // ✅ Keep external reactive data outside the table or push it into a real state slice. - // For values needed inside cells, close over them in the columns array (recreate - // columns when the value changes) or pass them via React context. - const ExternalContext = React.createContext(null) - - - - source: https://github.com/TanStack/table/issues/6236 (Unable to subscribe to table.options.meta) - priority: HIGH - status: active - version_context: - v9 alpha; subtle because meta worked as a stash bucket in v8 and people assume Subscribe - covers it - skills: - - react-subscribe-compiler-compat - - table-state - - mistake: Input loses focus inside composable AppTable on every keystroke - mechanism: | - In v9 alpha.12 (and via store v0.9 update in PR #6180), keying off a state-driven - selector inside `` caused inputs nested in toolbars/cells to lose - focus after each keypress because the entire subtree re-mounted. The fix landed - between alpha.12 → alpha.32 via the atoms refactor (PR #6234), but the underlying - lesson — Subscribe-driven children must NOT cause input remounts — is still load-bearing. - wrong_pattern: | - // ❌ Selecting a frequently-changing slice from the top-level table.AppTable boundary - // can remount everything under it. - ({ globalFilter: s.globalFilter })}> - - {/* loses focus */} - - - correct_pattern: | - // ✅ Push the Subscribe boundary as close to the consumer as possible - - - {/* keep input outside Subscribe; subscribe only inside the bit that needs it */} - - s.globalFilter}> - {(filter) => filter: {filter}} - - - - source: - 'https://github.com/TanStack/table/issues/6199 (Bug[v9]: broken inputs inside a composable - table)' - priority: HIGH - status: fixed-but-legacy-risk - version_context: v9 alpha.12 regression, fixed by alpha.32. Pattern lesson still applies. - skills: - - react-subscribe-compiler-compat - - composable-tables - - mistake: Using v8 useReactTable with React Compiler skips memoization - mechanism: | - React Compiler emits "Use of incompatible library" warning when it sees - `useReactTable()` because the returned table instance has mutable methods whose - references change. Compiler bails out and skips memoization for that component, - defeating the optimization. The v9 alpha Subscribe pattern is the answer — agents - should NOT add `'use no memo'` pragmas as a workaround. - wrong_pattern: | - // ❌ React Compiler skips this entire component - 'use client' - import { useReactTable, getCoreRowModel } from '@tanstack/react-table' - export function DataTable({ columns, data }) { - const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) - return
{header.column.id}{header.column.id}{flexRender(cell.column.columnDef.cell, cell.getContext())} - -
- } - correct_pattern: | - // ✅ Move to v9 alpha useTable + Subscribe; opt-in subscription model is React-Compiler-friendly - 'use client' - import { useTable, tableFeatures, rowSelectionFeature } from '@tanstack/react-table' - const features = tableFeatures({ rowSelectionFeature }) - export function DataTable({ columns, data }) { - const table = useTable({ features, data, columns }) - return ( - ({ rowSelection: s.rowSelection })}> - {() =>
} - - ) - } - source: - https://github.com/TanStack/table/issues/6137 (React Compiler skips memoization), https://github.com/TanStack/table/issues/5567 - (Table doesn’t re-render with new React Compiler + React 19) - priority: CRITICAL - status: active - version_context: - v8 fundamental limitation. The whole v9 alpha redesign exists to fix this. Agents - trained on v8 will keep producing the broken pattern. - - mistake: Table does not re-render when data changes under React Compiler - mechanism: | - With React Compiler enabled and React 19, mutating an array used as `data` and - then setting the state doesn't cause the table body to re-render because the - compiler memoizes `table.getRowModel().rows` based on the (unchanged) table - reference. The v8 row-model output is a method call, so the compiler can't tell - it depends on `data` identity. - wrong_pattern: | - // ❌ data updates but rows don't refresh - const [data, setData] = useState([]) - const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) - function addRow() { - setData([...data, newRow]) // table.getRowModel().rows is memoized stale - } - correct_pattern: | - // ✅ Use v9 alpha — table.Subscribe drives re-renders explicitly - const table = useTable({ features, data, columns }) - s.rowSelection}> - {() => ( - - {table.getRowModel().rows.map(row => )} - - )} - - source: https://github.com/TanStack/table/issues/5567 (29 comments) - priority: CRITICAL - status: active - version_context: - v8 + React Compiler — top-engagement open issue with 29 comments; v9 alpha designed - specifically to address this - skills: - - react-subscribe-compiler-compat - - state-management - - mistake: Premature Subscribe / custom selector optimization on small tables - mechanism: - Subscribe and narrow selectors are designed for large or expensive tables where full re-renders - measurably hurt. On small tables (a few hundred rows, simple features), default selector + inline - rendering is fine and the Subscribe boundaries add complexity without measurable benefit. - wrong_pattern: |- - // ❌ Wraps every header and cell in Subscribe on a 50-row table - header: ({ table }) => ( - s.sorting}> - {() => } - - ) - correct_pattern: |- - // ✅ Default useTable selector, inline rendering — Subscribe only when measured perf demands it - const table = useTable({ features, rowModels, columns, data }) - // reach for Subscribe later, scoped to the actual hotspot - priority: MEDIUM - version_context: 'Maintainer guidance: advanced state-management patterns are for advanced cases.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - name: Angular Rendering Directives & DI Context - slug: angular-rendering-directives + - slug: create-table-hook + package: '@tanstack/preact-table' domain: framework-adapters - description: - Structural directives flexRender / flexRenderCell / flexRenderHeader / flexRenderFooter - plus context tokens for prop-drill-free Angular rendering. type: framework - packages: - - '@tanstack/angular-table' - covers: - - FlexRender (tuple export of FlexRenderDirective and FlexRenderCell) - - '*flexRender structural directive' - - '*flexRenderCell shorthand' - - '*flexRenderHeader shorthand' - - '*flexRenderFooter shorthand' - - flexRenderProps input - - flexRenderComponent(component, options) - - inputs / outputs / bindings / directives / injector options - - inputBinding / outputBinding / twoWayBinding (Angular v20+) - - injectFlexRenderContext - - TanStackTable directive + injectTableContext / TanStackTableToken - - TanStackTableHeader directive + injectTableHeaderContext / TanStackTableHeaderToken - - TanStackTableCell directive + injectTableCellContext / TanStackTableCellToken - - TemplateRef rendering with implicit context - tasks: - - Render cells and headers with the shorthand structural directives, falling back to *flexRender for - custom props. - - Pass dynamic inputs and outputs to a cell component with flexRenderComponent. - - Eliminate input drilling by applying tanStackTableCell / tanStackTableHeader / tanStackTable directives - and injecting context tokens in descendants. + framework: preact + purpose: 'Build a typed reusable Preact table hook and optional component/context registry.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/composable-tables.md', + 'TanStack/table:docs/framework/preact/guide/table-context.md', + 'TanStack/table:examples/preact/composable-tables', + 'TanStack/table:packages/preact-table/src/createTableHook.tsx', + 'TanStack/table:packages/preact-table/src/createTableHookContexts.tsx', + ] failure_modes: - - mistake: Forgetting to import FlexRender in a standalone component - mechanism: - FlexRender is exported as a tuple of directives. Angular standalone components only see - directives that are listed in imports, so without the import, *flexRender and *flexRenderCell silently - do nothing or fail template compilation. - wrong_pattern: | - @Component({ - imports: [], - templateUrl: './app.html', - }) - export class AppComponent {} - correct_pattern: | - import { FlexRender } from '@tanstack/angular-table' - - @Component({ - imports: [FlexRender], - templateUrl: './app.html', - }) - export class AppComponent {} - source: docs/framework/angular/guide/rendering.md (FlexRender import) - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48; Angular >=19 - - mistake: - Returning a component class but expecting all CellContext properties to be visible without - input signals - mechanism: - FlexRender wires render context onto the component via ComponentRef.setInput, but only - declared input() / input.required() properties receive values. Undeclared properties are silently - ignored. - wrong_pattern: | - @Component({ template: `{{ cell.getValue() }}` }) - export class MyCellComponent { - cell: any // not declared as input - } - correct_pattern: | - import { Component, input } from '@angular/core' - import type { CellContext } from '@tanstack/angular-table' + - 'Using the app hook for one table when standalone useTable is clearer.' + - 'Importing context hooks from a different factory or reading them outside the matching AppTable/AppCell/AppHeader provider.' + - 'Prop-drilling stable table instances through memoized components and expecting context consumers to update without the factory wrappers/subscriptions.' - @Component({ template: `{{ cell().getValue() }}` }) - export class MyCellComponent { - readonly cell = input.required>() - } - source: docs/framework/angular/guide/rendering.md (Returning a component class) - priority: HIGH - status: active - version_context: v9.0.0-alpha.48; Angular >=19 - - mistake: Mixing bindings with inputs / outputs on the same property in flexRenderComponent - mechanism: - bindings are applied at component creation, inputs / outputs are applied post-creation. - Combining them on the same property causes double initialization or conflicting values. - wrong_pattern: | - flexRenderComponent(EditableCell, { - inputs: { value: 'a' }, - bindings: [inputBinding('value', this.name)], - }) - correct_pattern: | - flexRenderComponent(EditableCell, { - bindings: [ - inputBinding('value', this.name), - outputBinding('valueChange', (v) => console.log(v)), - ], - }) - source: docs/framework/angular/guide/rendering.md (bindings API note) - priority: MEDIUM - status: active - version_context: v9.0.0-alpha.48; Angular v20+ for bindings - - mistake: Drilling cell or header through input() in nested presentational components - mechanism: - Using FlexRender + context directives makes injectTableCellContext / injectTableHeaderContext - / injectTableContext available to any descendant of the [tanStackTableCell] / [tanStackTableHeader] - / [tanStackTable] host. Drilling defeats the DI pattern and complicates refactors. - wrong_pattern: | - - correct_pattern: | - - - // app-cell-actions.ts - import { injectTableCellContext } from '@tanstack/angular-table' + - slug: with-tanstack-query + package: '@tanstack/preact-table' + domain: framework-adapters + type: integration + framework: preact + purpose: 'Drive manually processed Preact table data from TanStack Query and table state.' + sources: + [ + 'TanStack/table:examples/preact/with-tanstack-query', + 'TanStack/table:docs/framework/preact/guide/pagination.md', + ] + failure_modes: + - 'Copying React Query hook names or React state glue instead of the Preact Query example.' + - 'Leaving table state out of the query key or query function request.' + - 'Expecting manual pagination/filtering/sorting flags to fetch or transform server data.' - @Component({ template: `` }) - export class CellActionsComponent { - readonly cell = injectTableCellContext() - onAction() { console.log(this.cell()) } - } - source: docs/framework/angular/guide/rendering.md (Context directives) - priority: MEDIUM - status: active - version_context: v9.0.0-alpha.48; Angular >=19 - - mistake: - Calling injectFlexRenderContext outside a *flexRender rendered subtree without applying the - corresponding context directive - mechanism: - Tokens are only provided in the child injector that FlexRender creates, or where the [tanStackTable] - / [tanStackTableHeader] / [tanStackTableCell] directives are applied. Components outside both scopes - throw a NullInjectorError. - wrong_pattern: | - // Sibling component in the same - correct_pattern: | - - source: docs/framework/angular/guide/rendering.md (Automatic token injection in FlexRender) - priority: MEDIUM - status: active - version_context: v9.0.0-alpha.48; Angular >=19 - - mistake: Defining columns inside createAngularTable signal causes flexRender remount on every tick - mechanism: | - When the `columns` array is defined inside the `createAngularTable` options - function, Angular recreates the array on every reactive update. `FlexRenderComponent` - sees new references and remounts components, running `ngOnInit` every time — - tanking performance. - wrong_pattern: | - // ❌ Columns recreated inside the signal scope - table = createAngularTable(() => ({ - data: this.data(), - columns: [ - { id: 'name', header: 'Name', cell: flexRenderComponent(NameCell) }, - ], - getCoreRowModel: getCoreRowModel(), - })) - correct_pattern: | - // ✅ Hoist columns out of the options function - private columns = [ - { id: 'name', header: 'Name', cell: flexRenderComponent(NameCell) }, - ] - table = createAngularTable(() => ({ - data: this.data(), - columns: this.columns, - getCoreRowModel: getCoreRowModel(), - })) - source: https://github.com/TanStack/table/issues/6159 ([Angular Table] poor performance of flexRenderComponent) - priority: CRITICAL - status: active - version_context: - v8.21.3 angular, also affects v9. Doc gap acknowledged by maintainers in the closed-as-docs - label. - skills: - - angular-rendering-directives - - rendering - - mistake: createAngularTable in library project emits ɵSIGNAL TS4023 - mechanism: | - When `createAngularTable` is used in a buildable Angular library, the inferred - return type references internal `@angular/core` symbols (`SIGNAL`, `ɵSIGNAL`) - that can't be named from outside. Build fails with TS4023. - wrong_pattern: | - // ❌ In a library file; build fails - @Component(...) - export class MyTable { - table = createAngularTable(() => ({ data, columns, getCoreRowModel: getCoreRowModel() })) - } - correct_pattern: | - // ✅ Explicitly type or wrap with helper to break the inferred reference - import type { AngularTable } from '@tanstack/angular-table' - @Component(...) - export class MyTable { - table: AngularTable = createAngularTable(() => ({ ... })) - } - source: - https://github.com/TanStack/table/issues/5774 (createAngularTable in monorepo causes ɵSIGNAL - error) - priority: HIGH - status: fixed-but-legacy-risk - version_context: - v8.20.5 angular + Angular 18; closed after Angular update but library pattern is - common - - name: Lit TableController Pattern - slug: lit-table-controller + - slug: with-tanstack-virtual + package: '@tanstack/preact-table' domain: framework-adapters - description: - TableController ReactiveController pattern for hosting a TanStack Table instance inside - a LitElement. - type: framework - packages: - - '@tanstack/lit-table' - covers: - - TableController class - - TableController constructor(host) addController side effect - - tableController.table(options, selector?) - - table.state (selected state) - - table.Subscribe ({ source, selector, children }) - - table.FlexRender (cell / header / footer rendering) - - litReactivity - - hostConnected / hostDisconnected lifecycle - - table.store and table.optionsStore subscriptions driving requestUpdate - tasks: - - Create a single TableController per LitElement and call .table(...) each render. - - Pass a selector to .table(...) to reduce work in the render method. - - Render headers and cells via table.FlexRender or table.Subscribe in lit-html templates. + type: integration + framework: preact + purpose: 'Apply Preact Virtual rendering to Table rows or columns using the adapter virtualization guide.' + sources: + [ + 'TanStack/table:docs/framework/preact/guide/virtualization.md', + 'TanStack/table:packages/preact-table/src/index.ts', + ] failure_modes: - - mistake: Instantiating TableController inside render() instead of as a class field - mechanism: - TableController.addController and store subscriptions run in the constructor. Recreating - it per render leaks subscriptions, drops the prior table instance (losing state), and double-triggers - host updates. - wrong_pattern: | - protected render() { - const controller = new TableController(this) - const table = controller.table({ features, rowModels: {}, columns, data: this._data }) - return html`...` - } - correct_pattern: | - private tableController = new TableController(this) + - 'Copying React-only component/hook details instead of using the Preact Virtual API exported for the installed versions.' + - 'Virtualizing raw input data instead of the final filtered, sorted, expanded, and paginated row model.' + - 'Assuming Table supplies scroll containers, measurements, spacer geometry, sticky CSS, or overscan defaults.' - protected render() { - const table = this.tableController.table({ - features, - rowModels: {}, - columns, - data: this._data, - }) - return html`...` - } - source: packages/lit-table/src/TableController.ts; examples/lit/basic-table-controller/src/main.ts - priority: CRITICAL - status: active - version_context: v9.0.0-alpha.48 - - mistake: Using table.FlexRender as a Lit directive instead of a function call - mechanism: - table.FlexRender is a function that returns a TemplateResult. It is invoked inside ${...} - interpolation with a header / cell / footer argument, not used as a custom element. - wrong_pattern: | - html`` - correct_pattern: | - html`` - source: packages/lit-table/src/TableController.ts (FlexRender attached); docs/framework/lit/lit-table.md - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - mistake: Calling table.Subscribe children outside a template interpolation - mechanism: - table.Subscribe returns a TemplateResult or string. It must be embedded with ${...} inside - an html`` template. Calling it as a statement produces a value that the renderer never sees. - wrong_pattern: | - protected render() { - table.Subscribe({ - selector: (s) => s.pagination, - children: (p) => html`${p.pageIndex + 1}`, - }) - return html`
` - } - correct_pattern: | - protected render() { - return html` -
- ${table.Subscribe({ - selector: (s) => s.pagination, - children: (p) => html`Page ${p.pageIndex + 1}`, - })} -
- ` - } - source: packages/lit-table/src/TableController.ts (Subscribe shape); docs/framework/lit/guide/table-state.md - priority: HIGH - status: active - version_context: v9.0.0-alpha.48 - - mistake: - Forgetting that host update is wired through table.store, so source-only Subscribe does not - skip host updates - mechanism: - TableController subscribes the host to the full table.store and optionsStore. table.Subscribe - with a source prop reads source.get() at render time but does not currently install an independent - host subscription, so the host still updates on any store change. - wrong_pattern: | - // expecting only rowSelection changes to invalidate the host - ${table.Subscribe({ - source: table.atoms.rowSelection, - selector: (s) => s[row.id], - children: (isSelected) => html``, - })} - correct_pattern: | - // accept host updates on any store change; reach for an explicit manual subscription only if you measure a real bottleneck - ${table.Subscribe({ - source: table.atoms.rowSelection, - selector: (s) => s[row.id], - children: (isSelected) => html` - - `, - })} - source: - packages/lit-table/src/TableController.ts (LitTable Subscribe note); docs/framework/lit/guide/table-state.md - (Selecting State with table.Subscribe) - priority: MEDIUM - status: active - version_context: v9.0.0-alpha.48 - maintainer_notes: - - The Lit adapter is scheduled to be rewritten with TanStack Lit Store during the v9 beta cycle. Current - contracts (TableController, store-level invalidation) may change. Skill content should describe the - current shape but note that beta may bring an API revision. - slug: getting-started - type: lifecycle - domain: lifecycle - status: ready - description: | - End-to-end first-table journey: install the framework adapter, declare `features` via `tableFeatures()`, declare `rowModels` factories with their *Fns parameters, create a column helper with both `TFeatures` and `TData` generics, instantiate `useTable` / `injectTable` / `createTable`, and render headers + cells with `table.FlexRender` (or `*flexRender*` directives on Angular). New users land here, not on `useLegacyTable`. - covers: - - Picking the right adapter package (`@tanstack/react-table`, `@tanstack/vue-table`, `@tanstack/angular-table`, - `@tanstack/solid-table`, `@tanstack/svelte-table` (Svelte 5+), `@tanstack/preact-table`, `@tanstack/lit-table`). - - 'Declaring the minimum-viable v9 table (no features) — must still pass `features: tableFeatures({})` - and `rowModels: {}`. Core row model is automatic; you only register filtered/sorted/paginated/etc. - when you use them.' - - "`createColumnHelper()` and the new `columnHelper.columns([...])` variadic - wrapper for preserving each column's `TValue` in nested/group columns." - - Rendering with `` / `` (preferred) - or the standalone `` import. `flexRender(def, ctx)` still works. - - 'Angular variant: `injectTable(() => ({ ... }))` returning a signal- backed instance; render via `*flexRenderHeader`, - `*flexRenderCell`, `*flexRenderFooter`. Cell/header render fns run inside an Angular injection context - — `inject()` is safe.' - - Default `useTable` selector is `(state) => state` — full re-render on any state change, mirroring - v8 ergonomics. Narrow it later only when you measure a problem. - - 'Where to go next: `migrate-v8-to-v9` if upgrading, `production- readiness` once it works.' - packages: - - '@tanstack/react-table' - - '@tanstack/vue-table' - - '@tanstack/angular-table' - - '@tanstack/solid-table' - - '@tanstack/svelte-table' - - '@tanstack/preact-table' - - '@tanstack/lit-table' - tasks: - - Render a basic v9 table with 3 columns and an in-memory array. - - 'Add sorting: register `rowSortingFeature` in `features` and `sortedRowModel: createSortedRowModel(sortFns)` - in `rowModels`.' - - Add pagination + filtering on top of sorting without touching the rest of the setup. - - Type a `ColumnDef[]` array outside the component for stable references. - correct_pattern: | - // React — minimum viable v9 table - import { - useTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, - createColumnHelper, - } from '@tanstack/react-table' - - type Person = { firstName: string; lastName: string; age: number } - - // Define OUTSIDE the component for stable identity. - const features = tableFeatures({ rowSortingFeature }) - const columnHelper = createColumnHelper() - - const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('lastName', { header: 'Last' }), - columnHelper.accessor('age', { header: 'Age' }), - ]) - - function PeopleTable({ data }: { data: Person[] }) { - const table = useTable({ - features, - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, - data, - }) - - return ( -
- - - - as a *flexRenderCell-rendered cell - - - - - - - ${table.FlexRender({ cell })}
- {table.getHeaderGroups().map(hg => ( - {hg.headers.map(h => ( - - ))} - ))} - {table.getRowModel().rows.map(row => ( - {row.getAllCells().map(cell => ( - - ))} - ))} -
- -
- ) - } - wrong_pattern: | - // BAD: agents trained on v8 will produce this and it will not compile. - // (1) `useReactTable` was removed in favor of `useTable`. - // (2) `getCoreRowModel: getCoreRowModel()` is no longer accepted — - // core is implicit; everything else moved to `rowModels`. - // (3) `createColumnHelper()` is missing the TFeatures generic. - // (4) No `features` => "Property 'features' is missing in type". - import { useReactTable, getCoreRowModel, createColumnHelper, flexRender } - from '@tanstack/react-table' - - const columnHelper = createColumnHelper() // wrong arity - const table = useReactTable({ // wrong hook - columns, - data, - getCoreRowModel: getCoreRowModel(), // removed option - }) + package: '@tanstack/solid-table' + domain: framework-adapters + type: framework + framework: solid + purpose: 'Create a Solid v9 table with createTable, reactive getters, and JSX rendering.' + sources: + [ + 'TanStack/table:docs/framework/solid/guide/migrating.md', + 'TanStack/table:examples/solid/basic-use-table', + 'TanStack/table:packages/solid-table/src/index.tsx', + ] failure_modes: - - "Forgetting `features: tableFeatures({})` — the option is required even for a 'no features' table. - Type error: 'features' is missing." - - 'Skipping `rowModels: {}` — same as above. Core is automatic but the option key itself must be present.' - - Calling `createColumnHelper()` instead of `createColumnHelper()`. - The generic order changed in v9 and the old shape doesn't compile. - - Defining `features` / `columns` / `data` literals inside the render body — every render produces - a new reference and busts table-internal memoization. Always declare these outside the component or - wrap in `useMemo`. - - Importing `useReactTable` (v8) or `createAngularTable` (v8 Angular) — both were renamed. v9 uses `useTable` - / `injectTable` / `createTable` consistently across adapters. - - Reaching for `useLegacyTable` for a new project. It's deprecated and ships every feature in the bundle. - New code must use `useTable` with explicit `features`. - - mistake: Reimplementing what TanStack Table's built-in APIs already provide - mechanism: - TanStack Table IS a state-management coordinator with built-in APIs for nearly every state - transition (`table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, - `column.toggleVisibility`, …). Agents often write their own setState logic, click handlers, or sort/filter - loops rather than using the built-ins, producing more code that is also less correct (skips internal - invariants, breaks reset APIs). - wrong_pattern: |- - // ❌ Reimplements sorting state manually instead of using the API - const [sorting, setSorting] = useState([]) - const sortedData = useMemo(() => [...data].sort((a,b) => /* …custom… */), [data, sorting]) - // then uses sortedData directly, bypassing the table - correct_pattern: |- - // ✅ Use the built-in APIs — table handles state, reset, multi-sort, etc. - const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - // then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() - priority: CRITICAL - version_context: - 'Always present. The maintainer flags this as the #1 tell that "an AI wrote this." - See `setSorting`/`setColumnFilters`/`toggleSelected`/`nextPage`/etc.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - mistake: API or state slice "missing" because the feature was not registered in `features` - mechanism: - In v9, `features` is a tree-shakeable registry. If a feature is not in `features`, TypeScript - hides its APIs and the runtime atom is not created. Agents who copy a snippet for `table.setColumnFilters(...)` - without registering `columnFilteringFeature` see a TS error or `table.atoms.columnFilters` is undefined - — and may incorrectly conclude the feature is broken or removed in v9. - wrong_pattern: |- - // ❌ rowSortingFeature missing — table.setSorting / state.sorting unavailable - const features = tableFeatures({}) // empty - const table = useTable({ features, rowModels: {}, columns, data }) - table.setSorting([{ id: 'age', desc: true }]) // ❌ does not exist on this table type - correct_pattern: |- - // ✅ Register every feature you intend to use; pair with its row model when applicable - const features = tableFeatures({ rowSortingFeature, rowPaginationFeature }) - const table = useTable({ - features, - rowModels: { - sortedRowModel: createSortedRowModel(sortFns), - paginatedRowModel: createPaginatedRowModel(), - }, - columns, data, - }) - priority: CRITICAL - version_context: - v9-specific. This is the first version of TanStack Table where features must be declared - for TypeScript and runtime to expose them. Expect heavy confusion from devs and agents trained on - v8. - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - slug: migrate-v8-to-v9 - type: lifecycle - domain: lifecycle - status: ready - description: | - Mechanical breaking-change migration from TanStack Table v8 to v9 across every adapter. Every old-shaped option, type, or method an agent will try to reproduce from v8 muscle memory has a v9 equivalent enumerated below. For React projects that cannot migrate every table at once, `useLegacyTable` from `@tanstack/react-table/legacy` accepts the v8 API shape on top of the v9 engine — deprecated, ships every feature, no `table.Subscribe`, but lets you upgrade incrementally. - covers: - - Hook rename per adapter (React, Angular, Solid, Vue, Preact, Svelte 5+, Lit, vanilla). - - '`features` + `rowModels` becoming required options; row-model factories now take their *Fns set - as a parameter.' - - '`createColumnHelper()` → `createColumnHelper()` (generic order + - arity change).' - - 'State surface: `table.getState()` → `table.store.state` (full) or `table.state` (selector output) - or `table.atoms..get()`.' - - '`flexRender(def, ctx)` still works; new preferred forms are `` and the standalone ``.' - - 'Sorting renames: `sortingFn` → `sortFn`, `sortingFns` → `sortFns`, `getSortingFn()` → `getSortFn()`, - etc.' - - '`enablePinning` split into `enableColumnPinning` + `enableRowPinning`.' - - '`ColumnSizing` feature split into `columnSizingFeature` + `columnResizingFeature`; `columnSizingInfo` - → `columnResizing`; `onColumnSizingInfoChange` → `onColumnResizingChange`.' - - '`_`-prefixed internal APIs removed (`row._getAllCellsByColumnId()` → `row.getAllCellsByColumnId()`, - `table._getFacetedRowModel()` → public form, etc.).' - - 'Type generics now carry `TFeatures` first: `Column`, `Table`, `Row`, `ColumnMeta`.' - - '`RowData` tightened from `unknown | object | any[]` to `Record | Array`.' - - Angular has no legacy adapter — Angular projects must migrate directly. Svelte v9 supports Svelte - 5+ only; stay on v8 for Svelte 3/4. - - 'The escape hatch: `useLegacyTable` (React) + `legacyCreateColumnHelper` + `getCoreRowModel`/`getSortedRowModel`/etc. - from `@tanstack/react-table/legacy` — same call shape as v8, no fine-grained subscriptions, larger - bundle.' - - 'The other escape hatch: `stockFeatures` to opt into "everything" without listing features explicitly.' - packages: - - '@tanstack/react-table' - - '@tanstack/vue-table' - - '@tanstack/angular-table' - - '@tanstack/solid-table' - - '@tanstack/svelte-table' - - '@tanstack/preact-table' - - '@tanstack/lit-table' - - '@tanstack/table-core' - tasks: - - 'Upgrade a v8 React table to v9 by renaming `useReactTable` → `useTable`, moving `getCoreRowModel`/`getFilteredRowModel`/`getSortedRowModel`/`getPaginationRowModel` - options into `rowModels`, and adding `features: tableFeatures({...})`.' - - Bridge a v8 React table to v9 via `useLegacyTable` from `@tanstack/react-table/legacy` so the rest - of the app can upgrade incrementally. - - Rename `table.getState()` reads throughout the codebase to `table.store.state` (full read) or `table.state` - (typed selector output) or `table.atoms..get()` (single-slice atom read). - - Update all `createColumnHelper()` call sites to `createColumnHelper()` - and adjust `ColumnDef` / `Column` / `Row` / `Cell` type annotations - to the new `TFeatures`-first generic order. - correct_pattern: | - // === v9 (correct) === - import { - useTable, - tableFeatures, - rowSortingFeature, - rowPaginationFeature, - columnFilteringFeature, - columnSizingFeature, - columnResizingFeature, - createColumnHelper, - createSortedRowModel, - createFilteredRowModel, - createPaginatedRowModel, - sortFns, - filterFns, - } from '@tanstack/react-table' - import type { ColumnDef } from '@tanstack/react-table' - - const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - columnFilteringFeature, - columnSizingFeature, - columnResizingFeature, // explicit — formerly part of ColumnSizing - }) - - const columnHelper = createColumnHelper() - - const columns: ColumnDef[] = columnHelper.columns([ - columnHelper.accessor('name', { - header: 'Name', - sortFn: 'alphanumeric', // renamed from sortingFn - }), - ]) - - const table = useTable({ - features, - rowModels: { - sortedRowModel: createSortedRowModel(sortFns), - filteredRowModel: createFilteredRowModel(filterFns), - paginatedRowModel: createPaginatedRowModel(), - }, - columns, - data, - enableColumnPinning: true, // split from enablePinning - enableRowPinning: true, - }) + - 'Using the v8 createSolidTable entrypoint or a React hook instead of v9 createTable.' + - 'Reading a signal once and passing its snapshot as data instead of a getter/reactive option when updates are expected.' + - 'Expecting Table to render DOM or CSS rather than iterating row/header APIs in Solid JSX.' - // State reads: - const all = table.store.state // full snapshot - const sorting = table.atoms.sorting.get() // per-slice atom - const cells = row.getAllCellsByColumnId() // no underscore - - // Rendering: - // - // - wrong_pattern: | - // === v8 muscle memory — an agent will produce all of this, and ALL of it - // is broken in v9. Each line is a specific failure mode. === - - import { - useReactTable, // (1) renamed → useTable - getCoreRowModel, // (2) no longer accepted as an option; - getFilteredRowModel, // move to `rowModels` as factories - getSortedRowModel, // created via `createSortedRowModel(sortFns)` etc. - getPaginationRowModel, - createColumnHelper, // (3) needs now - sortingFns, // (4) renamed → sortFns - filterFns, - flexRender, // still exists, but prefer table.FlexRender - } from '@tanstack/react-table' - import type { ColumnDef, Row } from '@tanstack/react-table' - - const columnHelper = createColumnHelper() // wrong generic arity - - const columns: ColumnDef[] = [ // (5) ColumnDef now - { - accessorKey: 'name', - header: 'Name', - sortingFn: 'alphanumeric', // (6) renamed → sortFn - }, + - slug: table-state + package: '@tanstack/solid-table' + domain: framework-adapters + type: framework + framework: solid + purpose: 'Use Solid-backed table atoms inside tracked scopes and choose native signals or external atoms for ownership.' + sources: + [ + 'TanStack/table:docs/framework/solid/guide/table-state.md', + 'TanStack/table:examples/solid/basic-external-state', + 'TanStack/table:packages/solid-table/src/createTable.ts', ] - - const table = useReactTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), // (2) move into `rowModels` - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - filterFns, // (7) no longer a root option — passed to factories - sortingFns, // (4)+(7) renamed AND no longer a root option - enablePinning: true, // (8) split → enableColumnPinning / enableRowPinning - onColumnSizingInfoChange: setInfo, // (9) renamed → onColumnResizingChange - }) - - // (10) BAD reads: - const all = table.getState() // → table.store.state (or table.state with a selector) - const cells = row._getAllCellsByColumnId() // underscore removed → row.getAllCellsByColumnId() - - // (11) Module augmentation - declare module '@tanstack/react-table' { - interface ColumnMeta { // (11) v9 adds TFeatures as first generic - customProp: string - } - } - - // === Quick-bridge alternative (DEPRECATED, but compiles): === - // Move just THIS file to `useLegacyTable` and keep the v8 shape; migrate - // it to the modern v9 shape later. Imports come from `/legacy`. - import { useLegacyTable, getCoreRowModel, legacyCreateColumnHelper } - from '@tanstack/react-table/legacy' - const legacyHelper = legacyCreateColumnHelper() - const legacyTable = useLegacyTable({ - columns, data, - getCoreRowModel: getCoreRowModel(), - }) failure_modes: - - Importing `useReactTable` / `createAngularTable` / `createSolidTable` (v8 names). All adapters consolidated - to `useTable` / `injectTable` / `createTable` in v9. The v8 imports will not exist and will fail at - module-resolution time. - - 'Passing `getCoreRowModel: getCoreRowModel()` / `getSortedRowModel: getSortedRowModel()` as root options. - These were removed — sortedRowModel/filteredRowModel/etc. now live under `rowModels` and are produced - by `createSortedRowModel(sortFns)`-style factories. Core is automatic.' - - Calling `createSortedRowModel()` without passing `sortFns`. The factories now REQUIRE their *Fns parameter - for tree-shaking — `createFilteredRowModel(filterFns)`, `createSortedRowModel(sortFns)`, `createGroupedRowModel(aggregationFns)`. - - "Omitting `features` entirely. The option is required even for a no-features table — pass `features: - tableFeatures({})` or `features: stockFeatures` if you want v8-like 'everything on'." - - Calling `createColumnHelper()` (v8 arity). v9 requires `createColumnHelper()`. `typeof features` is the standard idiom — declare features once at module scope and reuse - the type. - - Reading state via `table.getState()`. The method was removed. Use `table.store.state` for a flat snapshot, - `table.state` if you passed a `useTable` selector, or `table.atoms..get()` for a single slice. - - 'Renaming misses: `sortingFn` (column def) → `sortFn`, `sortingFns` → `sortFns`, `getSortingFn()` - → `getSortFn()`, `getAutoSortingFn()` → `getAutoSortFn()`, `SortingFn`/`SortingFns` types → `SortFn`/`SortFns`. - TypeScript will surface these but the agent will try the v8 names first.' - - 'Using `enablePinning: true`. It was split into `enableColumnPinning` and `enableRowPinning`. Pick - one (or both).' - - 'Treating column resizing as part of column sizing. v9 splits them: pass `columnSizingFeature` for - fixed widths and ALSO `columnResizingFeature` for drag-to-resize. State key `columnSizingInfo` is - now `columnResizing`; option `onColumnSizingInfoChange` is `onColumnResizingChange`.' - - 'Calling underscore-prefixed APIs: `row._getAllCellsByColumnId()`, `table._getFacetedRowModel()`, - `table._getFacetedMinMaxValues()`, `table._getFacetedUniqueValues()`, `table._getPinnedRows()`. All - became public — drop the underscore.' - - "`declare module '@tanstack/react-table' { interface ColumnMeta }` — v9 added `TFeatures` - as the first generic, so this becomes `ColumnMeta`. Module augmentation - silently widens types if the generic arity is wrong." - - Reaching for `useLegacyTable` on NEW code or in Angular. It's React-only, deprecated, bundles every - feature, and doesn't support `table.Subscribe`. It exists to unblock incremental migration, not to - be a long-term API. - - mistake: TS error "Property filterFns is missing" using useReactTable - mechanism: | - The `useReactTable` generic signature requires the filter functions registry to - be declared via `declare module` augmentation, otherwise `filterFns` appears - "required" in the props object. Without the augmentation in their project, - users with strict TS get red squigglies on a basic 3-prop call. - wrong_pattern: | - // ❌ TS error: Property 'filterFns' is missing in type ... - const table = useReactTable({ - data, columns, - getCoreRowModel: getCoreRowModel(), - }) - correct_pattern: | - // ✅ Either (a) cast the column types so the inferred constraint relaxes: - const columns: ColumnDef[] = [/* ... */] - // OR (b) augment the module to register custom filterFns: - declare module '@tanstack/react-table' { - interface FilterFns { myFn: FilterFn } - } - source: - https://github.com/TanStack/table/issues/5005 (Property filterFns is missing using useReactTable, - 14 comments) - priority: HIGH - status: active - version_context: v8 — long-standing top question; v9 cleans this up via tableFeatures registry - skills: - - migrate-v8-to-v9 - - setup - - getting-started - - mistake: Using useReactTable in v9 alpha when useTable is the new entry point - mechanism: | - v9 alpha renames `useReactTable` → `useTable` (and adds a `useLegacyTable` shim). - Agents trained on v8 will reach for `useReactTable`. The signature also changes — - `features` and `rowModels` are now mandatory configuration, not pre-bundled - defaults. `useReactTable` still exists for back-compat but produces v8-style - tables that don't have `.Subscribe`, `.atoms`, or feature composition. - wrong_pattern: | - // ❌ v8 entry point — won't have v9 Subscribe / atoms APIs - import { useReactTable, getCoreRowModel } from '@tanstack/react-table' - const table = useReactTable({ - data, columns, - getCoreRowModel: getCoreRowModel(), - }) - correct_pattern: | - // ✅ v9 entry — explicit features + row models - import { - useTable, tableFeatures, - sortingFeature, columnFilteringFeature, rowPaginationFeature, - } from '@tanstack/react-table' - const features = tableFeatures({ - sortingFeature, columnFilteringFeature, rowPaginationFeature, - }) - const table = useTable({ features, data, columns }) - source: 'PR #6202 (fix useLegacyTable infinite rerender), useTable.ts source, CHANGELOG history' - priority: CRITICAL - status: active - version_context: v9 alpha rename; v8 patterns dominate training data - - mistake: Importing pre-bundled getCoreRowModel etc. in v9 - mechanism: | - In v8, `getCoreRowModel()`, `getSortedRowModel()`, `getFilteredRowModel()` etc. - were standalone factories. In v9, row models are opt-in through `rowModels` on - the table options or auto-installed by features in `tableFeatures()`. Agents - will keep emitting v8 imports that either don't exist or aren't wired correctly. - wrong_pattern: | - // ❌ v8 pattern — won't drive v9 row models - const table = useTable({ - features, - data, columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - }) - correct_pattern: | - // ✅ v9 — row models opt-in via rowModels or feature inclusion - import { - useTable, tableFeatures, - sortingFeature, coreRowModelsFeature, sortedRowModelsFeature, - } from '@tanstack/react-table' - const features = tableFeatures({ - sortingFeature, - coreRowModelsFeature, - sortedRowModelsFeature, - }) - const table = useTable({ features, data, columns }) - source: 'PR #6234 (atoms refactor), v9 alpha package exports' - priority: CRITICAL - status: active - version_context: v9 alpha — fundamental restructure of the API surface - skills: - - migrate-v8-to-v9 - - customizing-feature-behavior - - mistake: data/columns are mutable in v8 but readonly in v9 - mechanism: | - PR #6183 makes `data` and `columns` `readonly` in v9 to force users to flow - changes through React state rather than mutating arrays in place. Code that - pushes to `data.push(...)` or splices columns will break at the TypeScript - level on v9, but agents will produce v8-style mutation patterns. - wrong_pattern: | - // ❌ v8 pattern, breaks at TS layer in v9 - const data = [] - function addRow(row) { data.push(row); rerender() } - correct_pattern: | - // ✅ Use React state / setState - const [data, setData] = useState([]) - function addRow(row) { setData(prev => [...prev, row]) } - source: 'PR #6183 (make data and columns readonly)' - priority: MEDIUM - status: active - version_context: v9 alpha - skills: - - migrate-v8-to-v9 - - setup - - mistake: Hallucinating react-table v7 / pre-v9 @tanstack/[framework]-table APIs - mechanism: - Every major release of TanStack Table (formerly react-table) has been a substantial upgrade. - Agents trained on older data confidently produce v7 or v8 API shapes that no longer exist in v9 - (e.g. `useReactTable`, inline `getCoreRowModel()` as an option, `useTable` from react-table v7). - wrong_pattern: |- - // ❌ v7 / v8 patterns the agent invents - import { useTable, useSortBy } from 'react-table' // v7 - const table = useTable({ columns, data }, useSortBy) + - 'Reading table atoms outside JSX, createMemo, createEffect, or another tracked scope and expecting later changes to rerun code.' + - 'Calling a state updater with a Solid setter assumption without resolving Table value-or-updater semantics.' + - 'Adding React-style broad rerender workarounds instead of relying on Solid narrow atom reads and computations.' - // or v8 - import { useReactTable, getCoreRowModel } from '@tanstack/react-table' - const table = useReactTable({ columns, data, getCoreRowModel: getCoreRowModel() }) - correct_pattern: |- - // ✅ v9 pattern — `features` + `rowModels` - import { useTable, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns } from '@tanstack/react-table' - const features = tableFeatures({ rowSortingFeature }) - const table = useTable({ - features, - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, data, - }) - priority: CRITICAL - version_context: - v7→v8 and v8→v9 both shifted the API substantially; agents trained on any pre-v9 - data will produce wrong shapes. - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - slug: client-to-server - type: lifecycle - domain: lifecycle - status: ready - description: | - Convert a client-side table to server-side (a.k.a. manual modes). Pattern: pass server data, set `manualSorting` / `manualFiltering` / `manualPagination` / `manualGrouping` / `manualExpanding` for whatever the server now owns, supply `rowCount` (and/or `pageCount`) so the table can paginate without seeing all rows, and remove the corresponding `rowModels` entries (you don't need `paginatedRowModel` if pagination is server-side). State for the server-managed slices is best owned EITHER via external TanStack Store atoms (passed via `options.atoms`) OR via classic `state` + `on*Change` controlled state — both work. With Query, the atom pattern lets you key your query on the atom and let the table write directly to it without `on*Change` plumbing. - covers: - - Setting `manualSorting`, `manualFiltering`, `manualPagination`, `manualGrouping`, `manualExpanding` - so the table stops trying to slice rows it doesn't have. - - Removing the matching `rowModels` keys so unused factories don't ship (don't register `paginatedRowModel` - if the server paginates). - - Providing `rowCount` (and optionally `pageCount`) so `table.getPageCount()` / `getCanNextPage()` work - without local rows. - - 'State ownership: external atoms via `useCreateAtom` + `options.atoms` (cleanest with Query, no `on*Change` - needed) OR classic `state` + `on*Change` controlled state. Both are documented patterns.' - - 'Cache key strategy: include the relevant atom value (or controlled state) in `queryKey`. Use `placeholderData: - keepPreviousData` to avoid the "0 rows flash" while paging.' - - "Invalidation: call `queryClient.invalidateQueries({ queryKey: [...] })` on writes — TanStack Table - doesn't invalidate for you." - packages: - - '@tanstack/react-table' - - '@tanstack/vue-table' - - '@tanstack/angular-table' - - '@tanstack/solid-table' - - '@tanstack/svelte-table' - - '@tanstack/preact-table' - tasks: - - Convert a sorting+pagination table that operates on a 50k local array to one that requests `/api/people?pageIndex=&pageSize=&sort=` - and renders the response. - - Wire pagination via an external TanStack Store atom (`useCreateAtom(...)`) + the - `atoms` option so the query auto-refetches without an explicit `onPaginationChange` handler. - - Wire the same conversion via classic `state` + `onPaginationChange` controlled state when you already - have signal/useState plumbing. - - Combine server-side sort + pagination + filter while keeping client-side column visibility / ordering - / pinning — those features still work locally because they don't depend on the row model. - correct_pattern: | - // React + TanStack Query + external pagination atom - import { useTable, tableFeatures, rowPaginationFeature, createColumnHelper } - from '@tanstack/react-table' - import { useQuery, keepPreviousData } from '@tanstack/react-query' - import { useCreateAtom, useSelector } from '@tanstack/react-store' - import type { PaginationState } from '@tanstack/react-table' - - const features = tableFeatures({ rowPaginationFeature }) - - function ServerTable({ columns }) { - // 1) Own pagination in an external atom. - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - const pagination = useSelector(paginationAtom) - - // 2) Key the query on the atom value. Query refetches automatically - // when the table calls table.setPageIndex(...) / setPageSize(...) - // because those writes flow into the atom. - const dataQuery = useQuery({ - queryKey: ['people', pagination], - queryFn: () => fetchPeople(pagination), - placeholderData: keepPreviousData, - }) - - // 3) Manual pagination + `rowCount` for getPageCount(). Note: no - // `paginatedRowModel` in `rowModels` — server paginates. - const table = useTable({ - features, - rowModels: {}, // core only - columns, - data: dataQuery.data?.rows ?? [], - rowCount: dataQuery.data?.rowCount, - atoms: { pagination: paginationAtom }, // own the slice externally - manualPagination: true, - }) - // No onPaginationChange needed — writes go to paginationAtom. - // ... - } - wrong_pattern: | - // BAD #1: kept `paginatedRowModel` even though the server paginates. - // This makes the table try to slice the (already-sliced) `data` array - // a second time, producing rows that don't exist. - const table = useTable({ - features, - rowModels: { paginatedRowModel: createPaginatedRowModel() }, - columns, - data: serverPage.rows, - // missing: manualPagination + rowCount - }) - - // BAD #2: forgot `rowCount`. `table.getPageCount()` returns - // `Math.ceil(rows.length / pageSize)` which is the page size, not the - // total — the pager shows "Page 1 of 1" forever. - const table = useTable({ - features, - rowModels: {}, - columns, - data: serverPage.rows, - manualPagination: true, - // missing: rowCount: serverPage.totalRowCount - }) - - // BAD #3: keyed Query on the wrong thing. `queryKey: ['people']` never - // changes, so pagination button clicks update the atom but Query - // doesn't refetch — the table stays on page 1's data. - useQuery({ queryKey: ['people'], queryFn: () => fetch(pagination) }) - - // BAD #4: classic controlled state but the table writes go nowhere - // because there's no `on*Change`. Either pass `atoms` OR pass - // `state + on*Change`. Don't pass `state` without `on*Change`. - const [pagination, setPagination] = useState({...}) - useTable({ - features, rowModels: {}, columns, data, - state: { pagination }, - // missing: onPaginationChange: setPagination - manualPagination: true, - }) + - slug: migrate-v8-to-v9 + package: '@tanstack/solid-table' + domain: framework-adapters + type: migration + framework: solid + purpose: 'Migrate createSolidTable code to v9 createTable, explicit features, atom-backed state, and new helpers.' + sources: + [ + 'TanStack/table:docs/framework/solid/guide/migrating.md', + 'TanStack/table:packages/solid-table/src/index.tsx', + 'TanStack/table:examples/solid/basic-use-table', + ] failure_modes: - - 'Forgetting `manualPagination: true` (or `manualSorting` / `manualFiltering`). The table will re-sort/re-filter/re-paginate - the already-server-processed rows, producing visually incorrect or duplicated results.' - - 'Leaving `paginatedRowModel: createPaginatedRowModel()` registered when the server paginates. The - factory ships in your bundle for no reason AND the table slices server-sliced data. Drop it from `rowModels`.' - - Omitting `rowCount`. Without it, `getPageCount()` is computed from `data.length / pageSize` — which - equals 1 if the server returned a single page. Pager UIs lock at 'Page 1 of 1'. - - Passing `state.pagination` without `onPaginationChange`. The library treats `state` as controlled; - without a writeback handler, `table.setPageIndex(2)` is a no-op. Either pass both, or pass `atoms.pagination` - (which is its own writeback). - - Mixing `state.pagination` AND `atoms.pagination` for the SAME slice. Precedence is `options.atoms[key]` - > `options.state[key]` > internal — `state` is silently ignored. Pick one mechanism per slice. - - Not including pagination/sort/filter in the Query `queryKey`. The query won't refetch when the user - pages or sorts because Query has no way to know its inputs changed. - - 'Forgetting `placeholderData: keepPreviousData` (or equivalent) when paging. The table renders zero - rows during the fetch, which collapses the height and jumps the scroll position.' - - slug: production-readiness - type: lifecycle - domain: lifecycle - status: ready - description: | - Ship-ready optimizations for v9: tree-shake the bundle by registering ONLY the `features` you actually use; memoize `features`, `data`, and `columns` for stable identity; replace `(state) => state` with narrow selectors or per-slice `useSelector(table.atoms.)` subscriptions; and push state-driven re-renders down the tree with `` (or the standalone `` import) so the expensive table body doesn't re-render every time you toggle a sort indicator. Production builds of the table devtools are also a `/production` import flip — see `compose-with-tanstack-devtools`. - covers: - - "Tree-shaking: list features explicitly in `tableFeatures({...})` rather than `stockFeatures`. A sort-only - table is ~6–7kb vs ~15–20kb for v8's all-in approach." - - 'Reference stability: `features`, `rowModels`, `columns`, and `data` should NOT be redefined on - every render. Declare at module scope or memoize with `useMemo` / Angular `signal()` / Solid memo.' - - "Default selector trade-off: `(state) => state` matches v8 behavior but forces a re-render on any - state change. Narrow it (`(state) => ({ sorting: state.sorting })`) once you've profiled." - - '`` (or ``) for moving the re-render - boundary down to a sub-tree.' - - 'Per-slice atom subscriptions: `useSelector(table.atoms.pagination)` in a leaf component re-renders - only when pagination changes — narrower than `table.Subscribe`, which still selects from the full - state object.' - - "React Compiler interaction: `` is the React-Compiler-safe way to read table state in nested - components — the compiler can't see through the table closure on its own." - - 'Angular: prefer `computed(() => table.atoms..get(), { equal: shallow })` to debounce object-identity - re-runs.' - - "Stress patterns: virtualize rows/columns with TanStack Virtual (see `compose-with-tanstack-virtual`), - keep the row virtualizer in the deepest possible component, and use `Subscribe` walls around column - DnD / pinning UIs that don't need to participate in row updates." - packages: - - '@tanstack/react-table' - - '@tanstack/vue-table' - - '@tanstack/angular-table' - - '@tanstack/solid-table' - - '@tanstack/svelte-table' - - '@tanstack/preact-table' - tasks: - - Audit a project's `features` declarations and remove any feature whose imports aren't actually wired - up in column defs / state. - - Replace `useTable(opts, (state) => state)` with a narrowed selector when only a subset of state is - rendered at the table level. - - 'Wrap a noisy pagination footer (re-renders on every keystroke in a filter) in ` ({ pagination: s.pagination })}>` so it only re-renders on pagination changes.' - - Move a `RowSelectionCount` component from reading `table.state.rowSelection` to `useSelector(table.atoms.rowSelection)` - for the narrowest subscription surface. - correct_pattern: | - // === Tree-shake by feature === - import { - useTable, - tableFeatures, - rowSortingFeature, // ✓ ship sort code - rowPaginationFeature, // ✓ ship pagination code - createSortedRowModel, - createPaginatedRowModel, - sortFns, - } from '@tanstack/react-table' - - // Module-scope, stable identity: - const features = tableFeatures({ rowSortingFeature, rowPaginationFeature }) - const rowModels = { - sortedRowModel: createSortedRowModel(sortFns), - paginatedRowModel: createPaginatedRowModel(), - } - - function MyTable({ data, columns }) { - // ✓ narrow selector — re-renders only when these slices change - const table = useTable( - { features, rowModels, columns, data }, - (state) => ({ - sorting: state.sorting, - pagination: state.pagination, - }), - ) - - return ( - <> - {/* heavy — keep stable */} - {/* ✓ Re-render wall: footer only re-renders on pagination changes, - not on sort changes that affect TableBody */} - ({ pagination: s.pagination })}> - {({ pagination }) => } - - - ) - } - - // === Narrowest subscription — per-slice atom === - import { useSelector } from '@tanstack/react-store' + - 'Renaming the entrypoint but retaining v8 row-model options outside tableFeatures.' + - 'Forcing React selector patterns into Solid instead of using signal-tracked table atom reads.' + - 'Missing shared v9 changes to instance method binding, feature APIs, meta helpers, sorting, and pinning.' - function SelectedCount({ table }) { - // re-renders ONLY when rowSelection changes, not sorting/pagination/etc. - const selection = useSelector(table.atoms.rowSelection) - return {Object.keys(selection).length} selected - } - wrong_pattern: | - // BAD #1: stockFeatures in a sort-only table. - // Ships filtering, faceting, grouping, pinning, expanding, sizing, - // resizing, visibility, ordering, row-selection, row-pinning code - // none of which is used. ~3x the necessary bundle for `features`. - import { useTable, stockFeatures } from '@tanstack/react-table' - const table = useTable({ features: stockFeatures, /* ... */ }) - - // BAD #2: unstable references — every render is a new identity. - function MyTable({ data, columns }) { - const features = tableFeatures({ rowSortingFeature }) // new every render - const opts = { // new every render - features, - rowModels: { sortedRowModel: createSortedRowModel(sortFns) }, - columns, // hopefully stable - data, - } - const table = useTable(opts) - // ... - } - - // BAD #3: default selector + deep re-render tree. - function MyTable() { - // (state) => state means every keystroke in a filter input re-renders - // every cell in the body unless you memo manually. - const table = useTable(opts) // implicit (state) => state - return - } - - // BAD #4: reading state in a way React Compiler can't track. - function PageFooter({ table }) { - // The compiler can't reason through the table closure — it may either - // bail out of optimization OR cache a stale read. - const { pagination } = table.state - return
{pagination.pageIndex + 1}
- // ✓ Fix: s.pagination}> - // or useSelector(table.atoms.pagination). - } + - slug: create-table-hook + package: '@tanstack/solid-table' + domain: framework-adapters + type: framework + framework: solid + purpose: 'Create a shared Solid createAppTable and typed helper/component registry without losing reactivity.' + sources: + [ + 'TanStack/table:docs/framework/solid/guide/composable-tables.md', + 'TanStack/table:examples/solid/composable-tables', + 'TanStack/table:packages/solid-table/src/createTableHook.tsx', + ] failure_modes: - - Using `stockFeatures` in production without auditing which features are actually rendered. Defeats - the whole reason tree-shaking exists in v9. Replace with an explicit `tableFeatures({...})` listing - only what your UI uses. - - Declaring `features`, `rowModels`, or `columns` inside the component body without `useMemo`. Internal - memoization keys off identity, so a new object every render forces full recomputation. - - Putting `data` in `useState` and never memoizing the array literal that produces it. `setData(rows.filter(...))` - returns a new array each call — that's fine; the FAQ pitfall is `data={[]}` or `data={rows ?? []}` - in JSX, which creates a new identity each render. - - Leaving `(state) => state` as the selector when only one component cares about one slice. The whole - tree re-renders on every state change. Narrow the selector and/or use `` boundaries. - - Forgetting that `` still selects from `table.store.state` (the full state). For a - single-slice subscription, `useSelector(table.atoms.)` is narrower and skips even constructing - a state snapshot. - - Hoisting heavy table state reads above virtualizers. The TanStack Virtual rule of thumb is to keep - `useVirtualizer` in the deepest component possible; pulling `table.state` into a parent re-renders - the virtualizer and kills scroll perf. - - Reading `table.state` in deeply-nested React-Compiler-compiled components instead of `` - or `useSelector`. The compiler doesn't see through the table closure, so it can't determine the dependency - edges correctly. - - mistake: Next.js warns about Date.now() in client component - mechanism: | - `table-core` uses `Date.now()` inside `getMemoOptions` for cache freshness tracking. - Next.js (v16+) flags this in client components as a hydration risk because server - and client times differ. The warning is inert at runtime but noisy. Workaround is - pending a switch to `performance.now()`. - wrong_pattern: | - // ❌ Next.js dev warnings on basic v8 setup - 'use client' - const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }) - correct_pattern: | - // ✅ No code-level fix yet — track issue #6127 / wait for upstream patch. - // Workaround: silence the rule for the table file or move table construction - // into a custom hook to localize. - source: - 'https://github.com/TanStack/table/issues/6127 (DX: Next.JS complain about using Date.now() - inside a Client Component)' - priority: LOW - status: active - version_context: v8.21.3 + Next 16 - skills: - - production-readiness - - setup - - mistake: Bundling stockFeatures or all features when only a few are used - mechanism: - TanStack Table v9 is tree-shakeable specifically so you only pay for features you use. - Registering `stockFeatures` (or every feature "just in case") forfeits the bundle benefit that motivated - the v9 redesign. - wrong_pattern: |- - // ❌ Pulls in every feature even though only sorting+pagination are used - import { stockFeatures, tableFeatures } from '@tanstack/react-table' - const features = tableFeatures(stockFeatures) - correct_pattern: |- - // ✅ Register only what this table uses - import { tableFeatures, rowSortingFeature, rowPaginationFeature } from '@tanstack/react-table' - const features = tableFeatures({ rowSortingFeature, rowPaginationFeature }) - priority: HIGH - version_context: v9. Tree-shaking via `features` is one of the headline reasons for the rewrite. - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - mistake: Premature Subscribe / custom selector optimization on small tables - mechanism: - Subscribe and narrow selectors are designed for large or expensive tables where full re-renders - measurably hurt. On small tables (a few hundred rows, simple features), default selector + inline - rendering is fine and the Subscribe boundaries add complexity without measurable benefit. - wrong_pattern: |- - // ❌ Wraps every header and cell in Subscribe on a 50-row table - header: ({ table }) => ( - s.sorting}> - {() => } - - ) - correct_pattern: |- - // ✅ Default useTable selector, inline rendering — Subscribe only when measured perf demands it - const table = useTable({ features, rowModels, columns, data }) - // reach for Subscribe later, scoped to the actual hotspot - priority: MEDIUM - version_context: 'Maintainer guidance: advanced state-management patterns are for advanced cases.' - source: maintainer interview (Phase 4, 2026-05-17) - status: active - - slug: compose-with-tanstack-store - type: composition - domain: composition - status: ready - description: | - v9 is built on TanStack Store. Each state slice (sorting, pagination, rowSelection, columnFilters, ...) is a separate atom. The table exposes three read surfaces — `table.atoms.` (per-slice readonly), `table.store` (flat readonly view), and `table.state` (selector output from `useTable`) — and two write paths — internal `table.baseAtoms.` OR YOUR `options.atoms[slice]` if you opt to own the slice. The `atoms` option lets a slice be shared across components, persisted, or wired to a non-table subscriber. Use `useCreateAtom` to create the atom in React; `useSelector` to read it in any component without going through the table; the table writes to your atom directly when you call `table.setSorting(...)` etc. - covers: - - "The three read surfaces and what they're for: `table.atoms.` (narrowest), `table.store.state` - (full snapshot), `table.state` (typed selector output)." - - 'Precedence: `options.atoms[key]` > `options.state[key]` > internal `baseAtoms[key]`. Mixing `state` - + `atoms` for the same slice — `atoms` wins and `state` is ignored.' - - 'When external atoms beat `state` + `on*Change`: cross-component subscriptions, persistence (writing - the atom to localStorage from outside the table), atom-based integrations, sharing one slice across - multiple tables.' - - '`useCreateAtom(initial)` for stable atom identity inside a React component (replacement - for the manual `useRef(createAtom(...))` pattern).' - - '`useSelector(table.atoms.)` / `useSelector(myAtom)` for fine-grained reactivity.' - - '`table.reset()` does NOT clear external atoms — you own them, so you reset them yourself with `myAtom.set(default)`.' - - 'Angular: atoms are signal-backed under the hood. Read them through `computed(() => table.atoms..get(), - { equal: shallow })`.' - packages: - - '@tanstack/react-table' - - '@tanstack/vue-table' - - '@tanstack/angular-table' - - '@tanstack/solid-table' - - '@tanstack/svelte-table' - - '@tanstack/preact-table' - - '@tanstack/store' - - '@tanstack/react-store' - tasks: - - Move a row-selection slice to an external `useCreateAtom({})` and pass it through - `options.atoms.rowSelection` so a sibling sidebar can `useSelector` the same atom. - - Replace classic `state.sorting` + `onSortingChange` controlled state with an external `sortingAtom` - for atom-based ergonomics — the table writes to the atom directly, no `on*Change` plumbing. - - Persist a column-visibility slice by subscribing the atom to localStorage outside the table render - path. - - Read `table.atoms.pagination` in a deeply-nested footer with `useSelector` so the rest of the table - doesn't re-render on page changes. - correct_pattern: | - // External atoms for sorting + pagination — owned by you, shared anywhere. - import { useCreateAtom, useSelector } from '@tanstack/react-store' - import { - useTable, tableFeatures, rowSortingFeature, rowPaginationFeature, - createSortedRowModel, createPaginatedRowModel, sortFns, - } from '@tanstack/react-table' - import type { PaginationState, SortingState } from '@tanstack/react-table' - - const features = tableFeatures({ rowSortingFeature, rowPaginationFeature }) - - function MyTable({ columns, data }) { - const sortingAtom = useCreateAtom([]) - const paginationAtom = useCreateAtom({ - pageIndex: 0, pageSize: 10, - }) - - // Fine-grained reads — these components re-render independently. - const sorting = useSelector(sortingAtom) - const pagination = useSelector(paginationAtom) - - const table = useTable({ - features, - rowModels: { - sortedRowModel: createSortedRowModel(sortFns), - paginatedRowModel: createPaginatedRowModel(), - }, - columns, - data, - atoms: { - sorting: sortingAtom, - pagination: paginationAtom, - }, - // NOTE: no onSortingChange / onPaginationChange needed — - // table.setSorting / table.setPageIndex write through to the atoms. - }) + - 'Using createTableHook for a one-off table instead of standalone createTable.' + - 'Passing snapshots where createAppTable needs reactive getters or signals for per-table data and state.' + - 'Reading context values outside registered App wrappers or prop-drilling the table when returned context hooks express the shared boundary.' - // Reset is your job for owned atoms: - const resetMyState = () => { - sortingAtom.set([]) - paginationAtom.set({ pageIndex: 0, pageSize: 10 }) - } - } - wrong_pattern: | - // BAD #1: passing the SAME slice via both `state` and `atoms`. Atoms - // win silently and the `state` value is ignored — confusing to debug. - useTable({ - features, rowModels: {}, - columns, data, - state: { sorting: localSorting }, // ignored - onSortingChange: setLocalSorting, // ignored - atoms: { sorting: sortingAtom }, // wins - }) - - // BAD #2: creating the atom inside render without `useCreateAtom`. - // A new atom every render → the table re-binds every render → state - // resets to initial. (The same trap as `useState(createAtom(...))`.) - function MyTable() { - const sortingAtom = createAtom([]) // wrong: unstable - useTable({ features, rowModels: {}, columns, data, - atoms: { sorting: sortingAtom } }) - } - - // BAD #3: expecting `table.reset()` to clear an external atom. It - // doesn't — you own the atom. Call sortingAtom.set([]) yourself. - - // ✓ Fix: - // ✓ Fix: `, -}) -export class CellActionsComponent { - readonly cell = injectTableCellContext() -} -``` - -Each directive uses Angular's `providers` array to register a per-host factory, -so contexts are correctly scoped — multiple `[tanStackTableCell]` directives on -sibling cells provide independent values. - ---- - -## `*flexRender` directly (custom props) - -Reach for the long form `*flexRender` when: - -- You're rendering something that isn't `columnDef.cell` / `header` / `footer` - (e.g. a registered `headerComponents` member, see `createTableHook`). -- You want to override the props passed to the render function. - -```html - - {{ rendered }} - -``` - -Custom props (e.g. extending the default context): - -```html - - {{ rendered }} - -``` - -Inside the rendered component, `injectFlexRenderContext()` returns this full -props object. - -You can also pass `flexRenderInjector:` to override the injector used for -`createComponent`. - ---- - -## Render-function injection context - -Because column-def `cell` / `header` / `footer` functions run inside -`runInInjectionContext`, this **works** at the top of those functions: - -```ts -cell: ({ getValue }) => { - const router = inject(Router) // ✅ legal - return router.url.endsWith('/admin') ? `[admin] ${getValue()}` : getValue() -} -``` - -Be deliberate: this runs every time the cell renders. For per-app values that -don't change, prefer `inject(...)` at the component level and close over. diff --git a/packages/angular-table/skills/angular/angular-rendering-directives/references/flex-render-component-options.md b/packages/angular-table/skills/angular/angular-rendering-directives/references/flex-render-component-options.md deleted file mode 100644 index fee80afb37..0000000000 --- a/packages/angular-table/skills/angular/angular-rendering-directives/references/flex-render-component-options.md +++ /dev/null @@ -1,64 +0,0 @@ -# `flexRenderComponent` — Full Options Reference - -When you need custom inputs not derived from the render context, output -callbacks, a custom injector, or Angular v20+ `bindings` / `directives`, -**wrap the component**: - -```ts -import { flexRenderComponent, type ColumnDef } from '@tanstack/angular-table' -import { EditableCell } from './editable-cell' - -const columns: Array> = [ - { - accessorKey: 'firstName', - cell: ({ getValue, row, column, table }) => - flexRenderComponent(EditableCell, { - inputs: { - value: getValue(), - }, - outputs: { - change: (value) => { - table.options.meta?.updateData(row.index, column.id, value) - }, - }, - }), - }, -] -``` - -## How inputs/outputs are wired - -- **`inputs`** → applied via `ComponentRef.setInput(key, value)` (works with - both `input()` signals and legacy `@Input()`). Diffed per change-detection - cycle with `KeyValueDiffers` — unchanged values are _not_ re-set, so object - inputs are reference-checked. Keep input objects referentially stable when - you can. -- **`outputs`** → resolved at component-instance level. The wrapper reads the - property by name, checks it is an `OutputEmitterRef`, and subscribes. The - subscription is cleaned up when the component is destroyed. -- **`injector`** → use when the rendered component needs to inject from a - specific scope (e.g. a feature module / sub-injector). -- **`bindings`** (Angular v20+) → forwarded directly to - `ViewContainerRef.createComponent` at creation time. Use this with - `inputBinding`, `outputBinding`, `twoWayBinding` for native programmatic - rendering semantics. -- **`directives`** (Angular v20+) → host directives forwarded the same way. - -```ts -import { inputBinding, outputBinding, twoWayBinding, signal } from '@angular/core' - -readonly name = signal('Ada') - -cell: () => - flexRenderComponent(EditableNameCell, { - bindings: [ - inputBinding('value', this.name), - twoWayBinding('value', this.name), - outputBinding('valueChange', (v) => console.log('changed', v)), - ], - }) -``` - -> **Do not mix `bindings` with `inputs`/`outputs`** on the same component. -> `bindings` apply at creation time and participate in the initial CD cycle; -> `inputs`/`outputs` apply after. Mixing them risks double-initialization. diff --git a/packages/angular-table/skills/angular/client-to-server/SKILL.md b/packages/angular-table/skills/angular/client-to-server/SKILL.md deleted file mode 100644 index 3f5f18ce52..0000000000 --- a/packages/angular-table/skills/angular/client-to-server/SKILL.md +++ /dev/null @@ -1,468 +0,0 @@ ---- -name: angular/client-to-server -description: > - Convert an Angular Table v9 from client-side to server-side processing. Flip - `manualPagination` / `manualSorting` / `manualFiltering` / `manualGrouping` / `manualExpanding` - for the slices the server now owns; drop the corresponding row-model factory slots from the - `features` object for the factories the server replaces; supply `rowCount` (server total) so - pagination computes correctly; hoist `pagination` / `sorting` / `columnFilters` / `globalFilter` - to Angular signals with `state` + `on[State]Change`; fetch via `rxResource` / `httpResource` / - `@tanstack/angular-query`; preserve previous data on refetch with `linkedSignal` (or - `placeholderData: keepPreviousData` for Query); set `getRowId` for stable selection across - refetches. -type: lifecycle -library: tanstack-table -framework: angular -library_version: '9.0.0-alpha.48' -requires: - - angular/table-state - - angular/getting-started - - filtering - - sorting - - pagination -sources: - - TanStack/table:docs/framework/angular/guide/table-state.md - - TanStack/table:docs/framework/angular/guide/migrating.md - - TanStack/table:examples/angular/remote-data/ - - TanStack/table:packages/angular-table/src/injectTable.ts ---- - -# Client → Server Conversion (Angular Table v9) - -> Goal: take a working client-side Angular Table v9 and migrate it to server-driven -> processing for one or more of pagination / sorting / filtering / grouping / -> expanding — without rewriting your row markup, columns, or feature surface. -> -> The canonical Angular example is `examples/angular/remote-data/`, using -> `rxResource` + `linkedSignal`. The same pattern composes with -> `@tanstack/angular-query` (see `compose-with-tanstack-query`). - ---- - -## 1. The 5-step recipe - -For each slice the server now owns: - -1. **Flip `manualX: true`** in table options. This tells the table "don't - process this on the client — trust the data you receive." -2. **Drop the matching client-side row-model factory slot** from `features` - (or keep it if you still want the feature's _state_ but no client - recomputation — see §3). -3. **Hoist the slice to an Angular signal**, control it via `state.x` + - `on[State]Change`. Server requests must depend on the signal. -4. **Pass `rowCount`** (the server's total) so `getPageCount()`, - `getCanNextPage()`, etc. compute correctly under `manualPagination`. -5. **Set `getRowId`** so row selection (and refetch identity) survives across - server refetches. - -Plus: keep previous data visible during refetches (avoid a "0-rows flash") with -`linkedSignal`, `placeholderData: keepPreviousData` (Query), or -`previousValue:` (`httpResource`). - ---- - -## 2. The `manualX` matrix - -| Slice | Option | Client row-model needed? | Notes | -| -------------- | ------------------------ | ------------------------ | -------------------------------------------------------------- | -| Pagination | `manualPagination: true` | drop `paginatedRowModel` | also pass `rowCount: ` | -| Sorting | `manualSorting: true` | drop `sortedRowModel` | feature still controls `sorting` state | -| Column filters | `manualFiltering: true` | drop `filteredRowModel` | also affects global filter when sharing the filtered row model | -| Global filter | `manualFiltering: true` | drop `filteredRowModel` | global filter shares the filtered row model | -| Grouping | `manualGrouping: true` | drop `groupedRowModel` | rare — most servers don't return grouped trees | -| Expanding | `manualExpanding: true` | drop `expandedRowModel` | server returns sub-rows pre-expanded | - -Selection, visibility, ordering, pinning, sizing, resizing, row-pinning are -all UI-only state — they don't have manual modes. They keep working unchanged. - ---- - -## 3. Canonical example — pagination + sorting + global filter - -The `examples/angular/remote-data/` pattern, condensed: - -```ts -import { - ChangeDetectionStrategy, - Component, - inject, - linkedSignal, - signal, -} from '@angular/core' -import { rxResource } from '@angular/core/rxjs-interop' -import { HttpClient, HttpParams } from '@angular/common/http' -import { map } from 'rxjs' -import { - FlexRender, - injectTable, - tableFeatures, - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - globalFilteringFeature, - createColumnHelper, - type ColumnDef, - type PaginationState, - type SortingState, -} from '@tanstack/angular-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - globalFilteringFeature, -}) - -const columnHelper = createColumnHelper() - -type TodoResponse = { items: Array; totalCount: number } - -@Component({ - selector: 'app-root', - imports: [FlexRender], - templateUrl: './app.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class App { - private readonly http = inject(HttpClient) - - // 1. Hoist controlled slices to signals - readonly pagination = signal({ pageIndex: 0, pageSize: 10 }) - readonly sorting = signal([{ id: 'id', desc: false }]) - readonly globalFilter = signal(null) - - // 2. Fetch — server query depends on those signals - private readonly data = rxResource({ - params: () => ({ - page: this.pagination(), - sorting: this.sorting(), - globalFilter: this.globalFilter(), - }), - stream: ({ params: { page, sorting, globalFilter } }) => { - let params = new HttpParams({ - fromObject: { - _page: page.pageIndex + 1, - _limit: page.pageSize, - }, - }) - if (globalFilter) params = params.set('title_like', globalFilter) - if (sorting.length) { - params = params - .set('_sort', sorting.map((s) => s.id).join(',')) - .set( - '_order', - sorting.map((s) => (s.desc ? 'desc' : 'asc')).join(','), - ) - } - return this.http - .get>('https://jsonplaceholder.typicode.com/todos', { - params, - observe: 'response', - }) - .pipe( - map( - (res) => - ({ - items: res.body ?? [], - totalCount: Number(res.headers.get('X-Total-Count')), - }) satisfies TodoResponse, - ), - ) - }, - }) - - // 3. Keep previous page visible during refetch — no "0 rows" flash - readonly dataWithLatest = linkedSignal< - { - value: TodoResponse | undefined - status: 'idle' | 'loading' | 'resolved' | 'error' - }, - TodoResponse - >({ - source: () => ({ - value: this.data.value(), - status: this.data.status(), - }), - computation: (source, previous) => { - if (previous && source.status === 'loading') return previous.value - return source.value ?? { items: [], totalCount: 0 } - }, - }) - - readonly columns: Array> = [ - columnHelper.accessor('id', { header: 'Id', cell: (i) => i.getValue() }), - columnHelper.accessor('title', { - header: 'Title', - cell: (i) => i.getValue(), - }), - columnHelper.accessor('completed', { - header: 'Completed', - cell: (i) => (i.getValue() ? '✅' : '❌'), - }), - ] - - // 4. Wire the table — manualX flags, features without row-model factories, supply rowCount - readonly table = injectTable(() => { - const data = this.dataWithLatest() - return { - features, // ← features has no paginatedRowModel/sortedRowModel/filteredRowModel slots - columns: this.columns, - data: data.items, - getRowId: (row) => String(row.id), - - // Controlled slices - state: { - pagination: this.pagination(), - sorting: this.sorting(), - globalFilter: this.globalFilter(), - }, - - // Manual modes - manualPagination: true, - manualSorting: true, - manualFiltering: true, - - // Server's truth about total row count - rowCount: data.totalCount, - - onPaginationChange: (u) => - typeof u === 'function' - ? this.pagination.update(u) - : this.pagination.set(u), - - onSortingChange: (u) => - typeof u === 'function' ? this.sorting.update(u) : this.sorting.set(u), - - // When filter changes, also reset page index - onGlobalFilterChange: (u) => { - typeof u === 'function' - ? this.globalFilter.update(u) - : this.globalFilter.set(u) - this.pagination.update((p) => ({ ...p, pageIndex: 0 })) - }, - } - }) -} -``` - -### What changed from the client-side version - -- `features` has no `paginatedRowModel`, `sortedRowModel`, or `filteredRowModel` - slots — the server is the source of truth for those. -- `manualPagination` / `manualSorting` / `manualFiltering: true`. -- `rowCount: data.totalCount` — required for correct `getPageCount()` and the - next/prev buttons. -- `state` + per-slice `on[State]Change` for everything the server reads. -- `getRowId` set so row selection survives refetch reorderings. -- `linkedSignal` keeps the previous response visible during loading — without - it, paginating yields a one-frame "no rows" flash because `data.value()` is - `undefined` mid-fetch. -- Resetting `pageIndex` on global-filter change is a UX rule, not framework - behavior — make it explicit. - ---- - -## 4. Wiring with `@tanstack/angular-query-experimental` - -For Query users, the equivalent of `linkedSignal` is -`placeholderData: keepPreviousData`. See `compose-with-tanstack-query` for the -full pattern. The table-side wiring (manual flags, dropped row models, -controlled signals, `rowCount`, `getRowId`) is identical. - ---- - -## 5. `rowCount` and friends - -Under `manualPagination: true`, the table no longer knows the total. You must -tell it: - -```ts -rowCount: data.totalCount // total rows server reports -// pageCount: 42 // can be passed instead, if your API gives pages not rows -``` - -If you omit both, `getPageCount()` returns `-1` and the "next page" button -never disables. If your API reports `pageCount` directly (rare), prefer -`pageCount` — otherwise compute it from `rowCount`. - ---- - -## 6. Always set `getRowId` when server-driven - -Without `getRowId`, row IDs default to row index. That works on the client -because order is stable per render. On the server, a refetch may return rows in -a different order — `RowSelectionState`, keyed by row ID, then targets the -wrong rows. - -```ts -getRowId: (row) => row.id -``` - -Required for: - -- `rowSelectionFeature` correctness across refetches -- pinned-row identity -- stable `track row.id` performance in `@for` - ---- - -## 7. Debouncing rapid input — global filter typing - -Naively, every keystroke triggers a server fetch. Two options: - -- **Manual signal indirection** — keep a `globalFilterInput` signal that the - UI writes to, then update `globalFilter` after a delay via `effect(...) + -setTimeout` or RxJS `debounceTime`. -- **Compose with `@tanstack/angular-pacer`** — see - `compose-with-tanstack-pacer` (not in this batch but on the roadmap). - -Resetting `pageIndex` to 0 when filter or sort changes is a UX standard: - -```ts -onGlobalFilterChange: (u) => { - typeof u === 'function' ? this.globalFilter.update(u) : this.globalFilter.set(u) - this.pagination.update((p) => ({ ...p, pageIndex: 0 })) -}, -``` - ---- - -## 8. Mixed mode — some slices server, others client - -Common pattern: pagination + sorting on the server, but row selection + -column visibility stay client-only. **Nothing special required** — only the -slices you mark `manualX` are server-driven. Selection / visibility / ordering -work unchanged. - -You can also keep client-side filtering on a column while paginating on the -server, but be wary: if rows are paginated server-side, you only have the -current page to filter against. Usually it's cleaner to flip all data-shape -slices to the server consistently. - ---- - -## 9. Resetting state on slice changes - -These behaviors are intentional and you'll often want to _override_ them when -server-driven: - -| Default | When server-driven | -| ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| `autoResetPageIndex: true` resets `pageIndex` to 0 when data identity changes | OK as-is — every fetch is a new array reference, so the page index resets unless you also pass `autoResetPageIndex: false` | -| Filter change does **not** auto-reset page | UX-standard to reset manually (see §7) | -| Sort change does **not** auto-reset page | Reset manually if your UX expects "new sort → page 1" | - -If your fetch always returns a fresh array, **set `autoResetPageIndex: false`** — -otherwise paginating to page 3 will reset back to page 0 the moment the new -data lands. The remote-data example demonstrates the alternative pattern for -edits (toggle the flag around the update). - ---- - -## Failure modes - -### 1. (CRITICAL) Flipping `manualPagination: true` but keeping `paginatedRowModel` on `features` - -The client row-model factory will re-paginate the (already-paginated) data, -chopping the visible rows down to the first `pageSize` of the page slice. -**Remove the `paginatedRowModel` slot from `features`** when going manual — or -accept double-pagination. - -### 2. (CRITICAL) Forgetting `rowCount` under `manualPagination` - -`getPageCount()` returns `-1`, `getCanNextPage()` is `true` forever, -"page N of -1" appears in the UI. Always pass either `rowCount: serverTotal` -or `pageCount: serverPageCount`. - -### 3. (CRITICAL) Missing `getRowId` with selection + server refetches - -The row-selection state is keyed by row ID. With index-as-ID, refetches that -return rows in any new order (sort flip, page change) reselect the wrong -rows. Always set `getRowId: row => row.id` (or whatever your primary key is). - -### 4. (HIGH) "0 rows" flash between pages - -If your fetch resolves to `undefined` during loading, `data.items` becomes `[]` -mid-fetch — the table renders empty for a frame. Use `linkedSignal` (or -`@tanstack/query`'s `placeholderData: keepPreviousData`, or -`httpResource`'s previous-value semantics) to keep the previous page visible. - -### 5. (HIGH) Forgetting to handle both value AND updater-fn shapes in `on[State]Change` - -```ts -// ❌ Crashes when the table passes an updater function -onPaginationChange: (value) => this.pagination.set(value) - -// ✅ -onPaginationChange: (u) => - typeof u === 'function' ? this.pagination.update(u) : this.pagination.set(u) -``` - -### 6. (HIGH) `autoResetPageIndex` resetting your server pagination - -By default, when data identity changes, the table resets to page 0. Under -server-driven pagination, _every_ fetch is a new array, so the table resets -the page index back to 0 every time. Set `autoResetPageIndex: false` and -manage page resets explicitly (e.g. reset on filter/sort change, but not on -the fetch itself). - -### 7. (HIGH) Filtering on the client when only one page is loaded - -```ts -manualPagination: true, -// columnFilteringFeature still registered, filteredRowModel slot still on features -``` - -The filtered row model now filters only the _current page_ — useless. If the -server paginates, the server must also filter; flip `manualFiltering: true` -and remove the `filteredRowModel` slot from `features`. - -### 8. (HIGH) Forgetting to depend on the controlled signals in your fetch - -If your `rxResource` / Query's `queryKey` doesn't read `pagination()`, -`sorting()`, `globalFilter()`, refetches won't happen. Both the table and the -fetcher must observe the same signals. - -### 9. (MEDIUM) Reimplementing pagination state with raw `pageIndex` / - -`pageSize` signals separate from the table - -```ts -// ❌ Two sources of truth -readonly pageIndex = signal(0) -readonly pageSize = signal(10) -// table doesn't know about either - -// ✅ -readonly pagination = signal({ pageIndex: 0, pageSize: 10 }) -state: { pagination: this.pagination() } -onPaginationChange: ... -``` - -Same lesson: use `setSorting`, not a manual sort signal that the table can't -see. - -### 10. (MEDIUM) Not resetting `pageIndex` on filter/sort change - -A common bug: user is on page 5, types in the filter, gets "no results" — but -the new filtered result set only has 2 pages. They have to manually click back -to page 1. Always reset `pageIndex` to 0 in `onGlobalFilterChange` / -`onColumnFiltersChange`. - -### 11. (MEDIUM) Thinking that omitting all row-model slots means "no row models work" - -Core row model is always automatic. `table.getRowModel().rows` returns the -data array as `Row<...>` objects no matter what — a `features` object with no -factory slots just means no client-side processing on top. - ---- - -## See also - -- `tanstack-table/angular/table-state` — state ownership, `state` vs `atoms` -- `tanstack-table/angular/compose-with-tanstack-query` — server fetch with Query -- `tanstack-table/angular/compose-with-tanstack-store` — external atom ownership -- `tanstack-table/core/filtering` — manualFiltering semantics -- `tanstack-table/core/sorting` — manualSorting semantics -- `tanstack-table/core/pagination` — manualPagination + `rowCount` / `pageCount` -- Example: `examples/angular/remote-data/` diff --git a/packages/angular-table/skills/angular/compose-with-tanstack-query/SKILL.md b/packages/angular-table/skills/angular/compose-with-tanstack-query/SKILL.md deleted file mode 100644 index 0cadcd7021..0000000000 --- a/packages/angular-table/skills/angular/compose-with-tanstack-query/SKILL.md +++ /dev/null @@ -1,486 +0,0 @@ ---- -name: angular/compose-with-tanstack-query -description: > - Compose TanStack Table v9 with `@tanstack/angular-query-experimental` for server-side data. - Key the query on the controlled table state that drives the request (pagination, sorting, - filters); use `placeholderData: keepPreviousData` to avoid a "0 rows flash" between pages; - set `manualPagination` / `manualSorting` / `manualFiltering` for the slices the server owns; - drop the matching client row-model factory slots from `features`; pass `rowCount` from the - server response; set `getRowId` for stable selection across refetches; hoist controlled slices - to Angular signals + `state` + `on[State]Change`. Alternative: `rxResource` / `httpResource` - if you don't want to add the Query dependency (see `client-to-server`). -type: composition -library: tanstack-table -framework: angular -library_version: '9.0.0-alpha.48' -requires: - - angular/table-state - - angular/client-to-server - - angular/getting-started -sources: - - TanStack/table:docs/framework/angular/guide/table-state.md - - TanStack/table:examples/angular/remote-data/ - - TanStack/query:packages/angular-query-experimental/src/ ---- - -# Compose with TanStack Query (Angular) - -> Goal: server-driven Angular Table v9 with `@tanstack/angular-query-experimental` -> as the fetch / cache / refetch layer. The pattern is the same as the -> `examples/angular/remote-data/` example, just with `injectQuery` instead of -> `rxResource`. -> -> The non-Query variant (`rxResource` / `httpResource`) is documented in -> `tanstack-table/angular/client-to-server`. Both work — Query adds caching, -> request deduplication, background refetch, and offline coordination. - ---- - -## 1. Install - -```bash -pnpm add @tanstack/angular-query-experimental -``` - -Then in `app.config.ts`: - -```ts -import { - provideTanStackQuery, - QueryClient, -} from '@tanstack/angular-query-experimental' - -export const appConfig: ApplicationConfig = { - providers: [ - provideTanStackQuery(new QueryClient()), - // ... - ], -} -``` - ---- - -## 2. The pattern in one snippet - -```ts -import { - ChangeDetectionStrategy, - Component, - computed, - inject, - signal, -} from '@angular/core' -import { HttpClient, HttpParams } from '@angular/common/http' -import { lastValueFrom, map } from 'rxjs' -import { - injectQuery, - keepPreviousData, -} from '@tanstack/angular-query-experimental' -import { - FlexRender, - injectTable, - tableFeatures, - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - globalFilteringFeature, - createColumnHelper, - type ColumnDef, - type PaginationState, - type SortingState, -} from '@tanstack/angular-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - globalFilteringFeature, -}) - -const columnHelper = createColumnHelper() - -type TodoResponse = { items: Array; totalCount: number } - -@Component({ - selector: 'app-root', - imports: [FlexRender], - templateUrl: './app.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class App { - private readonly http = inject(HttpClient) - - // 1. Hoist controlled slices to Angular signals - readonly pagination = signal({ pageIndex: 0, pageSize: 10 }) - readonly sorting = signal([]) - readonly globalFilter = signal(null) - - // 2. Query keyed on those signals — refetches when any of them change - readonly todosQuery = injectQuery(() => ({ - queryKey: ['todos', this.pagination(), this.sorting(), this.globalFilter()], - queryFn: () => { - const p = this.pagination() - const s = this.sorting() - const f = this.globalFilter() - - let params = new HttpParams({ - fromObject: { _page: p.pageIndex + 1, _limit: p.pageSize }, - }) - if (f) params = params.set('title_like', f) - if (s.length) { - params = params - .set('_sort', s.map((x) => x.id).join(',')) - .set('_order', s.map((x) => (x.desc ? 'desc' : 'asc')).join(',')) - } - - return lastValueFrom( - this.http - .get>('https://jsonplaceholder.typicode.com/todos', { - params, - observe: 'response', - }) - .pipe( - map( - (res) => - ({ - items: res.body ?? [], - totalCount: Number(res.headers.get('X-Total-Count')), - }) satisfies TodoResponse, - ), - ), - ) - }, - placeholderData: keepPreviousData, // ← prevents "0 rows" flash on refetch - staleTime: 30_000, - })) - - // 3. Stable column defs (module-scope-able) - readonly columns: Array> = [ - columnHelper.accessor('id', { header: 'Id', cell: (i) => i.getValue() }), - columnHelper.accessor('title', { - header: 'Title', - cell: (i) => i.getValue(), - }), - columnHelper.accessor('completed', { - header: 'Completed', - cell: (i) => (i.getValue() ? '✅' : '❌'), - }), - ] - - // 4. Wire the table — manual flags, features without row-model factories, rowCount, getRowId - readonly table = injectTable(() => { - const data = this.todosQuery.data() ?? { items: [], totalCount: 0 } - return { - features, // ← features has no paginatedRowModel/sortedRowModel/filteredRowModel slots - columns: this.columns, - data: data.items, - getRowId: (row) => String(row.id), - - state: { - pagination: this.pagination(), - sorting: this.sorting(), - globalFilter: this.globalFilter(), - }, - - manualPagination: true, - manualSorting: true, - manualFiltering: true, - - rowCount: data.totalCount, // for getPageCount() under manualPagination - - autoResetPageIndex: false, // we manage page resets explicitly - - onPaginationChange: (u) => - typeof u === 'function' - ? this.pagination.update(u) - : this.pagination.set(u), - onSortingChange: (u) => - typeof u === 'function' ? this.sorting.update(u) : this.sorting.set(u), - onGlobalFilterChange: (u) => { - typeof u === 'function' - ? this.globalFilter.update(u) - : this.globalFilter.set(u) - this.pagination.update((p) => ({ ...p, pageIndex: 0 })) // UX reset - }, - } - }) -} -``` - ---- - -## 3. The four mandatory pieces - -For server-driven Table + Query to work correctly: - -1. **`queryKey` includes every signal the request reads.** If `pagination` - changes but `queryKey` doesn't include `this.pagination()`, the query - won't refetch. -2. **`placeholderData: keepPreviousData`** keeps the last response visible - during refetches. Without it, `todosQuery.data()` becomes `undefined` - mid-fetch, your table shows 0 rows for a frame, the user notices. -3. **`manualPagination` / `manualSorting` / `manualFiltering: true`** for - slices the server owns + **remove the matching factory slots from `features`** - so the table doesn't re-process the data the server already filtered/sorted/paged. -4. **`rowCount: data.totalCount`** (or `pageCount`) so `getPageCount()` - computes correctly under `manualPagination`. - -Plus: **`getRowId` for stable identity** across refetches (required for -correct row selection). - ---- - -## 4. Loading and error UI - -`injectQuery` returns a signal-rich object. Read the state in templates: - -```html -@if (todosQuery.isPending()) { -

Loading…

-} @else if (todosQuery.isError()) { -

Failed: {{ todosQuery.error()?.message }}

-} @else { - - ... -
-} -``` - -With `placeholderData: keepPreviousData`, you'll usually want to show the -table even while refetching, plus an inline indicator: - -```html - - - ... - - - ... - -
- -@if (todosQuery.isFetching()) { -
Refreshing…
-} -``` - -`isPending()` is true only for the very first fetch; `isFetching()` is true -on every background refetch. - ---- - -## 5. Pagination button states - -Under `manualPagination` + `keepPreviousData`, the next-page button should be -disabled when there's no more data — but `getCanNextPage()` only knows that -because you passed `rowCount`. Always pass it: - -```html - -Page {{ table.atoms.pagination.get().pageIndex + 1 }} of {{ - table.getPageCount() }} - -``` - -Disabling buttons during `isFetching()` prevents double-clicks that fire a -second refetch. - ---- - -## 6. Row selection across refetches - -```ts -// In the table options -getRowId: (row) => String(row.id), -``` - -Plus register `rowSelectionFeature`. Now `RowSelectionState` is keyed by -`row.id` (the server primary key) — refetches that change row order don't -break selection. Without `getRowId`, IDs default to row index and selection -points at the wrong rows after a sort flip or refetch. - ---- - -## 7. Mutations (cell-level edits) - -Use Query mutations for cell edits and invalidate the list query on success. -For inline editing UI, see `compose-with-tanstack-form` (when it ships) — or -the `examples/angular/editable/` pattern with `flexRenderComponent` and a -local edit signal. - -```ts -import { inject } from '@angular/core' -import { injectMutation, QueryClient } from '@tanstack/angular-query-experimental' -import { lastValueFrom } from 'rxjs' - -private readonly queryClient = inject(QueryClient) - -readonly toggleTodoMutation = injectMutation(() => ({ - mutationFn: (id: number) => - lastValueFrom(this.http.patch(`/todos/${id}`, { /* … */ })), - onSuccess: () => { - this.queryClient.invalidateQueries({ queryKey: ['todos'] }) - }, -})) - -// In a cell: -cell: ({ row }) => flexRenderComponent(ToggleButton, { - inputs: { todo: row.original }, - outputs: { toggle: (id) => this.toggleTodoMutation.mutate(id) }, -}) -``` - -`invalidateQueries` triggers a background refetch; with `keepPreviousData`, -the user sees the existing list while the new one loads. - ---- - -## 8. Should I use external `Store` atoms or Angular signals here? - -For the filter / sort / pagination slices that the Query reads, **either -works**. The example above uses Angular signals because they read cleanly with -`@for` and `OnPush`, and Query's `queryKey` polls them on the next CD cycle. - -Use an external TanStack Store atom (see `compose-with-tanstack-store`) when: - -- The same slice must drive multiple tables. -- You're syncing the slice to the URL or `localStorage`. -- Other non-table consumers in the app already use the atom. - -In those cases: - -```ts -import { paginationStore } from './stores' -import { injectSelector } from '@tanstack/angular-store' - -readonly paginationSig = injectSelector(paginationStore, (s) => s) - -readonly todosQuery = injectQuery(() => ({ - queryKey: ['todos', this.paginationSig() /*, sort, filter */], - // ... -})) - -readonly table = injectTable(() => ({ - // ... - atoms: { pagination: paginationStore }, - // no state.pagination needed — the atom owns it -})) -``` - ---- - -## Failure modes - -### 1. (CRITICAL) `queryKey` missing the controlled signals - -```ts -// ❌ Query won't refetch when pagination changes -queryKey: ['todos'], - -// ✅ -queryKey: ['todos', this.pagination(), this.sorting(), this.globalFilter()], -``` - -Symptom: paginating "doesn't load the next page." Always include every signal -the request reads. - -### 2. (CRITICAL) No `placeholderData: keepPreviousData` → "0 rows flash" - -Without it, `data()` is `undefined` mid-refetch, the table renders 0 rows -for a frame. `keepPreviousData` (imported from -`@tanstack/angular-query-experimental`) keeps the last successful payload -visible until the new one resolves. - -### 3. (CRITICAL) Forgetting `rowCount` under `manualPagination` - -`getPageCount()` returns `-1`, "next" never disables. The server tells you -how many rows exist — pass it. - -### 4. (CRITICAL) Keeping client row-model factory slots for slices the server owns - -```ts -// ❌ Double-processes the data -manualPagination: true, -// features still has paginatedRowModel slot → re-paginates server page - -// ✅ remove the factory slot from features for server-owned slices -const features = tableFeatures({ - rowPaginationFeature, - // paginatedRowModel intentionally omitted — server handles it -}) -``` - -Same applies to `sortedRowModel` under `manualSorting` and `filteredRowModel` -under `manualFiltering`. - -### 5. (CRITICAL) Missing `getRowId` with row selection - -Selection is keyed by row ID. Index-as-ID breaks when the server returns rows -in a new order. `getRowId: (row) => row.id`. - -### 6. (HIGH) `autoResetPageIndex` bouncing the user to page 0 on every refetch - -Every Query response is a new array reference → table sees "new data" → resets -`pageIndex`. Set `autoResetPageIndex: false` and reset explicitly when you -want to (e.g. in `onGlobalFilterChange`). - -### 7. (HIGH) Refetching on every keystroke - -Typing into the global filter fires a fetch per character. Debounce: either -keep a separate `globalFilterInput` signal and propagate to `globalFilter` on -a delay, or compose with `@tanstack/angular-pacer` (when its skill ships). -Also reset `pageIndex: 0` on filter change. - -### 8. (HIGH) Forgetting the updater-fn branch in `on[State]Change` - -```ts -// ❌ Crashes when TanStack Table passes a function (e.g. table.setPageIndex((p) => p + 1)) -onPaginationChange: (value) => this.pagination.set(value) - -// ✅ -onPaginationChange: (u) => - typeof u === 'function' ? this.pagination.update(u) : this.pagination.set(u) -``` - -### 9. (MEDIUM) Reaching for `effect(...)` to call `query.refetch()` on signal changes - -Don't. The whole point of `queryKey` is that Query refetches when the key -changes. Adding an `effect` that calls `refetch()` produces double fetches and -race conditions. Trust the key. - -### 10. (MEDIUM) Two sources of truth for filter state - -Common bug: keep a `signal('')` for the input AND a `state.globalFilter` -controlled value, and try to sync them via `effect`. Pick one: the table's -`globalFilter` is fine for both UI and server query. If you need debouncing, -use an _additional_ `globalFilterInput` signal for the raw input and update -the table-controlled signal on a delay. - ---- - -## See also - -- `tanstack-table/angular/client-to-server` — the no-Query baseline using - `rxResource` / `httpResource` -- `tanstack-table/angular/table-state` — Angular signal + `state` + `on*Change` -- `tanstack-table/angular/compose-with-tanstack-store` — when to use shared - atoms instead of signals -- `tanstack-table/core/filtering` — manualFiltering semantics -- `tanstack-table/core/sorting` — manualSorting semantics -- `tanstack-table/core/pagination` — manualPagination + `rowCount` -- Example: `examples/angular/remote-data/` — analogous pattern with - `rxResource` instead of `injectQuery` -- `@tanstack/angular-query-experimental` docs for `injectQuery`, - `injectMutation`, `injectInfiniteQuery`, `QueryClient` diff --git a/packages/angular-table/skills/angular/compose-with-tanstack-store/SKILL.md b/packages/angular-table/skills/angular/compose-with-tanstack-store/SKILL.md deleted file mode 100644 index 8984eb643f..0000000000 --- a/packages/angular-table/skills/angular/compose-with-tanstack-store/SKILL.md +++ /dev/null @@ -1,405 +0,0 @@ ---- -name: angular/compose-with-tanstack-store -description: > - Compose TanStack Table v9 with `@tanstack/angular-store`. TanStack Table v9 is itself built on - TanStack Store — each state slice is an atom. Read surfaces include `table.atoms.` - (per-slice readonly, signal-backed), `table.state` / `table.store` (flat readonly views), and `table.baseAtoms.` - (writable). The `atoms` table option lets you replace an internal slice with an external - TanStack Store atom for cross-app sharing (URL sync, persistence, multi-table coordination). - In Angular, native signals + `state` + `on[State]Change` is the default; reach for external - atoms only when ownership crosses an app boundary the signal model can't easily span. -type: composition -library: tanstack-table -framework: angular -library_version: '9.0.0-alpha.48' -requires: - - angular/table-state - - state-management -sources: - - TanStack/table:docs/framework/angular/guide/table-state.md - - TanStack/table:docs/framework/angular/angular-table.md - - TanStack/table:packages/angular-table/src/reactivity.ts - - TanStack/table:packages/angular-table/src/injectTable.ts - - TanStack/store:packages/angular-store/src/ ---- - -# Compose with TanStack Store (Angular) - -> TanStack Table v9 **is** a TanStack Store consumer. The internal state model -> uses `alien-signals` atoms, exposed as `table.atoms.`, -> `table.baseAtoms.`, and the flat `table.store`. In Angular, those -> atoms are signal-backed via the `angularReactivity(injector)` binding. -> -> **For most Angular Table apps, native signals + `state` + `on[State]Change` -> is the right ownership model.** Reach for `@tanstack/angular-store` atoms -> when the slice must travel through code that doesn't share an injection -> scope with the table — URL sync, multi-table coordination, persistence -> layers, server caches, devtools. - ---- - -## 1. The three read surfaces - -Every TanStack Table instance exposes its state at three layers: - -| Surface | Shape | Angular reactivity | Use when | -| ------------------------- | --------------------------------- | ------------------------------- | ----------------------------------------------- | -| `table.baseAtoms.` | Writable `Atom` | Backed by an Angular `signal` | Direct internal writes; rare | -| `table.atoms.` | Readonly `Atom` (derived) | Backed by an Angular `computed` | Reading or driving Angular reactivity per-slice | -| `table.state` | Readonly flat proxy | Reads from slice atoms | Full-state JSON/debug output | -| `table.store` | Readonly flat `Store` | Backed by an Angular `computed` | Low-level flat store access; rare | - -All three are populated only for **registered features** (`features`). All -three are signal-backed via `angularReactivity(injector)`: -`createReadonlyAtom` → Angular `computed`, `createWritableAtom` → Angular -`signal`, subscriptions bridged through `toObservable(computed(signal), { -injector })`. - -Read them inside templates, `computed(...)`, or `effect(...)` and Angular -tracks the dependency. - -```ts -this.table.atoms.pagination.get() // current value (reactive) -this.table.atoms.pagination.subscribe(obs) // RxJS observer form -this.table.state.pagination // flat proxy read; prefer atoms for narrow reads -this.table.baseAtoms.pagination.set(...) // direct internal write (avoid) -``` - ---- - -## 2. The `atoms` option — bring your own atom - -The `atoms` table option lets you replace the internal `baseAtom` for a slice -with an **external TanStack Store atom**. Once registered, `table.atoms.` -reads from that external atom, and `table.set` / `feature.on*Change` write -through it. - -```ts -import { Store } from '@tanstack/store' -import { - injectTable, - tableFeatures, - rowPaginationFeature, - rowSortingFeature, - type PaginationState, - type SortingState, -} from '@tanstack/angular-table' - -const features = tableFeatures({ rowPaginationFeature, rowSortingFeature }) - -// Module-scope (or app-scope) shared atoms -export const paginationStore = new Store({ - pageIndex: 0, - pageSize: 25, -}) - -export const sortingStore = new Store([]) - -@Component({...}) -export class App { - readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - atoms: { - pagination: paginationStore, - sorting: sortingStore, - }, - })) -} -``` - -Now `paginationStore.state` and `table.atoms.pagination.get()` always agree. -Any other consumer of `paginationStore` (a URL-sync service, another table, -a devtools panel) sees the same updates. - -> **External atoms take precedence over `state.`.** If you supply both -> `atoms.pagination` and `state.pagination`, the atom wins silently. - ---- - -## 3. When to use external atoms vs Angular signals - -The maintainer guidance: **Angular signals first**. Atoms when ownership -crosses boundaries. - -| Scenario | Use | -| ------------------------------------------------------------ | ---------------------------------------------------------------------------- | -| Single component owns the slice | Angular `signal()` + `state` + `on[State]Change` | -| URL-sync (deep-linkable pagination, filter, sort) | External `Store` atom — see §4 | -| Two tables share a `globalFilter` | External `Store` atom on a shared module | -| Persisting to `localStorage` between sessions | External `Store` atom + subscribe to write | -| TanStack Query owns the server cache, table consumes filters | External `Store` atom or Angular signal — both work; pick what reads cleaner | -| Devtools / inspector across multiple tables | External `Store` atom — uniform consumer surface | - -In a pure component-local scenario, an Angular signal is simpler — fewer -imports, no module-level globals, plays nicely with `OnPush`. - ---- - -## 4. Real example — URL-synced pagination - -```ts -import { effect, signal } from '@angular/core' -import { Router, ActivatedRoute } from '@angular/router' -import { Store } from '@tanstack/store' -import { - injectTable, - tableFeatures, - rowPaginationFeature, - type PaginationState, -} from '@tanstack/angular-table' - -const features = tableFeatures({ rowPaginationFeature }) - -// Shared, app-scope atom -export const paginationStore = new Store({ - pageIndex: 0, - pageSize: 25, -}) - -@Component({ - selector: 'page-route', - // … -}) -export class PageRoute { - private readonly router = inject(Router) - private readonly route = inject(ActivatedRoute) - - constructor() { - // Seed from URL once - const qp = this.route.snapshot.queryParamMap - paginationStore.setState((p) => ({ - pageIndex: Number(qp.get('p') ?? p.pageIndex), - pageSize: Number(qp.get('s') ?? p.pageSize), - })) - - // Mirror to URL - paginationStore.subscribe(() => { - const { pageIndex, pageSize } = paginationStore.state - this.router.navigate([], { - queryParams: { p: pageIndex, s: pageSize }, - queryParamsHandling: 'merge', - replaceUrl: true, - }) - }) - } - - readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - atoms: { - pagination: paginationStore, // ← external atom owns the slice - }, - })) -} -``` - -`table.nextPage()` now writes to `paginationStore`, which writes to the URL. -A user copying the URL into a new tab lands on the same page. - ---- - -## 5. Read external atom values reactively in Angular - -`@tanstack/angular-store` provides `injectSelector` / `injectAtom` for -deriving Angular signals from TanStack Store atoms outside the table context: - -```ts -import { injectSelector } from '@tanstack/angular-store' -import { paginationStore } from './stores' - -@Component({...}) -export class StatsBar { - readonly pageIndex = injectSelector(paginationStore, (s) => s.pageIndex) - // Angular Signal, signal-tracked normally -} -``` - -Inside a table-owning component, you already have `table.atoms.pagination.get()` -— both forms are equivalent because the external atom _is_ the internal atom -for that slice. - ---- - -## 6. Multi-table coordination - -A common pattern: two tables on the same page should share a `globalFilter`. - -```ts -import { Store } from '@tanstack/store' - -export const sharedFilter = new Store(null) - -const featuresA = tableFeatures({ - columnFilteringFeature, - globalFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, -}) - -const featuresB = tableFeatures({ - columnFilteringFeature, - globalFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, -}) - -// Table A -readonly tableA = injectTable(() => ({ - features: featuresA, - columns: columnsA, - data: this.dataA(), - atoms: { globalFilter: sharedFilter }, -})) - -// Table B (separate component, same module) -readonly tableB = injectTable(() => ({ - features: featuresB, - columns: columnsB, - data: this.dataB(), - atoms: { globalFilter: sharedFilter }, -})) -``` - -Calling `tableA.setGlobalFilter('foo')` updates `sharedFilter`, which Table B -also reads — both views filter together. Without the shared atom you'd need -a separate cross-component sync mechanism. - ---- - -## 7. Reset semantics - -This is a known sharp edge worth understanding: - -- `table.resetPagination()` (and equivalents) updates _through the feature - state updater_. When the slice is owned by an external atom, the external - atom is updated. -- `table.reset()` (the core API) resets the **internal `baseAtoms`**. Do not - use it as the primary reset for externally-owned slices — it bypasses your - external atom. -- For an externally-owned slice, reset by writing to your atom directly - (`paginationStore.setState({ pageIndex: 0, pageSize: 25 })`) or by calling - the slice-specific reset API which routes through the updater. - ---- - -## 8. State and atom precedence — the rules - -For any given registered slice, the table picks state from this priority order: - -1. **`atoms.`** (external atom) — wins everything -2. **`state.`** (controlled value, in initializer) -3. **`initialState.`** (one-time seed) -4. Feature default (slice's blank value) - -**Don't supply more than one source for the same slice** unless you -intentionally want a specific layer to win. The precedence is silent — no -runtime warning today. - ---- - -## 9. Cross-app patterns - -### Hydration / SSR-like state seeding - -For SSR-rendered Angular tables that hydrate a known initial state from the -server payload, the atom approach is the cleanest: - -```ts -const paginationStore = new Store(serverPayload.pagination) -// later: -atoms: { - pagination: paginationStore -} -``` - -The atom is constructed with the hydrated value; the table never re-seeds -from `initialState` because `atoms` takes precedence. - -### Devtools / inspector - -A separate devtools component can subscribe to `paginationStore`, -`sortingStore`, etc. without holding a reference to the table — useful when -the devtools live in a different injection scope. - ---- - -## Failure modes - -### 1. (CRITICAL) Reimplementing TanStack Store with raw signals or Subjects - -```ts -// ❌ A homegrown shared store -@Injectable({ providedIn: 'root' }) -export class FilterService { - readonly value = signal(null) - setFilter(v: string | null) { this.value.set(v) } -} - -// then trying to pipe it into the table… -state: { globalFilter: this.filterService.value() } -onGlobalFilterChange: (u) => /* ... */ -``` - -This works, but you've also lost the atom-bridge that lets `table.setGlobalFilter` -write back through. The atom flow (`atoms: { globalFilter: filterStore }`) is -fewer moving parts and idiomatic v9. Prefer it when the slice spans multiple -consumers. - -### 2. (CRITICAL) Supplying both `state.x` and `atoms.x` for the same slice - -The atom wins; the Angular signal becomes a write-only ghost. No runtime -warning today. Pick one source of truth per slice. The most common bug here is -"I added atoms.pagination but the on[State]Change handler I left from before -no longer fires" — it does fire (the atom is updated through the table's -updater plumbing), but your Angular signal isn't being read by the table. - -### 3. (CRITICAL) Using `table.baseAtoms.x.set(...)` to update an externally-owned slice - -`baseAtoms` are the internal writable atoms. When `atoms.x` is registered, -`table.atoms.x` and the feature updater route through the external atom, but -the internal `baseAtoms.x` is now an orphan — writing to it has no effect on -the table's behavior. Write to the external atom instead. - -### 4. (HIGH) Calling `table.reset()` on an externally-owned slice - -`table.reset()` resets the internal `baseAtoms` — bypasses your external -atom. Use slice-specific resets (`resetSorting()`, `resetPagination()`, -`resetGlobalFilter()`) or write to the external atom directly. - -### 5. (HIGH) Forgetting that external atom state is reactive in Angular - -Reading `paginationStore.state` in a template **is reactive** in v9 because -the adapter wraps it — but reading it in plain TypeScript outside a reactive -scope is a snapshot. Use `injectSelector(paginationStore, …)` to get an -Angular signal for general consumption, or read `table.atoms.pagination.get()` -inside the component that owns the table. - -### 6. (MEDIUM) Putting `new Store(...)` inside a component class field - -Module-level (or `providedIn: 'root'` service) is the right place. Creating a -`new Store(...)` in a component field gives you a per-instance atom — which -defeats the cross-app sharing point. Use external atoms specifically for -_shared_ state. - -### 7. (MEDIUM) Hand-rolling subscription cleanup - -When you `paginationStore.subscribe(fn)`, that returns an unsubscribe. Inside -an Angular component, prefer Angular's `effect(...) + -injectSelector(paginationStore, …)` for derived signals, or -`DestroyRef.onDestroy(unsubscribe)` for raw subscriptions. - ---- - -## See also - -- `tanstack-table/angular/table-state` — the prerequisite atom model -- `tanstack-table/angular/client-to-server` — server-driven tables (where - external atoms shine for URL sync) -- `tanstack-table/angular/compose-with-tanstack-query` — Query + Table - (sometimes external atoms simplify the bridge) -- `tanstack-table/core/state-management` — framework-agnostic atom semantics -- `@tanstack/angular-store` docs — `injectSelector`, `injectAtom`, - `createStoreContext` diff --git a/packages/angular-table/skills/angular/compose-with-tanstack-virtual/SKILL.md b/packages/angular-table/skills/angular/compose-with-tanstack-virtual/SKILL.md deleted file mode 100644 index 7837a6cf45..0000000000 --- a/packages/angular-table/skills/angular/compose-with-tanstack-virtual/SKILL.md +++ /dev/null @@ -1,399 +0,0 @@ ---- -name: angular/compose-with-tanstack-virtual -description: > - Compose TanStack Table v9 with `@tanstack/angular-virtual` for virtualized rendering of large - row sets. TanStack Table does NOT virtualize on its own. Pattern: get `rows = table.getRowModel().rows`, - feed `rows.length` to `injectVirtualizer({ count, estimateSize, getScrollElement, overscan })`, - iterate `virtualizer.getVirtualItems()` in the template, position each row with - `transform: translateY(item.start)` inside a tall sentinel, set - `[style.height.px]="virtualizer.getTotalSize()"` to make the scrollbar correct. Handle the - table-feature interactions: row-expanding (variable subRow heights → measure with - `measureElement`), column sizing/pinning (column virtualization is separate), - row-selection (selection state survives virtualization because it's keyed by row ID). -type: composition -library: tanstack-table -framework: angular -library_version: '9.0.0-alpha.48' -requires: - - angular/table-state - - angular/getting-started - - angular/angular-rendering-directives -sources: - - TanStack/table:docs/framework/angular/angular-table.md - - TanStack/virtual:packages/angular-virtual/src/ - - TanStack/table:examples/angular/basic-inject-table/ ---- - -# Compose with TanStack Virtual (Angular) - -> TanStack Table is headless — it computes which rows / cells exist, but does -> not decide which ones to render to the DOM. For tables larger than a few -> hundred visible rows, pair with [`@tanstack/angular-virtual`](https://tanstack.com/virtual) -> so only the rows in the viewport (+ overscan) actually mount. -> -> Required reading: `tanstack-table/angular/getting-started` and -> `tanstack-table/angular/table-state`. - ---- - -## 1. Install - -```bash -pnpm add @tanstack/angular-virtual -``` - -Requires the same Angular version as `@tanstack/angular-table`. - ---- - -## 2. The integration in one shape - -```ts -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - computed, - signal, - viewChild, - ElementRef, -} from '@angular/core' -import { - FlexRender, - injectTable, - tableFeatures, - type ColumnDef, -} from '@tanstack/angular-table' -import { injectVirtualizer } from '@tanstack/angular-virtual' - -const features = tableFeatures({}) - -@Component({ - selector: 'app-virtual-table', - imports: [FlexRender], - templateUrl: './virtual-table.html', - styleUrl: './virtual-table.css', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class VirtualTable { - readonly data = signal>(makeData(50_000)) - readonly scrollContainer = - viewChild.required>('scroll') - - readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - getRowId: (row) => row.id, - })) - - // Stable reference to the rows array for the virtualizer - readonly rows = computed(() => this.table.getRowModel().rows) - - readonly rowVirtualizer = injectVirtualizer(() => ({ - count: this.rows().length, - getScrollElement: () => this.scrollContainer().nativeElement, - estimateSize: () => 36, // fixed-height rows - overscan: 10, - })) -} -``` - -```html - -
- - - @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (header of headerGroup.headers; track header.id) { - - } - - } - - - - @for (virtualRow of rowVirtualizer.getVirtualItems(); track - virtualRow.key) { @let row = rows()[virtualRow.index]; - - @for (cell of row.getVisibleCells(); track cell.id) { - - } - - } - -
- {{ value }} -
- {{ value }} -
-
-``` - -### What's doing what - -- **Table** produces `rows` (`table.getRowModel().rows`). Length, identity, - order are decided by registered features (sort, filter, pagination, - grouping). -- **Virtualizer** turns `rows.length` into the subset of "virtual items" - currently visible (+ `overscan`). It tracks scroll on `getScrollElement()` - and emits `getVirtualItems()` keyed by `virtualRow.key` (the row index by - default). -- **Template** renders only the virtual items, positions each with - `translateY(virtualRow.start)` inside a sentinel of total height - `getTotalSize()`. The scrollbar reflects the full row count, but only the - visible window has DOM nodes. - ---- - -## 3. Mandatory layout details - -This integration touches CSS in a few non-obvious places. None are optional: - -- **Scroll container has a fixed height** (`height: 600px` / `100vh` / - whatever) **and `overflow: auto`**. The virtualizer needs both to compute - visible range. -- **Use `display: grid` on ``, `display: flex` on `` / - `` / ``**, or use `
` markup. Native `
` layout - defeats positioning rows absolutely. The virtual example above uses CSS - grid to keep semantic table markup while letting the rows position freely. -- **Container `` is `position: relative` with explicit - `height = virtualizer.getTotalSize()`.** Without that height, the scrollbar - doesn't reflect the full data. -- **Rows are `position: absolute; top: 0; left: 0; width: 100%` with - `transform: translateY(virtualRow.start)`.** -- **Sticky header**: `position: sticky; top: 0; z-index: 1` on the `` - / its `` — the scroll container provides the scrolling. - ---- - -## 4. Variable row heights — measure dynamically - -When rows can be different heights (expanded subRows, dynamic cell content), -pass `measureElement` and a sensible `estimateSize`: - -```ts -readonly rowVirtualizer = injectVirtualizer(() => ({ - count: this.rows().length, - getScrollElement: () => this.scrollContainer().nativeElement, - estimateSize: () => 36, - overscan: 10, - measureElement: (element) => element?.getBoundingClientRect().height ?? 36, -})) -``` - -In the template, bind the element so the virtualizer can measure it: - -```html - -``` - -(See `@tanstack/angular-virtual` docs for the exact directive name and API; -the principle is: every mounted row reports its real size, the virtualizer -caches that, scrollbar adjusts.) - ---- - -## 5. Row expanding — `rowExpandingFeature` - -Combine with `rowExpandingFeature` for "click to expand details": - -- Register `rowExpandingFeature` in `features` and - `expandedRowModel: createExpandedRowModel()` as a slot on `features`. -- Use `table.getExpandedRowModel().rows` (or `getRowModel().rows`, which - already includes expansion under `paginateExpandedRows: true` semantics — - see `tanstack-table/core/row-expanding`). -- **Always use `measureElement`** because expansion changes row heights. -- The virtualizer keys items by index; expanded subRows shift later rows - down — that's correct and expected. - ---- - -## 6. Row selection works transparently - -Row selection is keyed by row ID (`getRowId`), not by DOM presence. A row can -be selected while off-screen; scrolling it into view shows the right checkbox -state. **Always set `getRowId`** — critical for both selection and -virtualizer key stability. - ---- - -## 7. Column virtualization (horizontal) - -For very wide tables (50+ columns), virtualize columns too — a second -`injectVirtualizer` over `table.getVisibleLeafColumns().length`. The pattern -mirrors row virtualization but on the X axis. Combine with -`columnPinningFeature` so pinned columns escape the virtualizer (always -rendered, sticky). - -That's a meaningfully bigger lift — most tables don't need it. Reach for it -only when you've profiled and column count is the bottleneck. - ---- - -## 8. Interaction with pagination - -**If you paginate, you usually don't virtualize.** Pagination already caps -the rendered row count to `pageSize`. Adding virtualization on top is -typically wasted effort — you've already solved the rendering bottleneck. - -The exceptions: - -- Pages can hold thousands of rows (rare). -- Pagination is "load more" / infinite scroll style — then virtualize the - accumulated rows. - ---- - -## 9. Interaction with sticky / pinned rows - -`rowPinningFeature` + virtualization is fiddly. Pinned rows live at the -top/bottom of the table; they should render _outside_ the virtualizer's -absolute positioning. Render them in dedicated `` / -top/bottom-of-`` sections, and call `table.getCenterRows()` (the -non-pinned rows) to feed the virtualizer. See -`tanstack-table/core/row-pinning` for the API surface. - ---- - -## 10. SSR / first-paint - -On the server / first hydration, the scroll container's height is unknown; -the virtualizer can render zero rows. Two mitigations: - -- Render a small initial chunk server-side (without the virtualizer) and let - Angular hydrate into the virtualized version client-side. -- Provide an explicit `initialRect: { width, height }` to the virtualizer - options for SSR. - ---- - -## Failure modes - -### 1. (CRITICAL) Trying to use TanStack Table's own virtualization - -There is none. TanStack Table doesn't ship a virtualizer. If an agent -suggests `getVirtualizedRows()` or `enableVirtualization: true` on the table — -those don't exist. Use `@tanstack/angular-virtual`. - -### 2. (CRITICAL) Missing height on the scroll container - -```html - -
- -
-
-``` - -The virtualizer measures the _scroll element_'s viewport. Without an explicit -or computed height, the viewport is 0 and nothing renders. - -### 3. (CRITICAL) Missing `getTotalSize()` height on the row container - -```html - - - - - - -``` - -Without this, you can scroll to the bottom of the _visible_ rows but can -never reach row 1000. The scrollbar lies. - -### 4. (CRITICAL) Forgetting `transform: translateY(...)` per row - -Absolutely-positioned rows without `transform` stack at `top: 0` — every row -renders on top of every other. - -### 5. (CRITICAL) Using native `
` layout with absolute-positioned rows - -Native `
` layout overrides positioning on `` / ` - -// v9 — uses the FlexRender component - - - -``` - -`FlexRender` handles grouping placeholder / aggregated branches when `columnGroupingFeature` is registered. - -Source: `packages/lit-table/src/flexRender.ts`. - -### 5. Move to slice atoms (optional but preferred) - -```ts -// v8 / v9 fallback — @state() field + onStateChange -@state() private _sorting: SortingState = [] - -protected render() { - const table = this.tableController.table({ - /* … */, - state: { sorting: this._sorting }, - onSortingChange: (u) => { - this._sorting = u instanceof Function ? u(this._sorting) : u - }, - }) -} - -// v9 preferred — external atom (per-slice ownership, no on*Change needed) -import { createAtom } from '@tanstack/store' - -const sortingAtom = createAtom([]) - -protected render() { - const table = this.tableController.table({ - /* … */, - atoms: { sorting: sortingAtom }, - }) -} -``` - -Source: `examples/lit/basic-external-atoms/src/main.ts`. - -### 6. Drop `onStateChange` - -The v8-style global `onStateChange` is gone. Subscribe per-slice with `on*Change`, an external atom, or `table.store.subscribe(...)` if you really need every change. - -## Common Mistakes - -### CRITICAL Keeping the v8 controller-with-thunk shape - -Wrong: - -```ts -// v8 shape — `controller.table` was a property -private tableController = new TableController(this, () => ({ columns, data: this.data })) - -protected render() { - const table = this.tableController.table // no longer a property in v9 -} -``` - -Correct: drop the thunk; call `.table(options, selector?)` each render. -Source: `packages/lit-table/src/TableController.ts`. - -### CRITICAL Keeping `get*RowModel` options after upgrading - -Wrong: - -```ts -this.tableController.table({ - features, - columns, - data, - getSortedRowModel: getSortedRowModel(), // ignored -}) -``` - -Correct: - -```ts -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -this.tableController.table({ - features, - columns, - data, -}) -``` - -v9 doesn't read `get*RowModel` options. Features and their row model factories are registered together in `tableFeatures({...})`. - -### CRITICAL Calling a feature API without registering the feature - -Wrong: - -```ts -const features = tableFeatures({}) // no rowSelectionFeature -const table = this.tableController.table({ - features, - columns, - data, -}) -table.getIsAllRowsSelected() // type error / runtime no-op -``` - -Correct: - -```ts -const features = tableFeatures({ rowSelectionFeature }) -const table = this.tableController.table({ - features, - columns, - data, - enableRowSelection: true, -}) -``` - -This is the #1 v9 trap. -Source: `docs/guide/features.md`. - -### HIGH Re-using `getCoreRowModel: getCoreRowModel()` - -Wrong: still passing `getCoreRowModel` — v9 includes the core row model automatically. - -Correct: drop it. The core row model is always included automatically. - -### HIGH Single-generic column helper / `ColumnDef` - -Wrong: - -```ts -const columnHelper = createColumnHelper() -const columns: ColumnDef[] = [ - /* … */ -] -``` - -Correct: - -```ts -const columnHelper = createColumnHelper() -const columns: Array> = [ - /* … */ -] -``` - -### HIGH Reimplementing built-ins - -Wrong: hand-rolled sort/filter/pagination outside the table — #1 AI tell. - -Correct: register the matching feature + factory and use the feature APIs. -Source: `docs/guide/features.md`. - -### MEDIUM Calling `flexRender` directly when grouping is registered - -Wrong: `flexRender(cell.column.columnDef.cell, cell.getContext())` for an aggregated/placeholder cell. - -Correct: use `FlexRender({ cell })` — it handles the aggregated/placeholder branches when `columnGroupingFeature` is registered. - -## See Also - -- `tanstack-table/lit/getting-started` — green-field v9 setup. -- `tanstack-table/lit/lit-table-controller` — controller lifecycle in depth. -- `tanstack-table/lit/table-state` — atoms, Subscribe, createTableHook. diff --git a/packages/lit-table/skills/lit/table-state/SKILL.md b/packages/lit-table/skills/lit/table-state/SKILL.md deleted file mode 100644 index 15b3bcd309..0000000000 --- a/packages/lit-table/skills/lit/table-state/SKILL.md +++ /dev/null @@ -1,475 +0,0 @@ ---- -name: lit/table-state -description: > - Wiring reactivity for `@tanstack/lit-table` v9. Covers `TableController` - (constructed once per LitElement host, `.table(options, selector?)` called per - render), reading state via `table.state` / `table.store` / `table.atoms.`, - rendering with `table.FlexRender` / `FlexRender`, fine-grained subscriptions - via the `table.subscribe` directive, owning slices with external atoms via `createAtom` + - `options.atoms`, and packaging shared config into `createTableHook` - (`useAppTable`, `createAppColumnHelper`, `useTableContext`, - `table.AppCell` / `table.AppHeader` / `table.AppFooter`). Routing keywords: - TableController, ReactiveController, useAppTable, atoms, lit-context, - FlexRender, lit-table. -type: framework -library: tanstack-table -framework: lit -library_version: '9.0.0-alpha.48' -requires: - - state-management - - setup -sources: - - TanStack/table:docs/framework/lit/guide/table-state.md - - TanStack/table:docs/framework/lit/lit-table.md - - TanStack/table:packages/lit-table/src/TableController.ts - - TanStack/table:packages/lit-table/src/subscribe-directive.ts - - TanStack/table:packages/lit-table/src/createTableHook.ts - - TanStack/table:packages/lit-table/src/flexRender.ts - - TanStack/table:packages/lit-table/src/reactivity.ts - - TanStack/table:examples/lit/basic-table-controller/src/main.ts - - TanStack/table:examples/lit/basic-external-atoms/src/main.ts - - TanStack/table:examples/lit/basic-subscribe/src/main.ts - - TanStack/table:examples/lit/basic-app-table/src/main.ts ---- - -> **Maintainer note:** the Lit adapter now runs on `@tanstack/lit-store`. The old `table.Subscribe({ source, selector, children })` helper has been replaced by the `table.subscribe(source, selector?, template)` directive (see Core Pattern 4). Further changes may land during the v9 beta cycle. - -This skill builds on `tanstack-table/state-management` and `tanstack-table/setup`. Read those first — `state-management` explains the v9 atom model. The Lit adapter wires that atom model into a `ReactiveController` (`TableController`) attached to a `LitElement` host. - -## Setup - -The shape every Lit v9 table follows: register `features` (including row model factories) at module scope, construct `TableController` once per host element, and call `.table(options, selector?)` from inside `render()`. - -```ts -import { LitElement, html } from 'lit' -import { customElement, state } from 'lit/decorators.js' -import { repeat } from 'lit/directives/repeat.js' -import { - FlexRender, - TableController, - rowSortingFeature, - createSortedRowModel, - sortFns, - tableFeatures, - type ColumnDef, -} from '@tanstack/lit-table' - -type Person = { firstName: string; lastName: string; age: number } - -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -const columns: Array> = [ - { - accessorKey: 'firstName', - header: 'First Name', - cell: (info) => info.getValue(), - }, - { accessorKey: 'lastName', header: () => html`Last Name` }, - { accessorKey: 'age', header: 'Age' }, -] - -@customElement('people-table') -export class PeopleTable extends LitElement { - // ONE controller per host. The constructor calls host.addController(this). - private tableController = new TableController(this) - - @state() - private data: Person[] = [] - - protected render() { - const table = this.tableController.table( - { - features, - columns, - data: this.data, - }, - (state) => ({ sorting: state.sorting }), - ) - - return html` -
`. Either: - -- Use `display: grid` on `` and `display: flex` on `` / `` uses `height: virtualizer.getTotalSize() + 'px'`, each row uses `position: absolute` + `transform: translateY(...)`. - -### MEDIUM Mixing virtualization with `manualPagination` - -You usually don't need both. Server pagination already limits the row count. Virtualize when the client holds the full dataset. - -## See Also - -- `tanstack-table/lit/table-state` — Subscribe for fine-grained re-renders. -- `tanstack-table/lit/lit-table-controller` — controller lifecycle. -- `tanstack-table/row-expanding` — virtualizing rows with sub-component rows requires variable height + measureElement. diff --git a/packages/lit-table/skills/lit/getting-started/SKILL.md b/packages/lit-table/skills/lit/getting-started/SKILL.md deleted file mode 100644 index f234379ff3..0000000000 --- a/packages/lit-table/skills/lit/getting-started/SKILL.md +++ /dev/null @@ -1,343 +0,0 @@ ---- -name: lit/getting-started -description: > - End-to-end first-table journey for `@tanstack/lit-table` v9: install the - adapter (plus required `lit` and `@lit/context` peers), declare `features` - via `tableFeatures()` (row model factories live as slots on the features - object), build a typed column helper, construct one `TableController` per - LitElement host, call `.table(options, selector?)` inside `render()`, and - render with `FlexRender({ cell|header|footer })`. Routing keywords: install - lit-table, first table, getting started, TableController, - basic-table-controller, tableFeatures. -type: lifecycle -library: tanstack-table -framework: lit -library_version: '9.0.0-alpha.48' -requires: - - setup - - column-definitions - - state-management - - lit/table-state -sources: - - TanStack/table:docs/installation.md - - TanStack/table:docs/framework/lit/lit-table.md - - TanStack/table:examples/lit/basic-table-controller/src/main.ts - - TanStack/table:packages/lit-table/src/TableController.ts ---- - -> **Maintainer note:** the Lit adapter is scheduled for a rewrite alongside TanStack Lit Store during the v9 beta cycle. APIs in this skill may change in a future beta. The patterns below match `9.0.0-alpha.48`. - -This skill walks through a first working Lit v9 table end-to-end. Read `tanstack-table/setup` and `tanstack-table/state-management` for v9 core concepts and `tanstack-table/lit/lit-table-controller` for the controller lifecycle. - -## Install - -`@tanstack/lit-table` is the Lit adapter. It depends on `@tanstack/table-core` and `@tanstack/store`, and lists `lit` and `@lit/context` as peer dependencies. - -```bash -npm install @tanstack/lit-table lit @lit/context -``` - -Peer dependency versions: `lit ^3.1.3`, `@lit/context ^1.1.0`. - -Source: `packages/lit-table/package.json`. - -## Step 1 — Declare `features` - -v9 is explicit about what a table uses. Use `tableFeatures({...})` at module scope. The TypeScript shape drives state inference, API surface, and tree-shaking. - -```ts -import { - tableFeatures, - rowPaginationFeature, - rowSortingFeature, -} from '@tanstack/lit-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, -}) -``` - -If `features` does not include `rowSelectionFeature`, then `table.atoms.rowSelection`, `table.setRowSelection`, etc. become TypeScript errors — and the runtime won't ship that logic. Pass `tableFeatures({})` for a minimum-overhead table with just the core row model. - -Source: `docs/framework/lit/lit-table.md`; `docs/guide/features.md`. - -## Step 2 — Add row model factories to `features` - -Each registered feature that needs a row-model stage gets its factory as a slot directly on the `tableFeatures({...})` call. Pass any associated `*Fns` maps as additional slots. - -```ts -import { - tableFeatures, - rowPaginationFeature, - rowSortingFeature, - createPaginatedRowModel, - createSortedRowModel, - sortFns, -} from '@tanstack/lit-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - sortFns, -}) -``` - -The core row model is always included. A feature-free table needs only `tableFeatures({})`. - -Update the `features` declaration from Step 1 to include these slots instead of keeping a separate object. - -## Step 3 — Type your data and build columns - -```ts -import type { ColumnDef } from '@tanstack/lit-table' -import { html } from 'lit' - -type Person = { - firstName: string - lastName: string - age: number - visits: number - status: string - progress: number -} - -const columns: Array> = [ - { - accessorKey: 'firstName', - header: 'First Name', - cell: (info) => info.getValue(), - }, - { - accessorFn: (row) => row.lastName, - id: 'lastName', - header: () => html`Last Name`, - cell: (info) => html`${info.getValue()}`, - }, - { - accessorKey: 'age', - header: () => 'Age', - cell: (info) => info.renderValue(), - }, - { accessorKey: 'visits', header: () => html`Visits` }, - { accessorKey: 'status', header: 'Status' }, - { accessorKey: 'progress', header: 'Profile Progress' }, -] -``` - -For a more type-safe path, use `createColumnHelper()`. - -Source: `examples/lit/basic-table-controller/src/main.ts`. - -## Step 4 — Build the LitElement host with `TableController` - -```ts -import { LitElement, html } from 'lit' -import { customElement, state } from 'lit/decorators.js' -import { repeat } from 'lit/directives/repeat.js' -import { FlexRender, TableController } from '@tanstack/lit-table' - -@customElement('lit-table-example') -class LitTableExample extends LitElement { - // ONE controller per host. Constructed as a class field so the constructor's - // `host.addController(this)` call happens once. - private tableController = new TableController(this) - - @state() - private data: Person[] = makeData(20) - - private rerender() { - this.data = makeData(20) - } - - protected render() { - // Build the table for THIS render pass. - // First call constructs the core table + subscribes the host to table.store. - // Later calls merge options into the same instance. - const table = this.tableController.table( - { - features, - columns, - data: this.data, - }, - () => ({}), // empty selector — we don't need to project state for this minimal example - ) - - return html` - -
` - (see §3), preserving semantic markup, OR -- Use `
` markup throughout. - -### 6. (HIGH) Variable row heights without `measureElement` - -Default `estimateSize` is a constant. Different real heights → wrong -positions → rows visually overlap or leave gaps. Pass `measureElement` and a -way for each mounted row to report its real size. - -### 7. (HIGH) Pagination + virtualization both enabled - -Pagination already caps row count. Adding virtualization on top doubles the -indirection for no win. Pick one. - -### 8. (HIGH) Reimplementing virtualization with `IntersectionObserver` - -Saw an agent build a homegrown "render rows when visible" with -`IntersectionObserver`? That's hundreds of lines of broken virtualization. -Use the library. - -### 9. (HIGH) Wrong `track` in the virtualized `@for` - -```html - -@for (virtualRow of rowVirtualizer.getVirtualItems(); track row.id) - - -@for (virtualRow of rowVirtualizer.getVirtualItems(); track virtualRow.key) -``` - -Track by the virtual item's stable key (or index). The row is _inside_ the -virtual item — Angular uses the outer track for DOM reuse. - -### 10. (MEDIUM) `injectVirtualizer` outside an injection context - -Like `injectTable`, `injectVirtualizer` calls `assertInInjectionContext()`. -Place it on a class field, in a constructor, or inside `runInInjectionContext`. - -### 11. (MEDIUM) Recreating `count` / `estimateSize` on every signal change without - -stable callbacks - -Move `estimateSize`, `measureElement`, `getScrollElement` to stable -references (class arrow methods or module-scope functions) where possible. -Otherwise the virtualizer re-initializes its internal state on every change. - -### 12. (MEDIUM) Missing `getRowId` — selection breaks across re-sorts in a - -virtualized table - -This isn't virtualization-specific, but it's especially visible here because -virtualization renders a window of rows; refreshing that window via scroll -makes mismatched checkbox state obvious. `getRowId: (row) => row.id` is -mandatory. - ---- - -## See also - -- `tanstack-table/angular/getting-started` — baseline table that this skill - layers virtualization on top of -- `tanstack-table/angular/production-readiness` — when to reach for - virtualization vs server-side pagination -- `tanstack-table/core/row-expanding` — variable subRow heights + virtual -- `tanstack-table/core/column-layout` — pinning interaction -- `@tanstack/angular-virtual` docs — `injectVirtualizer`, options reference, - variable-height patterns diff --git a/packages/angular-table/skills/angular/getting-started/SKILL.md b/packages/angular-table/skills/angular/getting-started/SKILL.md deleted file mode 100644 index 9b2527e241..0000000000 --- a/packages/angular-table/skills/angular/getting-started/SKILL.md +++ /dev/null @@ -1,487 +0,0 @@ ---- -name: angular/getting-started -description: > - End-to-end first-table journey for TanStack Table v9 in Angular: install - `@tanstack/angular-table`, declare `features` with `tableFeatures()` (row-model factories and - fn registries live on the features object), build columns with the `TFeatures, TData` generic - order, call `injectTable(() => ({...}))` from an injection context, and render with `FlexRender` / - `*flexRenderHeader` / `*flexRenderCell` / `*flexRenderFooter`. Covers the minimum-viable - signal-backed table plus the upgrade path to sorting + filtering + pagination. -type: lifecycle -library: tanstack-table -framework: angular -library_version: '9.0.0-alpha.48' -requires: - - angular/table-state - - angular/angular-rendering-directives - - setup - - column-definitions -sources: - - TanStack/table:docs/framework/angular/angular-table.md - - TanStack/table:docs/framework/angular/guide/table-state.md - - TanStack/table:docs/framework/angular/guide/rendering.md - - TanStack/table:packages/angular-table/src/injectTable.ts - - TanStack/table:examples/angular/basic-inject-table/ - - TanStack/table:examples/angular/basic-app-table/ ---- - -# Getting Started — Angular Table v9 - -> Goal: from zero to a working signal-backed, sorted + paginated, type-safe -> table in Angular ≥19. -> -> v9 is **explicit**: tell the table which features you want with `features`. -> Row-model factories and fn registries live on the features object alongside -> the feature flags. That explicitness is what makes the v9 bundle tree-shakeable. - ---- - -## 1. Install - -```bash -pnpm add @tanstack/angular-table -# or npm / yarn / bun -``` - -Requires Angular ≥19 (signal APIs, `input()`, structural directive metadata). -Standalone components are assumed. - ---- - -## 2. The simplest possible table (core only) - -```ts -// app.ts -import { ChangeDetectionStrategy, Component, signal } from '@angular/core' -import { - FlexRender, - injectTable, - tableFeatures, - type ColumnDef, -} from '@tanstack/angular-table' - -type Person = { - id: string - firstName: string - lastName: string - age: number -} - -// 1. features OUTSIDE the component class (stable reference) -const features = tableFeatures({}) // empty = core row model only - -// 2. columns OUTSIDE the component class (stable reference) -const columns: Array> = [ - { - accessorKey: 'firstName', - header: 'First name', - cell: (info) => info.getValue(), - }, - { - accessorKey: 'lastName', - header: 'Last name', - cell: (info) => info.getValue(), - }, - { - accessorKey: 'age', - header: () => 'Age', - cell: (info) => info.getValue(), - }, -] - -@Component({ - selector: 'app-root', - imports: [FlexRender], // tuple imports BOTH directives - templateUrl: './app.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class App { - readonly data = signal>([ - { id: '1', firstName: 'Ada', lastName: 'Lovelace', age: 36 }, - { id: '2', firstName: 'Alan', lastName: 'Turing', age: 41 }, - ]) - - // 3. injectTable in an injection context (a class field qualifies) - readonly table = injectTable(() => ({ - features, // required in v9; core row model is automatic - columns, // stable ref - data: this.data(), // signal read → re-syncs the table on change - })) -} -``` - -```html - - - - @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (header of headerGroup.headers; track header.id) { - - } - - } - - - - @for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getVisibleCells(); track cell.id) { - - } - - } - -
- @if (!header.isPlaceholder) { - - {{ value }} - - } -
- - {{ value }} - -
-``` - -That's a complete v9 table. No sorting, no pagination — just `` markup -driven by the row model. - -### What the boilerplate is doing - -- `tableFeatures({})` registers no opt-in features. The core row model - (`getRowModel()`) is always available. With `features: tableFeatures({})`, - `table.atoms.*` only contains the slices core ships with — no `pagination`, - no `sorting`, no `rowSelection` until you add the matching features. -- `injectTable(() => ({...}))` runs the initializer, builds the table, and - re-runs the initializer whenever any signal read inside changes. Stable - references outside the initializer keep `columns` / `features` - from getting recreated on every data update. - ---- - -## 3. Add a feature — sorting - -Each opt-in feature has two pieces in v9: - -1. The **feature** itself (`rowSortingFeature`) in `features` — adds APIs - like `column.toggleSorting()` and the `sorting` state slice. -2. The **row-model factory** (`createSortedRowModel()`) on the features object - — produces the sorted output. Without it, `table.getRowModel().rows` is - unsorted regardless of sort state. - -```ts -import { - injectTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, - type ColumnDef, -} from '@tanstack/angular-table' - -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), // <-- enables sorting output - sortFns, -}) - -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), -})) -``` - -In the template, drive sorting from the header: - -```html -@if (!header.isPlaceholder) { - -} -``` - -> **Use `column.toggleSorting()`, not your own sort handler.** It correctly -> handles the asc → desc → unsorted cycle. Same applies for every other -> feature. - -`sortFns` is the registry of built-in sort functions -(`alphanumeric`, `basic`, `datetime`, etc.). Pass only the ones you use to -tree-shake (`sortFns: { basic: sortFns.basic }`), or pass `sortFns` -in its entirety for all of them. - ---- - -## 4. Add filtering + pagination - -```ts -import { - injectTable, - tableFeatures, - rowSortingFeature, - columnFilteringFeature, - rowPaginationFeature, - createSortedRowModel, - createFilteredRowModel, - createPaginatedRowModel, - sortFns, - filterFns, - type ColumnDef, -} from '@tanstack/angular-table' - -const features = tableFeatures({ - rowSortingFeature, - columnFilteringFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, - filterFns, -}) - -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - initialState: { - pagination: { pageIndex: 0, pageSize: 10 }, - }, -})) -``` - -Pagination controls — again, prefer the table APIs: - -```html - - - Page {{ table.atoms.pagination.get().pageIndex + 1 }} of {{ - table.getPageCount() }} - - - - -``` - -Reading state in the template via `table.atoms..get()` is signal-backed -— Angular tracks it and re-renders on change. - ---- - -## 5. Use the column helper for safer types - -`createColumnHelper()` (generic order: features first!) gives -type-safe accessor / display / group definitions, plus a `columns(...)` method -for better inference across heterogeneous columns: - -```ts -import { createColumnHelper } from '@tanstack/angular-table' - -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First name', - cell: (info) => info.getValue(), - }), - columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, { - id: 'fullName', - header: 'Full name', - }), - columnHelper.display({ - id: 'actions', - header: 'Actions', - cell: ({ row }) => `Edit #${row.original.id}`, - }), -]) -``` - -> v9 changed the generic order: `createColumnHelper()`, -> **not** `createColumnHelper()`. Same for `ColumnDef`. - -If multiple components share the same `features` object, factor them into a -`createTableHook(...)` call — see -`tanstack-table/angular/angular-rendering-directives` §10 and the -`composable-tables` example. - ---- - -## 6. Stable row identity — set `getRowId` - -If your rows have a primary key, set `getRowId`. This makes row selection, -row pinning, and refetch-based updates correct. - -```ts -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - getRowId: (row) => row.id, // ← stable ID across re-fetches -})) -``` - -Without `getRowId`, the row index becomes the ID — selection state -("rows 0–4 selected") survives sorting but breaks across server refetches that -return rows in a new order. - ---- - -## 7. State ownership — start with internal, hoist when you need to - -The simplest table lets TanStack Table own all state internally. You set -starting values with `initialState`, and you use APIs like `table.nextPage()` -and `table.setSorting(...)` to drive updates. - -```ts -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - initialState: { - pagination: { pageIndex: 0, pageSize: 25 }, - sorting: [{ id: 'age', desc: true }], - }, -})) -``` - -Hoist a slice into an Angular signal only when something outside the table -needs to read or react to it (URL sync, debounced server fetch, persistence, -cross-component coordination). The pattern is `state` + `on[State]Change` → -see `tanstack-table/angular/table-state` §6. - -For full server-driven tables, see `tanstack-table/angular/client-to-server`. - ---- - -## Failure modes - -### 1. (CRITICAL) Calling `injectTable` outside an injection context - -`injectTable` calls `assertInInjectionContext`. It must be invoked from a -class-field initializer, constructor, or factory inside a DI scope. Calling it -from a service method or a `setTimeout` callback throws: - -> `NG0203: inject() must be called from an injection context...` - -If you need to construct a table from a service method, capture the injector -and use `runInInjectionContext(injector, () => injectTable(...))`. - -### 2. (CRITICAL) Hallucinating v8 `createAngularTable` or `getCoreRowModel()` - -```ts -// ❌ v8 -import { createAngularTable, getCoreRowModel } from '@tanstack/angular-table' - -// ✅ v9 -import { injectTable, tableFeatures } from '@tanstack/angular-table' -``` - -There is no `getCoreRowModel()` / `getSortedRowModel()` / `getFilteredRowModel()` -in v9. Core row model is automatic; the rest are -`createSortedRowModel()` / `createFilteredRowModel()` / etc. -registered as slots on the `features` object alongside `sortFns` / `filterFns`. - -### 3. (CRITICAL) Reimplementing what the table API already does - -Telltale AI signs in a getting-started snippet: - -- Custom `sortBy()` on the data signal instead of `table.setSorting()` / - `column.toggleSorting()`. -- Manual `pageIndex` math instead of `table.nextPage()` / `table.getCanNextPage()`. -- Computing `getCanNextPage()` as `pageIndex < Math.ceil(rows / pageSize) - 1` - instead of asking the table. -- Manual filtering of the data array before passing it to the table when you - could just register `columnFilteringFeature` + `createFilteredRowModel`. - -The table already does all of this. Use it. - -### 4. (HIGH) Feature without its row model (or vice versa) - -```ts -// ❌ rowSortingFeature without sortedRowModel → sort state changes, rows don't reorder -const features = tableFeatures({ rowSortingFeature }) - -// ✅ factory and fn registry on the features object -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -``` - -Full mapping table → [`references/feature-row-model-mapping.md`](references/feature-row-model-mapping.md). - -### 5. (HIGH) Declaring `columns` / `features` inside the initializer - -```ts -// ❌ Recreated on every signal change -readonly table = injectTable(() => ({ - features: tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns }), - columns: [/* … */], - data: this.data(), -})) - -// ✅ Stable references outside, signal reads inside -const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns }) -const columns: Array> = [/* … */] - -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), -})) -``` - -### 6. (HIGH) Importing a non-existent `flexRender` function - -In Angular, `FlexRender` is a directive tuple, not a function. There is no -`flexRender(fn, ctx)` call expression — that's the React/Vue API. Always: - -```ts -import { FlexRender } from '@tanstack/angular-table' -@Component({ imports: [FlexRender] }) -``` - -and use `*flexRenderCell` / `*flexRenderHeader` / `*flexRenderFooter` in the -template. - -Lower-severity failure modes (MEDIUM: `createColumnHelper` generic-order flip, -importing only `FlexRenderDirective` without the shorthand) → -[`references/feature-row-model-mapping.md`](references/feature-row-model-mapping.md#lower-severity-failure-modes-medium). - ---- - -## References - -- [Feature → row-model mapping table and MEDIUM failure modes](references/feature-row-model-mapping.md) - ---- - -## See also - -- `tanstack-table/angular/table-state` — state model, ownership, controlled vs internal -- `tanstack-table/angular/angular-rendering-directives` — full rendering API surface -- `tanstack-table/angular/migrate-v8-to-v9` — for projects upgrading from v8 -- `tanstack-table/angular/client-to-server` — flipping a working table to a server endpoint -- `tanstack-table/angular/production-readiness` — tree-shaking, stable refs, selectors -- Example: `examples/angular/basic-inject-table/` -- Example: `examples/angular/basic-app-table/` (uses `createTableHook`) diff --git a/packages/angular-table/skills/angular/getting-started/references/feature-row-model-mapping.md b/packages/angular-table/skills/angular/getting-started/references/feature-row-model-mapping.md deleted file mode 100644 index 6612313afd..0000000000 --- a/packages/angular-table/skills/angular/getting-started/references/feature-row-model-mapping.md +++ /dev/null @@ -1,51 +0,0 @@ -# Feature → Row Model Mapping & Optional Patterns - -Reference material extracted from the getting-started SKILL.md. - ---- - -## Feature → row model mapping - -Every opt-in v9 feature has two pieces: - -1. The **feature** itself in `features` — adds APIs (e.g. - `column.toggleSorting()`) and the matching state slice. -2. A **row-model factory slot** on the `features` object — produces the derived - row output. Without it, sort/filter/paginate UI updates but rows don't reorder. - -Fn registries (`sortFns`, `filterFns`, `aggregationFns`) are also slots on the -`features` object, not parameters to the factory calls. - -| Feature | Slot needed on `features` | -| ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -| `rowSortingFeature` | `sortedRowModel: createSortedRowModel()` + `sortFns` | -| `columnFilteringFeature` / `globalFilteringFeature` | `filteredRowModel: createFilteredRowModel()` + `filterFns` | -| `rowPaginationFeature` | `paginatedRowModel: createPaginatedRowModel()` | -| `rowExpandingFeature` | `expandedRowModel: createExpandedRowModel()` | -| `columnGroupingFeature` | `groupedRowModel: createGroupedRowModel()` + `aggregationFns` | -| `columnFacetingFeature` | `facetedRowModel: createFacetedRowModel()` (+ `facetedMinMaxValues` / `facetedUniqueValues`) | -| `rowSelectionFeature` | (no row model needed) | -| `columnVisibilityFeature` / `columnOrderingFeature` / `columnPinningFeature` / `columnSizingFeature` / `columnResizingFeature` / `rowPinningFeature` | (no row model needed) | - ---- - -## Lower-severity failure modes (MEDIUM) - -### Wrong `createColumnHelper` generic order - -```ts -// ❌ v8 shape -const columnHelper = createColumnHelper() - -// ✅ v9 — features first -const columnHelper = createColumnHelper() -``` - -Or use `createAppColumnHelper()` from a `createTableHook(...)` factory, -which pre-binds `TFeatures`. - -### Importing only `FlexRenderDirective` and missing the shorthand - -`FlexRender` is preferred — it imports both `FlexRenderDirective` (the long -`*flexRender`) and `FlexRenderCell` (the shorthand). If you only import one, -`*flexRenderCell` won't compile. diff --git a/packages/angular-table/skills/angular/migrate-v8-to-v9/SKILL.md b/packages/angular-table/skills/angular/migrate-v8-to-v9/SKILL.md deleted file mode 100644 index 1eb290bf5c..0000000000 --- a/packages/angular-table/skills/angular/migrate-v8-to-v9/SKILL.md +++ /dev/null @@ -1,422 +0,0 @@ ---- -name: angular/migrate-v8-to-v9 -description: > - Mechanical v8 → v9 migration for `@tanstack/angular-table`: `createAngularTable` → - `injectTable`, `get*RowModel()` options → row-model factories on the `features` object (no - separate `rowModels` option; fn registries like `filterFns`/`sortFns` are also slots on - `features`), required `features` via `tableFeatures()`, state access via - `table.atoms..get()` or `table.state` instead of `table.getState()`, - `createColumnHelper()` generic-order flip, every type now requires `TFeatures`, - `enablePinning` split into `enableColumnPinning` / `enableRowPinning`, `sortingFn` → `sortFn` - rename pile, `ColumnSizingInfo` → `ColumnResizing` split, removal of `_`-prefixed internals, - signal-backed atoms replacing v8 memoized accessors, and structural-directive rendering replacing - v8 component-based rendering. -type: lifecycle -library: tanstack-table -framework: angular -library_version: '9.0.0-alpha.48' -requires: - - angular/table-state - - angular/getting-started - - angular/angular-rendering-directives -sources: - - TanStack/table:docs/framework/angular/guide/migrating.md - - TanStack/table:docs/framework/angular/angular-table.md - - TanStack/table:packages/angular-table/src/injectTable.ts - - TanStack/table:packages/angular-table/src/reactivity.ts ---- - -# Migrate from TanStack Table v8 to v9 (Angular) - -> **Angular does not ship a legacy v8 API in v9** (unlike React's -> `useLegacyTable`). You migrate directly to v9's `injectTable` + `features` -> shape (row-model factories and fn registries now live on the `features` object). -> There is no incremental in-place adapter — the public entrypoint name itself changes. - -This skill is a mechanical translation table. Work through it top-to-bottom. - -For exhaustive lookup tables (row-model mapping, feature registration, type -generics, sorting renames, sizing-vs-resizing split, etc.) → -[`references/v8-to-v9-mapping.md`](references/v8-to-v9-mapping.md). - ---- - -## 1. Entrypoint rename - -```ts -// v8 -import { createAngularTable, getCoreRowModel } from '@tanstack/angular-table' - -const v8Table = createAngularTable(() => ({ - columns, - data: data(), - getCoreRowModel: getCoreRowModel(), -})) - -// v9 -import { injectTable, tableFeatures } from '@tanstack/angular-table' - -const features = tableFeatures({}) // empty is valid; core row model is automatic - -const v9Table = injectTable(() => ({ - features, - columns, - data: data(), -})) -``` - -Key behavioral change: **the `injectTable` initializer re-runs when signals -inside it change**, then the adapter calls `table.setOptions({ ...prev, ...new })`. -Move stable values (`columns`, `features`) **outside** the initializer so they -aren't recreated on every data update. - ---- - -## 2. Required new option: `features` - -v9 is opt-in for every feature. `features` is required. Row-model factories and -fn registries are now **slots on the features object** — there is no separate -`rowModels` option. - -```ts -// v8 — features were bundled, row models added piecewise -createAngularTable(() => ({ - columns, - data: data(), - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - filterFns, // root option - sortingFns, // root option -})) - -// v9 -import { - injectTable, - tableFeatures, - columnFilteringFeature, - rowSortingFeature, - rowPaginationFeature, - createFilteredRowModel, - createSortedRowModel, - createPaginatedRowModel, - filterFns, - sortFns, // note rename: sortingFns → sortFns -} from '@tanstack/angular-table' - -const features = tableFeatures({ - columnFilteringFeature, - rowSortingFeature, - rowPaginationFeature, - filteredRowModel: createFilteredRowModel(), // factory is now a features slot - sortedRowModel: createSortedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - filterFns, // fn registry is also a features slot - sortFns, // note rename: sortingFns → sortFns -}) - -injectTable(() => ({ - features, - columns, - data: data(), -})) -``` - -Row-model and feature lookup tables → [`references/v8-to-v9-mapping.md`](references/v8-to-v9-mapping.md#row-model-migration-table). - ---- - -## 3. State access: `getState()` → atoms or `table.state` - -```ts -// v8 -const { sorting, pagination } = table.getState() - -// v9 — full-state flat proxy -const { sorting, pagination } = table.state - -// v9 — per slice (preferred for Angular render code) -const sorting = table.atoms.sorting.get() -const pagination = table.atoms.pagination.get() -``` - -In Angular, all three (`table.atoms.`, `table.state`, -`table.baseAtoms.`) are signal-backed — reading them inside a template, -`computed(...)`, or `effect(...)` registers an Angular dependency -automatically. No `toSignal(...)` wrappers needed. Prefer direct atom reads for -specific slices; keep `table.state` for full-state JSON/debug output or code -that genuinely wants the flat state shape. - -See `tanstack-table/angular/table-state` for the full state surface mental -model. - ---- - -## 4. Controlled state — `on[State]Change` shape - -The shape is largely the same. **`onStateChange` (the single global v8 hook) is -gone in v9.** Slices are controlled individually via `state.` + -`on[State]Change` callbacks. Each callback receives either a new value or an -updater function: - -```ts -// v9 pattern -import { signal } from '@angular/core' -import type { SortingState, PaginationState } from '@tanstack/angular-table' - -readonly sorting = signal([]) -readonly pagination = signal({ pageIndex: 0, pageSize: 10 }) - -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - state: { - sorting: this.sorting(), - pagination: this.pagination(), - }, - onSortingChange: (updater) => { - updater instanceof Function - ? this.sorting.update(updater) - : this.sorting.set(updater) - }, - onPaginationChange: (updater) => { - updater instanceof Function - ? this.pagination.update(updater) - : this.pagination.set(updater) - }, -})) -``` - -> Always check `updater instanceof Function` (or `typeof updater === 'function'`). -> TanStack Table calls the callback with both shapes depending on the -> transition. - ---- - -## 5. Column helper generic-order flip - -```ts -// v8 -const columnHelper = createColumnHelper() - -// v9 — TFeatures FIRST, then TData -const columnHelper = createColumnHelper() -``` - -New in v9: `columnHelper.columns([...])` preserves each column's `TValue` — -prefer it over a bare array literal: - -```ts -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First', - cell: (i) => i.getValue(), - }), - columnHelper.display({ - id: 'actions', - header: 'Actions', - cell: () => 'Edit', - }), -]) -``` - -If you don't want to repeat `TFeatures` everywhere, use `createTableHook(...)` -and the resulting `createAppColumnHelper()` which pre-binds features. - -Every public type now requires `TFeatures` (`ColumnDef`, -`Cell`, etc.). Full mapping → -[`references/v8-to-v9-mapping.md`](references/v8-to-v9-mapping.md#type-generics--every-type-takes-tfeatures-now). - ---- - -## 6. Rendering — directive-based, with shorthand directives - -v8 Angular rendering was already directive-flavored, but v9 adds the -**shorthand directives** (`*flexRenderCell`, `*flexRenderHeader`, -`*flexRenderFooter`) that auto-resolve the column-def slot and context. Prefer -them over the long `*flexRender="… ; props: …"` form. - -```html - - - - - - - -``` - -```ts -import { FlexRender } from '@tanstack/angular-table' // ← imports both directives -@Component({ imports: [FlexRender], ... }) -``` - -v9-only: - -- `flexRenderComponent(Component, { inputs, outputs, bindings, directives, injector })` - for explicit component rendering. -- DI tokens (`TanStackTable` / `TanStackTableHeader` / `TanStackTableCell` - directives + `injectTableContext()` / `injectTableHeaderContext()` / - `injectTableCellContext()` / `injectFlexRenderContext()`) — no more input - drilling. -- Column-def `cell` / `header` / `footer` functions run inside - `runInInjectionContext`, so `inject(...)` and signals work in them. - -See `tanstack-table/angular/angular-rendering-directives` for the full surface. - ---- - -## 7. Reactivity model — signals replace v8 memo accessors - -v8 backed reactivity with manual memoized getters. v9's adapter -(`angularReactivity(injector)`) backs every readonly atom with an Angular -`computed` and every writable atom with an Angular `signal`. Consequences: - -- **No `toSignal(...)` adapters around table state.** Read `table.atoms.x.get()` - directly inside templates, `computed`, `effect` for specific slices. Use - `table.state` when you need the full-state flat shape. -- **`computed(...)` is for derivation / equality, not for "make it reactive".** - Use `{ equal: shallow }` from `@tanstack/angular-table` on object/array - slices to skip downstream work on no-op updates. -- **The `injectTable` initializer re-runs on signal changes.** Don't put - expensive object literals in there. - ---- - -## 8. Renames at a glance - -- `sortingFn` → `sortFn`, `sortingFns` → `sortFns`, `SortingFn` → `SortFn` - (full table in [`references/v8-to-v9-mapping.md`](references/v8-to-v9-mapping.md#naming-renames--sorting)). -- `enablePinning` → `enableColumnPinning` / `enableRowPinning` (split). -- `columnSizingInfo` state → `columnResizing` state (sizing/resizing split into - two features — see [`references/v8-to-v9-mapping.md`](references/v8-to-v9-mapping.md#column-sizing-vs-resizing--split)). -- All `_`-prefixed internal APIs removed; use the public equivalents. - ---- - -## Migration checklist - -- [ ] Replace `createAngularTable` import + call with `injectTable`. -- [ ] Add `features: tableFeatures({...})` (or `stockFeatures`) — required. -- [ ] Convert every `get*RowModel()` option to a slot on the `features` object - using the matching `create*RowModel()` factory. -- [ ] Move `filterFns` / `sortFns` / `aggregationFns` fn registries to slots - on the `features` object (not as factory parameters). -- [ ] Update `createColumnHelper()` → `createColumnHelper()`. -- [ ] Update every `ColumnDef` / `Cell` etc. to include - `TFeatures`. -- [ ] Replace `table.getState().slice` reads with `table.atoms..get()` - where possible; use `table.state` for full-state/debug reads. -- [ ] Remove any usage of the v8 single `onStateChange` — split into per-slice - `on[State]Change`. -- [ ] In `on[State]Change` callbacks, handle both value and updater-fn shapes. -- [ ] Move `columns` and `features` **outside** the `injectTable` initializer. -- [ ] Switch any `flexRender` long-form to `*flexRenderCell` / `*flexRenderHeader` / - `*flexRenderFooter` shorthand where applicable. -- [ ] Where you need component rendering with explicit options, switch wrapper - shape to `flexRenderComponent(Component, { inputs, outputs, ... })`. -- [ ] Replace prop-drilled cell/header inputs with - `injectTableCellContext()` / `injectTableHeaderContext()` / - `injectFlexRenderContext()` (optional but worthwhile). -- [ ] Rename `sortingFn` → `sortFn`, `getSortingFn` → `getSortFn`, - `sortingFns` → `sortFns`, `SortingFn` → `SortFn`. -- [ ] Replace `columnSizingInfo` state / setters / change handler with the - `columnResizing` equivalents; add `columnSizingFeature` and - `columnResizingFeature` to `features` if you actually drag-resize - (`columnResizingFeature` requires `columnSizingFeature`). -- [ ] Replace `enablePinning` with `enableColumnPinning` / `enableRowPinning`. -- [ ] Update `ColumnMeta` module augmentation to include the `TFeatures` - generic. -- [ ] Drop any `_`-prefixed internal API usages; replace with public - equivalents. -- [ ] (Optional) Adopt `tableOptions(...)` for shared base config. -- [ ] (Optional) Adopt `createTableHook(...)` for app-wide table infrastructure. - ---- - -## Failure modes - -### 1. (CRITICAL) Leaving `getCoreRowModel()` / `getSortedRowModel()` / etc. in v9 options - -These options don't exist anymore. The equivalent factories become slots on the -`features` object (e.g. `sortedRowModel: createSortedRowModel()`). The TypeScript -error is loud but agents sometimes silence it with `as any` — don't. - -### 2. (CRITICAL) Reaching for `createAngularTable` from v8 muscle memory - -Always `injectTable(() => ({...}))`. The injection-context requirement means -it must run from a class field, constructor, or -`runInInjectionContext(injector, () => injectTable(...))`. - -### 3. (CRITICAL) Registering a feature but forgetting its row model - -```ts -// ❌ filtering enabled, but no filtered row model — UI changes, rows don't filter -const features = tableFeatures({ columnFilteringFeature }) // missing filteredRowModel - -// ✅ -const features = tableFeatures({ - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, -}) -``` - -Same for sorting, pagination, expanding, grouping, faceting. Selection, -visibility, ordering, pinning, sizing, resizing do **not** need a row model. - -### 4. (HIGH) `getState()` → `table.state` text replacement can be too broad - -Bulk-replacing `table.getState().x` with `table.state.x` works for _current -value_ reads, but it hides which slice the component depends on. For Angular -render code and reactive derivations, switch to `table.atoms.x.get()` — it's -already signal-backed and needs no wrapper. Keep `table.state` for full-state -debug output. - -### 5. (HIGH) Stale `sortingFn` / `sortingFns` references in column defs - -The rename is mechanical: `sortingFn` → `sortFn`, `sortingFns` → `sortFns`, -`getSortingFn` → `getSortFn`. Missed renames produce silent runtime fallbacks -to default sort. - -### 6. (HIGH) Column helper using v8 generic order - -```ts -// ❌ TS will complain — the first generic is TFeatures, not TData -const columnHelper = createColumnHelper() -``` - -### 7. (HIGH) Putting `features` / `columns` inside the `injectTable` initializer - -The v8 mental model was "build columns inside the hook". v9's -`injectTable` initializer re-runs on every signal read change — keep heavy -literals outside. Because row-model factories and fn registries live on the -`features` object, keeping `features` stable at module scope covers all of them. - -Lower-severity failure modes (MEDIUM/LOW: `stockFeatures` cleanup, `enablePinning` -removal, `columnSizingInfo` rename, single-handler porting, hand-rolled -`TFeatures` in render fns, `_`-prefix internal usage, reimplementing built-in -APIs, `ColumnMeta` augmentation drift, `flexRender` as a function, mixing v8/v9 -atoms) → [`references/v8-to-v9-mapping.md`](references/v8-to-v9-mapping.md#lower-severity-failure-modes-mediumlow). - ---- - -## References - -- [Full v8 → v9 mapping tables (row models, features, types, renames, MEDIUM failure modes)](references/v8-to-v9-mapping.md) - ---- - -## See also - -- `tanstack-table/angular/getting-started` — what the v9 target shape looks like -- `tanstack-table/angular/table-state` — signal-backed atom model -- `tanstack-table/angular/angular-rendering-directives` — full rendering API -- `tanstack-table/angular/production-readiness` — once compiling, optimize the bundle -- `tanstack-table/core/migrate-v8-to-v9` — framework-agnostic core changes diff --git a/packages/angular-table/skills/angular/migrate-v8-to-v9/references/v8-to-v9-mapping.md b/packages/angular-table/skills/angular/migrate-v8-to-v9/references/v8-to-v9-mapping.md deleted file mode 100644 index a29cd4171f..0000000000 --- a/packages/angular-table/skills/angular/migrate-v8-to-v9/references/v8-to-v9-mapping.md +++ /dev/null @@ -1,268 +0,0 @@ -# v8 → v9 Mapping Tables — Full Reference - -Mechanical translation tables and detailed renames for the Angular Table v8 → -v9 migration. The SKILL.md keeps the primary patterns; this file is the -exhaustive lookup. - ---- - -## Row-model migration table - -Row-model factories and fn registries are now **slots on the `features` object**, -not a separate `rowModels` option. Factory calls no longer take fn arguments; -pass the fn map as a sibling slot instead. - -| v8 option | v9 `features` slot key | v9 factory call | v9 fn registry slot | -| -------------------------- | ---------------------- | ----------------------------- | ------------------- | -| `getCoreRowModel()` | (automatic) | — | — | -| `getFilteredRowModel()` | `filteredRowModel` | `createFilteredRowModel()` | `filterFns` | -| `getSortedRowModel()` | `sortedRowModel` | `createSortedRowModel()` | `sortFns` | -| `getPaginationRowModel()` | `paginatedRowModel` | `createPaginatedRowModel()` | — | -| `getExpandedRowModel()` | `expandedRowModel` | `createExpandedRowModel()` | — | -| `getGroupedRowModel()` | `groupedRowModel` | `createGroupedRowModel()` | `aggregationFns` | -| `getFacetedRowModel()` | `facetedRowModel` | `createFacetedRowModel()` | — | -| `getFacetedMinMaxValues()` | `facetedMinMaxValues` | `createFacetedMinMaxValues()` | — | -| `getFacetedUniqueValues()` | `facetedUniqueValues` | `createFacetedUniqueValues()` | — | - -## Feature registration table - -| v8 (implicit) | v9 feature import | -| --------------- | ------------------------- | -| filter columns | `columnFilteringFeature` | -| global filter | `globalFilteringFeature` | -| sort rows | `rowSortingFeature` | -| pagination | `rowPaginationFeature` | -| row selection | `rowSelectionFeature` | -| expanding rows | `rowExpandingFeature` | -| pin rows | `rowPinningFeature` | -| pin columns | `columnPinningFeature` | -| hide columns | `columnVisibilityFeature` | -| reorder columns | `columnOrderingFeature` | -| size columns | `columnSizingFeature` | -| resize columns | `columnResizingFeature` | -| group columns | `columnGroupingFeature` | -| facet columns | `columnFacetingFeature` | - -> If you don't want to think about tree-shaking yet, you can pass -> `stockFeatures`: -> -> ```ts -> import { stockFeatures } from '@tanstack/angular-table' -> features: stockFeatures -> ``` -> -> This restores v8-like "everything bundled" behavior — but the v9 bundle -> wins are gone. Plan to migrate to a curated `tableFeatures({...})` as part of -> productionization. - ---- - -## Type generics — every type takes `TFeatures` now - -```txt -v8 v9 ---- --- -Column → Column -ColumnDef → ColumnDef -Table → Table -Row → Row -Cell → Cell -Header → Header -HeaderContext → HeaderContext -CellContext → CellContext -``` - -Easiest fix: extract `typeof features` once. - -```ts -const features = tableFeatures({ rowSortingFeature, columnFilteringFeature }) - -type Features = typeof features - -const columns: Array> = [ - /* … */ -] -``` - -If you're on `stockFeatures`: - -```ts -import type { StockFeatures, ColumnDef } from '@tanstack/angular-table' -const columns: Array> = [ - /* … */ -] -``` - -`ColumnMeta` module augmentation also needs `TFeatures`: - -```ts -declare module '@tanstack/angular-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue, - > { - align?: 'left' | 'right' - } -} -``` - ---- - -## Naming renames — sorting - -| v8 | v9 | -| ----------------------------------- | ------------------------ | -| `sortingFn` (column-def field) | `sortFn` | -| `column.getSortingFn()` | `column.getSortFn()` | -| `column.getAutoSortingFn()` | `column.getAutoSortFn()` | -| `SortingFn` type | `SortFn` | -| `SortingFns` interface | `SortFns` | -| `sortingFns` (built-in fn registry) | `sortFns` | - -Find-and-replace, then verify `sortFns` is a slot on `features` alongside `sortedRowModel: createSortedRowModel()`. - ---- - -## Column sizing vs resizing — split - -v8 combined sizing and resizing into one feature. v9 splits them so you can -ship only what you use. - -| v8 | v9 | -| --------------------------------- | ---------------------------------------------------------- | -| `ColumnSizing` (single feature) | `columnSizingFeature` + `columnResizingFeature` (separate) | -| `columnSizingInfo` state | `columnResizing` state | -| `setColumnSizingInfo()` | `setColumnResizing()` | -| `onColumnSizingInfoChange` option | `onColumnResizingChange` option | - -If you only render fixed-width columns and never drag-to-resize, import only -`columnSizingFeature`. - ---- - -## Pinning option split - -```ts -// v8 -enablePinning: true - -// v9 — explicit -enableColumnPinning: true -enableRowPinning: true -``` - ---- - -## Row API: privates promoted to public - -| v8 | v9 | -| ---------------------------------------- | -------------------------------------- | -| `row._getAllCellsByColumnId()` (private) | `row.getAllCellsByColumnId()` (public) | - -All other `_`-prefixed internal APIs from v8 are removed in v9 — use the -public equivalents. If you were touching `_`-prefixed members, you were on -unsupported territory; v9 forces the fix. - ---- - -## `tableOptions(...)` — new composition helper - -v9 ships `tableOptions(...)` for type-safe partial option composition. Useful -during migration if you have shared base config: - -```ts -import { tableOptions, tableFeatures, rowSortingFeature } from '@tanstack/angular-table' - -const baseOptions = tableOptions({ - features: tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, - }), - debugTable: isDevMode(), -}) - -readonly table = injectTable(() => ({ - ...baseOptions, - columns: this.columns, - data: this.data(), -})) -``` - -And for whole-app patterns, `createTableHook(...)` — see -`tanstack-table/angular/angular-rendering-directives`. - ---- - -## `RowData` type tightened - -`RowData` is now stricter (object-like; no primitives). If you had -`type Row = string`, that won't fly in v9 — wrap it in an object. - ---- - -## Lower-severity failure modes (MEDIUM/LOW) - -### Skipping `stockFeatures` cleanup later - -Using `stockFeatures` is a fine migration shortcut, but it forgoes the v9 -bundle wins. Once everything compiles, swap `stockFeatures` for an explicit -`tableFeatures({...})` listing only the features you use. - -### Forgetting `enablePinning` is gone - -`enablePinning: true` silently does nothing. Use `enableColumnPinning: true` -and/or `enableRowPinning: true`. - -### Forgetting `columnSizingInfo` is gone - -Replace state name `columnSizingInfo` with `columnResizing`, setter -`setColumnSizingInfo` with `setColumnResizing`, handler -`onColumnSizingInfoChange` with `onColumnResizingChange`. And add -`columnResizingFeature` to `features` if you actually need resizing. - -### Single global `onStateChange` ported as a giant per-slice fan-out - -In v9 each slice has its own callback. If you previously used `onStateChange` -to multiplex changes, port to the specific `on[State]Change` callbacks you -actually care about — don't recreate a megaswitch. - -### Hand-rolling `TFeatures` in render-fn types - -When a `cell` / `header` function signature requires `CellContext`, -let `createColumnHelper()` infer it for you. Spelling -features by hand in dozens of render-fn signatures is a sign you should be -using `createAppColumnHelper` from `createTableHook`. - -### Reaching for `_`-prefixed internals because the public method "doesn't exist" - -The "missing" method usually means the feature isn't in `features`. Add the -feature; don't peek at internals. - -### Reimplementing what the table API already does - -Symptoms of v8 muscle memory carrying through: - -- Manually sorting the data signal in `effect(...)` instead of - `table.setSorting(...)` and using `table.getRowModel().rows`. -- Manually paginating the rows array instead of using `paginatedRowModel`. -- Hand-rolling `getSelectedRowModel().flatRows` from a `rowSelection` signal. - -If a v8 escape hatch was needed because of a bug or missing API, check the v9 -public API first — many things were added in v9. - -### `ColumnMeta` augmentation without the `TFeatures` generic - -The interface now takes `TFeatures` first. Old declarations get silently merged -but typed wrong. - -### Importing `flexRender` as a function - -There's no `flexRender(fn, ctx)` in Angular. `FlexRender` is a directive tuple -constant. Imports + template directive form is the only shape. - -### Trying to share v9 atoms with v8 — incompatible - -If you have a partial v8 codebase coexisting, **don't** try to bridge atoms -across the version boundary. Migrate per-component. diff --git a/packages/angular-table/skills/angular/production-readiness/SKILL.md b/packages/angular-table/skills/angular/production-readiness/SKILL.md deleted file mode 100644 index 546d69d3bd..0000000000 --- a/packages/angular-table/skills/angular/production-readiness/SKILL.md +++ /dev/null @@ -1,472 +0,0 @@ ---- -name: angular/production-readiness -description: > - Ship-ready optimizations for Angular Table v9: register only the `features` you actually use - (tree-shake the bundle); keep `columns` / `features` (which carries row-model factories and - fn registries) as stable references OUTSIDE the `injectTable` initializer; pass only the `*Fns` - your data needs as slots on the features object; use `ChangeDetectionStrategy.OnPush`; lean on - signal-backed atoms (`table.atoms..get()`) instead of broad `table.state` reads where - granularity matters; use `{ equal: shallow }` on object/array `computed` selectors; set - `getRowId` for stable identity; track by `id` in every `@for`; defer cell components with - `flexRenderComponent` only when you need its options; scope DI tokens via `[tanStackTable*]` - directives to kill prop drilling. -type: lifecycle -library: tanstack-table -framework: angular -library_version: '9.0.0-alpha.48' -requires: - - angular/table-state - - angular/getting-started - - angular/angular-rendering-directives -sources: - - TanStack/table:docs/framework/angular/angular-table.md - - TanStack/table:docs/framework/angular/guide/table-state.md - - TanStack/table:docs/framework/angular/guide/migrating.md - - TanStack/table:packages/angular-table/src/injectTable.ts - - TanStack/table:packages/angular-table/src/reactivity.ts - - TanStack/table:examples/angular/composable-tables/ ---- - -# Production Readiness (Angular Table v9) - -> Once your table compiles and renders, this is the cost reduction pass. -> Angular's signal-backed adapter makes most of v9's perf "free" if you don't -> fight it — the work is mostly about _what you don't do_: not recreating -> objects, not pulling in features you don't use, not over-wrapping with -> selectors. - ---- - -## 1. Bundle: register only the features you use - -The single biggest v9 win is feature tree-shaking. Every feature you put in -`tableFeatures({...})` pulls in its code; everything you leave out is dropped -by the bundler. - -```ts -// ❌ Pulls in EVERY feature, even unused ones -const features = stockFeatures - -// ✅ Only what this table actually uses -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - columnFilteringFeature, -}) -``` - -Use `stockFeatures` to bootstrap during a v8 → v9 migration, then **come back -and curate**. The bundle wins only land once you do. - -The same applies to feature-fn registries — pass only the `*Fns` your data -needs as slots on the features object: - -```ts -import { - tableFeatures, - createSortedRowModel, - createFilteredRowModel, - sortFns, - filterFns, -} from '@tanstack/angular-table' - -// ❌ pulls in every built-in sort + filter fn -const features = tableFeatures({ - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - sortFns, - filterFns, -}) - -// ✅ only what you use -const features = tableFeatures({ - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - sortFns: { basic: sortFns.basic, datetime: sortFns.datetime }, - filterFns: { includesString: filterFns.includesString }, -}) -``` - -Same logic for `aggregationFns` if you use grouping. - ---- - -## 2. Stable references — keep them OUTSIDE the initializer - -`injectTable(() => ({...}))` **re-runs the initializer every time a signal read -inside it changes** and then calls `table.setOptions({ ...prev, ...new })`. -Anything you create inside the initializer is recreated on every signal -change. Because row-model factories and fn registries live on the `features` -object, keeping `features` stable at module scope covers all of them. - -```ts -// ❌ columns / features recreated on every data() change -@Component({...}) -export class App { - readonly table = injectTable(() => ({ - features: tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns }), - columns: [/* … */], - data: this.data(), - })) -} - -// ✅ stable references outside; only reactive reads inside -const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns }) -const columns: Array> = [/* … */] - -@Component({...}) -export class App { - readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - })) -} -``` - -Same rule for the controlled-state pattern — keep `state: { pagination: this.pagination() }` -inside the initializer, but keep the signal definitions on the class. - -For shared infrastructure across multiple tables, `createTableHook(...)` lets -you define `features` / default options once at module scope. - ---- - -## 3. `ChangeDetectionStrategy.OnPush` everywhere - -Every component that hosts or renders a TanStack Table should be: - -```ts -@Component({ - // ... - changeDetection: ChangeDetectionStrategy.OnPush, -}) -``` - -With signal-backed atoms, OnPush is sufficient — atom reads in the template -are tracked through `computed`, so Angular schedules a check when the signal -changes. Default change detection causes redundant work on every event. - -All `examples/angular/*` use `OnPush`. Match that. - ---- - -## 4. Read narrowly — `table.atoms..get()` over `table.state` - -Both surfaces are signal-backed. The difference is _which signal_ gets read. - -```ts -// Wider — reads through the flat state proxy -const pageIndex = computed(() => this.table.state.pagination.pageIndex) - -// Narrower — depends only on the pagination atom -const pageIndex = computed(() => this.table.atoms.pagination.get().pageIndex) -``` - -For most apps the difference is negligible. For render code, high-frequency -atoms, or deeply-derived components, prefer per-atom reads. Keep `table.state` -for places that genuinely want the full-state shape, such as debug JSON. - ---- - -## 5. `{ equal: shallow }` on object/array `computed` - -When you derive an object or array slice, downstream `effect`s and `computed`s -re-run whenever the reference changes — even if the structural contents -didn't. Use `shallow` from `@tanstack/angular-table` to short-circuit: - -```ts -import { computed } from '@angular/core' -import { shallow } from '@tanstack/angular-table' - -readonly pagination = computed( - () => this.table.atoms.pagination.get(), - { equal: shallow }, -) - -readonly visibleColumns = computed( - () => this.table.atoms.columnVisibility.get(), - { equal: shallow }, -) -``` - -This is **not** about reactivity — atoms are reactive already. This is about -skipping no-op downstream recomputations when the slice rebuilds with the same -values. - -**Don't reach for it on every read.** Reserve it for derived selectors whose -downstream is expensive (effects that hit the server, big template -re-renders). - ---- - -## 6. `track row.id` and `getRowId` - -Always provide a stable identity: - -```ts -readonly table = injectTable(() => ({ - // ... - getRowId: (row) => row.id, // stable primary key -})) -``` - -Then in every `@for`: - -```html -@for (row of table.getRowModel().rows; track row.id) { ... } @for (cell of -row.getVisibleCells(); track cell.id) { ... } @for (header of -headerGroup.headers; track header.id) { ... } -``` - -Without `getRowId`, IDs default to row index — re-rendering the entire row list -on a sort flip, refetch, or pagination move because Angular thinks every row -is new. - ---- - -## 7. Render-cost rules for cells - -Cell render fns run for every visible cell on every re-render. Cheap is the -goal. - -- **Return a primitive when you can.** `cell: (info) => info.getValue()` is - fastest. -- **Return a component class (not a wrapper) when only inputs need wiring.** - The renderer's `KeyValueDiffers` skips `setInput` for unchanged values. -- **Reach for `flexRenderComponent(...)` only for explicit options** — - custom inputs not derived from context, output callbacks, an injector, - Angular v20+ `bindings` / `directives`. -- **Don't put expensive `inject()` calls in render fns.** They run inside - `runInInjectionContext` every render. Inject at the component level and - close over the value. -- **Don't allocate inside render fns when you can avoid it.** Closures, new - array literals, etc. - -### Stable input references - -For object inputs (like `data` arrays you pass into a sub-component), keep the -reference stable across renders. `KeyValueDiffers` is reference-based for -Angular's default input equality, so a `{ ...obj }` literal on every render -defeats it. - ---- - -## 8. Kill prop drilling with DI tokens - -Passing `cell` / `header` / `table` through 2+ component layers is both -ergonomic noise and a perf hazard (each input has its own diffing cost). -Replace with the host directives + inject helpers: - -```html - -``` - -```ts -export class CellActionsComponent { - readonly cell = injectTableCellContext() - // cell() is a Signal>; reads are reactive -} -``` - -Inside `*flexRender*` components, the tokens are auto-provided (no host -directive needed) — see `tanstack-table/angular/angular-rendering-directives` -§7. - ---- - -## 9. Large data — let virtualization do the work - -A 10k-row table is fine in v9 in terms of state, but rendering 10k rows is -slow. **Don't render what's off-screen.** Pair with `@tanstack/angular-virtual` -— see `tanstack-table/angular/compose-with-tanstack-virtual`. - -Also consider: - -- Server-side pagination if data is huge — see - `tanstack-table/angular/client-to-server`. -- `defaultColumn: { size, minSize, maxSize }` to set sane sizing defaults if - you've registered `columnSizingFeature`. - ---- - -## 10. Avoid `effect(...)` for cross-slice sync — write directly - -```ts -// ❌ Effect chain — runs after CD, can layer-cake -effect(() => { - const filter = this.globalFilter() - this.pagination.update((p) => ({ ...p, pageIndex: 0 })) -}) - -// ✅ Reset inline in the on*Change handler -onGlobalFilterChange: (u) => { - typeof u === 'function' - ? this.globalFilter.update(u) - : this.globalFilter.set(u) - this.pagination.update((p) => ({ ...p, pageIndex: 0 })) -} -``` - -The on\*Change handler runs synchronously at the source of truth; an `effect` -runs after Angular's CD pass, which can lead to double renders. - ---- - -## 11. Avoid double-controlling a slice - -Don't supply both `state.x` and `atoms.x` for the same slice — atom wins -silently, the Angular signal becomes a write-only sink, and you've doubled -the wiring cost. Pick exactly one source of truth per slice (see -`tanstack-table/angular/table-state` §6–§7). - ---- - -## 12. Build hygiene - -- **`bundle-stats` / `source-map-explorer`**: after curating `features`, - verify your final bundle doesn't include retired features. If you see - `rowGroupingFeature` in the bundle but never imported it, something is - pulling in `stockFeatures` indirectly. -- **`debugTable: isDevMode()`** — only in dev. Don't leave `debugTable: true` - in production. - ---- - -## 13. Quick wins checklist - -- [ ] `features` listed explicitly (no `stockFeatures` in production). -- [ ] `*Fns` registry slots on `features` pass only what you use (not the full - `sortFns` / `filterFns` / `aggregationFns` spread when you can narrow it). -- [ ] `columns` and `features` are at module scope or stable class fields — - never inside the `injectTable` initializer (factories and fn registries - live on `features`, so this covers them too). -- [ ] Component is `ChangeDetectionStrategy.OnPush`. -- [ ] `getRowId` set when rows have a stable primary key. -- [ ] All `@for` blocks track by `id`. -- [ ] Cell render fns return primitives or component classes when possible; - `flexRenderComponent(...)` reserved for explicit-option cases. -- [ ] Reading state inside `effect`s / heavy `computed`s uses - `table.atoms..get()` (not the flat state proxy). -- [ ] Object/array `computed` selectors that feed expensive downstream use - `{ equal: shallow }`. -- [ ] No `cell` / `header` / `table` inputs drilled through multiple - components — replaced with `injectTableCellContext()` / etc. -- [ ] `debugTable` only in `isDevMode()`. -- [ ] Tables larger than a few hundred visible rows use virtualization. - ---- - -## Failure modes - -### 1. (CRITICAL) Shipping `stockFeatures` to production - -`stockFeatures` defeats the v9 bundle wins. The migrating skill explicitly -calls this out — `stockFeatures` is a v8 → v9 bootstrap, not a production -end-state. - -### 2. (CRITICAL) Recreating `columns` / `features` inside the `injectTable` initializer - -The initializer re-runs on every signal read change. A new `columns` reference -triggers full column-model rebuilds — for big tables this is visibly slow. -Module-scope both `columns` and `features` (which carries the factories and fn -registries). - -### 3. (CRITICAL) Reimplementing what the table already does - -Symptoms: - -- Manual sort on the data array inside a `computed`/`effect`, then passing - the sorted array to the table. -- Manual pagination math driving `data: paged()` — paginated by the user, not - the table. -- Hand-rolled global filter `.filter(...)` inside `effect`. - -All of these are far slower than the built-in row models (which memoize and -short-circuit) and ship more code. Use `table.setSorting(...)`, -`table.setColumnFilters(...)`, and the row-model factories registered on the -`features` object. - -### 4. (HIGH) `OnPush` not set - -Default change detection runs on every event in the entire app. Even with -signal-backed atoms, you're paying for unnecessary template checks. `OnPush` -is the table's idiomatic setting. - -### 5. (HIGH) `@for` without stable `track` - -`@for (row of rows)` without a `track` value at all is a build error in -Angular ≥17 strict mode; `track $index` defeats DOM reuse on sort/refetch. -Always `track row.id` (and `getRowId` on the table). - -### 6. (HIGH) Over-wrapping every read in `computed(...)` - -```ts -// ❌ adds a computed layer for no reason -readonly pagination = computed(() => this.table.atoms.pagination.get()) -``` - -The atom is already signal-backed. Use `computed` for derivation, custom -equality, or shared selectors — not for "make it reactive." - -### 7. (HIGH) `{ equal: shallow }` on every `computed` - -Shallow equality has a runtime cost (one pass over keys). For primitive -selectors it's strictly slower than `Object.is`. Reserve it for derived -object/array slices whose downstream is expensive. - -### 8. (HIGH) Drilling `cell` / `header` / `table` through multiple - -components - -Inputs add diffing cost on every change-detection cycle. Replace with the -`[tanStackTableCell]` / `[tanStackTableHeader]` / `[tanStackTable]` host -directives and `injectTableCellContext()` / etc. at the leaf. - -### 9. (HIGH) `flexRenderComponent(...)` for every cell - -`flexRenderComponent` adds a wrapper with `reflectComponentType` overhead -and an `OutputEmitterRef` subscription scan. For plain component pass-through -where context inputs cover everything, **return the component class -directly** — the renderer does `setInput` on its own. - -### 10. (MEDIUM) `effect(...)` chains for what should be on\*Change inline - -If the user changes `globalFilter` and your `pagination` reset lives in an -`effect`, you get a CD pass for the filter and a second one for the reset. -Inline the reset in `onGlobalFilterChange`. - -### 11. (MEDIUM) Forgetting `autoResetPageIndex: false` for server-driven tables - -Every fetch produces a new array reference, which triggers the default -auto-reset and bounces the user back to page 0 mid-pagination. See -`tanstack-table/angular/client-to-server` §9. - -### 12. (MEDIUM) `debugTable: true` left in production - -Turns on per-operation `console.info` logging from the core. Use -`debugTable: isDevMode()`. - -### 13. (MEDIUM) Reaching for `Subscribe` patterns ported from React docs - -Angular doesn't need a `Subscribe` boundary the way React does. The -adapter's signal binding handles fine-grained reactivity at the atom level — -templates re-evaluate the dependencies they actually read. - ---- - -## See also - -- `tanstack-table/angular/getting-started` — the first-table baseline -- `tanstack-table/angular/table-state` — narrow vs wide reads, controlled state -- `tanstack-table/angular/angular-rendering-directives` — `flexRenderComponent`, - DI tokens -- `tanstack-table/angular/client-to-server` — server-driven optimizations -- `tanstack-table/angular/compose-with-tanstack-virtual` — virtualizing big - tables -- Example: `examples/angular/composable-tables/` — `createTableHook` for - app-wide infrastructure diff --git a/packages/angular-table/skills/angular/table-state/SKILL.md b/packages/angular-table/skills/angular/table-state/SKILL.md deleted file mode 100644 index 91e8aa4616..0000000000 --- a/packages/angular-table/skills/angular/table-state/SKILL.md +++ /dev/null @@ -1,422 +0,0 @@ ---- -name: angular/table-state -description: > - TanStack Table v9 state ownership in Angular: signal-backed atoms via `angularReactivity`, - the `injectTable(() => ({...}))` lazy initializer pattern, reading `table.atoms..get()` - inside templates / `computed(...)` / `effect(...)`, `shallow` for object slices, controlled state - with Angular signals + `state` + `on[State]Change`, and when to reach for external TanStack Store - atoms instead. Required reading before any other Angular Table v9 skill. -type: framework -library: tanstack-table -framework: angular -library_version: '9.0.0-alpha.48' -requires: - - state-management - - setup -sources: - - TanStack/table:docs/framework/angular/angular-table.md - - TanStack/table:docs/framework/angular/guide/table-state.md - - TanStack/table:docs/framework/angular/guide/migrating.md - - TanStack/table:packages/angular-table/src/injectTable.ts - - TanStack/table:packages/angular-table/src/reactivity.ts - - TanStack/table:packages/angular-table/src/lazySignalInitializer.ts - - TanStack/table:examples/angular/basic-inject-table/ - - TanStack/table:examples/angular/row-selection-signal/ ---- - -# Angular Table State (v9) - -> **TanStack Table is a state-management coordinator.** v9 rebuilt that coordinator -> on top of TanStack Store (`alien-signals`). In Angular, the adapter bridges those -> atoms to native Angular signals, so reading `table.atoms..get()` from a -> template, `computed(...)`, or `effect(...)` participates in Angular reactivity. -> Prefer that direct atom read when you need a specific state slice. -> -> This is the prerequisite for every other Angular Table skill. Don't skip it. - ---- - -## 1. Prerequisites — `features` decides what state exists - -In v9, **a state slice only exists if its feature is registered in `features`**. -This is the #1 v9-specific gotcha and the root cause of many "missing API" -TypeScript errors. - -```ts -import { - injectTable, - tableFeatures, - rowPaginationFeature, - rowSortingFeature, - createPaginatedRowModel, - createSortedRowModel, - sortFns, -} from '@tanstack/angular-table' - -// Declare features OUTSIDE the initializer (see §2 below) -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), -})) - -this.table.atoms.pagination.get() // ✅ -this.table.atoms.sorting.get() // ✅ -// this.table.atoms.rowSelection // ❌ TS error — rowSelectionFeature not registered -``` - -If you see `Property 'atoms.rowSelection' does not exist` or -`table.toggleRowSelected is not a function`, **add the feature to `features`** — -don't reach for `@ts-ignore`, don't reimplement the API, don't switch to -`stockFeatures` until you understand which features you actually need. - -`tableFeatures({})` (empty) is valid — you get the core row model only. - ---- - -## 2. The `injectTable` lazy-initializer model - -`injectTable` is the v9 entrypoint (replacing v8's `createAngularTable`). It must -run inside an Angular injection context (a component constructor / class field). - -```ts -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), -})) -``` - -### The initializer is a `computed`-like function - -The initializer runs **whenever any Angular signal read inside it changes**. The -adapter then calls `table.setOptions({ ...previous, ...newOptions })` to sync. -That means: - -- **Reactive values that should re-sync the table** (`this.data()`, controlled - state signals) go _inside_ the initializer. -- **Stable references** (`columns`, `features`) go _outside_ — or you'll - recreate the column model on every data update. - -```ts -// ❌ WRONG — columns + features recreated on every data change -readonly table = injectTable(() => ({ - features: tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns }), - columns: [/* … */], - data: this.data(), -})) - -// ✅ Stable references outside, signal reads inside -const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns }) -const columns: Array> = [/* … */] - -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), // ← only the signal read should be inside -})) -``` - -### The returned table is signal-reactive too - -The `table` returned by `injectTable` exposes APIs that read signal-backed atoms -internally, so calling `table.getRowModel()`, `table.getSelectedRowModel()`, -`table.atoms.pagination.get()`, etc. inside templates / `computed` / `effect` -_just works_ — no manual subscriptions. - ---- - -## 3. The three state surfaces - -A table instance has three ways to look at its state: - -| Surface | Shape | Use when | -| ------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------- | -| `table.baseAtoms.` | writable TanStack Store atom (always exists for registered slices) | low-level direct write; rare | -| `table.atoms.` | **readonly** derived atom per registered feature; backed by Angular `computed` | reading current value or driving reactivity | -| `table.state` | flat proxy object over every registered slice | full-state JSON/debug output | - -All three are signal-backed in Angular. Reading any of them inside a template, -`computed(...)`, or `effect(...)` registers an Angular dependency. For normal -render code, prefer `table.atoms..get()` so the read is explicit and -limited to the slice the component needs. Use `table.state` when you actually -need the flat full-state shape, such as `JSON.stringify(table.state, null, 2)`. - -```ts -// Read current value (anywhere) -const pagination = this.table.atoms.pagination.get() - -// Same value through the flat proxy; use mainly for full-state debug output -const pagination2 = this.table.state.pagination - -// Reactive derivation with custom equality -import { computed } from '@angular/core' -import { shallow } from '@tanstack/angular-table' - -readonly pageIndex = computed( - () => this.table.atoms.pagination.get().pageIndex, -) - -readonly pagination = computed( - () => this.table.atoms.pagination.get(), - { equal: shallow }, // structural equality — skip downstream work on no-op updates -) -``` - -### When do I need `computed(...)`? - -You **don't need `computed`** just to make an atom reactive. The atom is already -signal-backed. Use `computed(...)` only when you want: - -1. **Derivation** — `computed(() => this.table.atoms.pagination.get().pageIndex)` -2. **Custom equality** — `{ equal: shallow }` on object/array slices, so - downstream `effect`s skip no-op updates when the slice is recreated with the - same values. -3. **Caching of an expensive transformation** that reads from multiple atoms. - -For plain reads in a template, `{{ table.atoms.pagination.get().pageIndex }}` -is fine. - ---- - -## 4. Setting state — use feature APIs, not direct writes - -TanStack Table exposes a method for nearly every state transition. **Use those -methods.** Don't reimplement what's already in the public API — that's the #1 -tell of AI-generated table code. - -```ts -// ✅ Use the API -this.table.setPageIndex(0) -this.table.nextPage() -this.table.setPageSize(25) -this.table.setSorting([{ id: 'age', desc: true }]) -this.table.setColumnFilters([{ id: 'status', value: 'active' }]) -this.table.toggleAllRowsSelected(true) -this.table.resetSorting() -this.table.resetPagination() -this.table.resetPagination(true) // reset to feature default, not initialState -row.toggleSelected() -column.toggleVisibility() -column.toggleSorting() - -// ❌ Don't write to atoms directly unless you really have to -this.table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 })) -``` - -Direct `baseAtoms` writes bypass `on[State]Change` handlers and won't update -externally owned state — if you've controlled the slice with an Angular signal, -you must update the signal, not the base atom. - ---- - -## 5. Setting starting values — `initialState` - -`initialState` is the single right place to seed registered slices. It is also -the value that reset APIs reset to. - -```ts -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - initialState: { - sorting: [{ id: 'age', desc: true }], - pagination: { pageIndex: 0, pageSize: 25 }, - }, -})) - -// Later -this.table.resetSorting() // → initialState.sorting -this.table.resetSorting(true) // → feature default ([]) -``` - -`initialState` only applies to slices whose feature is registered. Mutating -`initialState` after construction does **not** re-seed state — use it for -starting values only. - ---- - -## 6. Controlled state — the recommended Angular pattern - -Most Angular Table apps that need cross-component access to a state slice use -**Angular signals + `state` + `on[State]Change`**. This keeps ownership in -Angular's signal model while `injectTable` keeps the table in sync. - -```ts -import { signal } from '@angular/core' -import { - injectTable, - rowPaginationFeature, - rowSortingFeature, - tableFeatures, - type PaginationState, - type SortingState, -} from '@tanstack/angular-table' - -const features = tableFeatures({ rowPaginationFeature, rowSortingFeature }) - -export class Component { - readonly data = signal>([]) - readonly sorting = signal([]) - readonly pagination = signal({ pageIndex: 0, pageSize: 10 }) - - readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - state: { - sorting: this.sorting(), - pagination: this.pagination(), - }, - onSortingChange: (updater) => { - updater instanceof Function - ? this.sorting.update(updater) - : this.sorting.set(updater) - }, - onPaginationChange: (updater) => { - updater instanceof Function - ? this.pagination.update(updater) - : this.pagination.set(updater) - }, - })) -} -``` - -### `on[State]Change` rules - -- **Always pass an updater-or-value handler.** TanStack Table calls - `on[State]Change(updaterOrValue)` where `updaterOrValue` is either a new value - or `(old) => new` — check with `instanceof Function` / `typeof === 'function'`. -- **Pair `on[State]Change` with `state.`.** Providing - `onPaginationChange` without `state.pagination` will result in your callback - firing but the table reading its own internal atom — confusing. -- **The v8 `onStateChange` (single global handler) is gone in v9.** Slices are - controlled individually. - -### Don't double-own a slice - -For any given slice, exactly one of these should be the source of truth: - -- `initialState.` (uncontrolled, internal) -- `state.` + `on[State]Change` (controlled by Angular signal) -- `atoms.` (controlled by external TanStack Store atom — see §7) - -If you supply both `state.x` and `atoms.x`, the external atom wins silently. If -you supply both `initialState.x` and `state.x`, `state.x` wins. Pick one. - ---- - -## 7. Beyond signals: external atoms, state types, app-wide hooks - -For most Angular apps, **signals + `state` + `on[State]Change` (§6) is the -right ownership model.** When you need more, see -[`references/external-atoms-and-app-hook.md`](references/external-atoms-and-app-hook.md): - -- **External TanStack Store atoms** — `atoms: { pagination: paginationAtom }` - for slices owned by `@tanstack/store` / `@tanstack/angular-store`, when - multiple non-table parts of the app share the slice. -- **State type imports** — `PaginationState`, `SortingState`, - `RowSelectionState`, `TableState`, etc. -- **`createTableHook(...)`** — app-wide `injectAppTable` / - `createAppColumnHelper` that pre-bind `features` (which now carries row-model - factories and fn registries). Also exposes `tableComponents` / `cellComponents` / - `headerComponents` registries (covered in `angular-rendering-directives`). - ---- - -## Failure modes - -### 1. (CRITICAL) Hallucinating v8 `createAngularTable` / pre-v9 APIs - -```ts -// ❌ v8 — does not exist in v9 -import { createAngularTable, getCoreRowModel } from '@tanstack/angular-table' -const table = createAngularTable(() => ({ - columns, - data: data(), - getCoreRowModel: getCoreRowModel(), -})) - -// ✅ v9 -import { injectTable, tableFeatures } from '@tanstack/angular-table' -const features = tableFeatures({}) -const table = injectTable(() => ({ - features, - columns, - data: data(), -})) -``` - -Also retired: `getFilteredRowModel`, `getSortedRowModel`, `getPaginationRowModel` -as top-level options → migrated to slots on the `features` object -(`filteredRowModel: createFilteredRowModel()`, `sortedRowModel: createSortedRowModel()`, -`paginatedRowModel: createPaginatedRowModel()`) with fn registries (`filterFns`, `sortFns`) also on `features`. - -### 2. (CRITICAL) Missing API because feature not in `features` - -`table.atoms.rowSelection`, `table.toggleAllRowsSelected`, -`row.getCanSelect`, `column.getCanSort` etc. are **only** present when the -matching feature is in `features`. The fix is to add the feature, not to -patch around it. - -### 3. (CRITICAL) Reimplementing built-in state transitions - -```ts -// ❌ DON'T -this.pagination.update((p) => ({ ...p, pageIndex: p.pageIndex + 1 })) - -// ✅ -this.table.nextPage() -``` - -Same for `setPageIndex`, `setPageSize`, `setSorting`, `toggleSorting`, -`setColumnFilters`, `setGlobalFilter`, `toggleSelected`, `toggleAllRowsSelected`, -`setColumnVisibility`, `setColumnOrder`, `setExpanded`, `toggleExpanded`, -`resetSorting`, `resetPagination`, `resetRowSelection`, etc. - -### 4. (HIGH) Expensive values declared **inside** the `injectTable` initializer - -Because the initializer re-runs when any reactive read inside it changes, -declaring `columns` or `features` inside the function causes them to be -recreated and re-applied on every data update (row-model factories and fn -registries are part of `features`, so moving `features` outside covers all of -them). Move them outside the class or to stable class fields. - -### 5. (HIGH) Forgetting that the initializer re-runs - -If you `console.log` inside the `injectTable` initializer, you'll see it fire -multiple times during the component lifetime — that's correct. The adapter -handles the diff and calls `table.setOptions`. Don't kick off side-effects from -inside the initializer; put them in an `effect(...)` reading the relevant -atoms. - -Lower-severity failure modes (MEDIUM/LOW: `state.x` vs `atoms.x` conflict, -updater-fn handling in `on[State]Change`, in-place mutation of state values, -premature `computed` wrapping) → -[`references/external-atoms-and-app-hook.md`](references/external-atoms-and-app-hook.md#lower-severity-failure-modes-mediumlow). - ---- - -## References - -- [External TanStack Store atoms, state types, `createTableHook` setup, and MEDIUM failure modes](references/external-atoms-and-app-hook.md) - ---- - -## See also - -- `tanstack-table/angular/getting-started` — end-to-end first table -- `tanstack-table/angular/angular-rendering-directives` — `*flexRender*`, DI tokens, `flexRenderComponent` -- `tanstack-table/angular/migrate-v8-to-v9` — v8 → v9 mechanical mapping -- `tanstack-table/angular/compose-with-tanstack-store` — external atoms in depth -- `tanstack-table/angular/client-to-server` — controlled state for server-driven tables -- `tanstack-table/core/state-management` — framework-agnostic atom model diff --git a/packages/angular-table/skills/angular/table-state/references/external-atoms-and-app-hook.md b/packages/angular-table/skills/angular/table-state/references/external-atoms-and-app-hook.md deleted file mode 100644 index b891bece76..0000000000 --- a/packages/angular-table/skills/angular/table-state/references/external-atoms-and-app-hook.md +++ /dev/null @@ -1,153 +0,0 @@ -# External Atoms & `createTableHook` — Full Reference - -Extended ownership options beyond signals + `state` + `on[State]Change`. - ---- - -## External TanStack Store atoms — alternative ownership - -The core `atoms` table option is re-exported from Angular Table. Use it when: - -- You already have a `Store` / `Atom` from `@tanstack/store` or - `@tanstack/angular-store` that should drive a table slice. -- Multiple non-table parts of the app need to read/write the slice through the - same atom (e.g. URL sync, persistence, multi-table coordination). - -**For most Angular apps, prefer signals + `state` first.** Atoms are the -cross-app sharing alternative, not the default. See -`compose-with-tanstack-store` for the full pattern. - -```ts -import { Store } from '@tanstack/store' - -const paginationAtom = new Store({ pageIndex: 0, pageSize: 10 }) - -readonly table = injectTable(() => ({ - features, - columns, - data: this.data(), - atoms: { - pagination: paginationAtom, // external atom owns the slice - }, -})) -``` - -When `atoms.pagination` is registered, `table.atoms.pagination.get()` reads from -the external atom (not the internal base atom). `table.setPagination(...)` -writes through the external atom's setter. - ---- - -## State types - -Import slice types directly from `@tanstack/angular-table`: - -```ts -import type { - PaginationState, - SortingState, - RowSelectionState, - ColumnFiltersState, - GlobalFilterTableState, - ColumnVisibilityState, - ColumnOrderState, - ColumnPinningState, - ColumnSizingState, - ColumnResizingState, - ExpandedState, - GroupingState, - TableState, -} from '@tanstack/angular-table' - -// TableState is inferred from registered features -type MyTableState = TableState -``` - ---- - -## `createTableHook` — app-wide table infrastructure - -When multiple tables in an app share the same `features` object and -component conventions, factor them into a `createTableHook(...)` call once and -import the resulting `injectAppTable` / `createAppColumnHelper`. - -```ts -// app-table.ts -import { - createTableHook, - tableFeatures, - rowSortingFeature, - rowPaginationFeature, - createSortedRowModel, - createPaginatedRowModel, - sortFns, -} from '@tanstack/angular-table' - -export const { - injectAppTable, - createAppColumnHelper, - injectTableContext, - injectTableCellContext, - injectTableHeaderContext, -} = createTableHook({ - features: tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, - }), - getRowId: (row) => row.id, -}) -``` - -Then in components: - -```ts -import { injectAppTable, createAppColumnHelper } from './app-table' - -const columnHelper = createAppColumnHelper() // only TData generic needed - -readonly table = injectAppTable(() => ({ - columns, - data: this.data(), -})) // features (including row-model factories) inherited -``` - -`createTableHook` also lets you register `tableComponents` / `cellComponents` / -`headerComponents` registries — see `angular-rendering-directives` for that. - ---- - -## Lower-severity failure modes (MEDIUM/LOW) - -### Using `state.x` and `atoms.x` together for the same slice - -External atom silently wins. If you intended Angular-signal ownership, remove -`atoms.x` (or vice versa). Pick exactly one source of truth per slice. - -### Forgetting the updater function form in `on[State]Change` - -```ts -// ❌ Crashes when TanStack Table passes a function -onSortingChange: (value) => this.sorting.set(value) - -// ✅ -onSortingChange: (updater) => { - updater instanceof Function - ? this.sorting.update(updater) - : this.sorting.set(updater) -} -``` - -### Mutating `state` slice values in place - -`state: { sorting: this.sorting() }` snapshots the current value. Mutating the -underlying signal value with `.push()` won't trigger re-evaluation. Always -produce a new reference: `this.sorting.update(s => [...s, newSort])`. - -### Premature `computed` selector wrapping for a single read - -For a single atom read in a template (`{{ table.atoms.pagination.get().pageIndex }}`), -a wrapper `computed` adds no value. Reach for `computed` only for derivation, -shared selectors, or `{ equal: shallow }`. diff --git a/packages/angular-table/skills/create-table-hook/SKILL.md b/packages/angular-table/skills/create-table-hook/SKILL.md new file mode 100644 index 0000000000..1a347929b0 --- /dev/null +++ b/packages/angular-table/skills/create-table-hook/SKILL.md @@ -0,0 +1,148 @@ +--- +name: create-table-hook +description: > + Create an Angular injectAppTable/createAppColumnHelper abstraction with shared features/defaults, registered components, injectTableContext/injectTableCellContext/injectTableHeaderContext, and correct FlexRender component-versus-function handling. +metadata: + type: framework + library: '@tanstack/angular-table' + framework: angular + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/angular/guide/composable-tables.md' + - 'TanStack/table:examples/angular/composable-tables' + - 'TanStack/table:packages/angular-table/src/helpers/createTableHook.ts' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Use it for repeated app conventions; keep one-off tables on `injectTable`. + +## Setup + +```ts +import { + createTableHook, + rowSortingFeature, + tableFeatures, +} from '@tanstack/angular-table' + +export const { + injectAppTable, + createAppColumnHelper, + injectTableContext, + injectTableCellContext, + injectTableHeaderContext, +} = createTableHook({ + features: tableFeatures({ rowSortingFeature }), +}) +``` + +```ts +const helper = createAppColumnHelper() +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) + +export class PeopleTable { + readonly table = injectAppTable(() => ({ columns, data: this.data() })) +} +``` + +## Core Patterns + +### Consume typed DI context in registered components + +```ts +export class NameCell { + readonly cell = injectTableCellContext() + readonly value = computed(() => this.cell().getValue()) +} +``` + +Use the matching table/header/cell injector instead of threading context props through reusable components. + +### Distinguish component types from render functions + +Register either an Angular component type or a render function. Return component types directly when FlexRender should set context inputs; use `flexRenderComponent(Component, options)` only for explicit inputs/outputs/injector/bindings/directives. + +## Common Mistakes + +### CRITICAL Calling app helpers outside DI + +Wrong: + +```ts +export function make() { + return injectAppTable(() => options) +} +``` + +Correct: + +```ts +export class PeopleTable { + readonly table = injectAppTable(() => options()) +} +``` + +Both app-table construction and returned context injectors require Angular injection context. + +Source: `packages/angular-table/src/helpers/createTableHook.ts` + +### HIGH Prop drilling registered context + +Wrong: + +```ts +cell: (ctx) => flexRenderComponent(NameCell, { inputs: { cell: ctx.cell } }) +``` + +Correct: + +```ts +cell: (ctx) => ctx.cell.NameCell +``` + +Registered components can consume the app hook’s typed cell context directly. + +Source: `docs/framework/angular/guide/composable-tables.md` + +### HIGH Wrapping a function as a component + +Wrong: + +```ts +flexRenderComponent((props) => props.cell.getValue()) +``` + +Correct: + +```ts +;(props) => props.cell.getValue() +``` + +`flexRenderComponent` validates an Angular component type; plain render functions are already valid render content. + +Source: `packages/angular-table/src/flex-render/flexRenderComponent.ts` + +### MEDIUM Abstracting a one-off table + +Wrong: + +```ts +const hook = createTableHook({ features: tableFeatures({}) }) +``` + +Correct: + +```ts +readonly table = injectTable(() => ({ features, columns, data: this.data() })) +``` + +Use the app hook only when shared defaults, types, or registered components justify it. + +Source: `docs/framework/angular/guide/composable-tables.md` + +## API Discovery + +Inspect `node_modules/@tanstack/angular-table/src/helpers/createTableHook.ts`, `helpers/{table,cell,header}.ts`, and `flex-render/` for exact DI and rendering contracts. diff --git a/packages/angular-table/skills/getting-started/SKILL.md b/packages/angular-table/skills/getting-started/SKILL.md new file mode 100644 index 0000000000..1e21f768b5 --- /dev/null +++ b/packages/angular-table/skills/getting-started/SKILL.md @@ -0,0 +1,138 @@ +--- +name: getting-started +description: > + Create an Angular TanStack Table v9 table with injectTable inside injection context, explicit stable tableFeatures and columns, signal-backed data, and FlexRender structural directives or helpers. +metadata: + type: framework + library: '@tanstack/angular-table' + framework: angular + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - '@tanstack/table-core#table-features' +sources: + - 'TanStack/table:docs/framework/angular/guide/migrating.md' + - 'TanStack/table:docs/framework/angular/guide/rendering.md' + - 'TanStack/table:examples/angular/basic-inject-table' + - 'TanStack/table:packages/angular-table/src/index.ts' +--- + +This skill builds on `@tanstack/table-core#core` and `@tanstack/table-core#table-features`. Read them first for the headless model and explicit features. + +## Setup + +```ts +import { Component, signal } from '@angular/core' +import { FlexRender, injectTable, tableFeatures } from '@tanstack/angular-table' + +type Person = { name: string; age: number } +const features = tableFeatures({}) +const columns = [ + { accessorKey: 'name', header: 'Name' }, + { accessorKey: 'age', header: 'Age' }, +] + +@Component({ + selector: 'app-table', + imports: [FlexRender], + template: `
- {{ value }} - @switch (header.column.getIsSorted()) { @case ('asc') { ▲ } @case ('desc') { ▼ - } } - - {{ rendered }} -{{ value }}{{ value }}{{ value }} - {{ value }} - - -
+ + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + + } + + } + +
+ {{ + value + }} +
`, +}) +export class TableComponent { + readonly data = signal([{ name: 'Ada', age: 36 }]) + readonly table = injectTable(() => ({ features, columns, data: this.data() })) +} +``` + +## Core Patterns + +### Keep static inputs outside the initializer + +`injectTable` reruns its options initializer when a signal read changes. Define features, row-model factories, and columns at module or stable class scope; read only changing values inside. + +### Render each content kind correctly + +Import `FlexRender` for `*flexRender`, `*flexRenderCell`, `*flexRenderHeader`, and `*flexRenderFooter`. Definitions may yield primitives, `TemplateRef`, component types, or `flexRenderComponent(...)`; Table does not supply markup or CSS. + +## Common Mistakes + +### CRITICAL Calling injectTable outside DI + +Wrong: + +```ts +export function makeTable() { + return injectTable(() => ({ features, columns, data })) +} +``` + +Correct: + +```ts +export class TableComponent { + readonly table = injectTable(() => ({ features, columns, data: this.data() })) +} +``` + +`injectTable` asserts an Angular injection context and registers lifecycle cleanup there. + +Source: `packages/angular-table/src/injectTable.ts` + +### HIGH Reallocating static options reactively + +Wrong: + +```ts +injectTable(() => ({ + features: tableFeatures({}), + columns: makeColumns(), + data: this.data(), +})) +``` + +Correct: + +```ts +const features = tableFeatures({}) +const columns = makeColumns() +injectTable(() => ({ features, columns, data: this.data() })) +``` + +Every signal change reruns the initializer; rebuilding static inputs invalidates memoized Table work. + +Source: `packages/angular-table/src/injectTable.ts` + +### HIGH Treating a render function as a component + +Wrong: + +```ts +cell: () => flexRenderComponent(() => 'value') +``` + +Correct: + +```ts +cell: () => 'value' +``` + +`flexRenderComponent` wraps an Angular component type; ordinary functions and primitives are handled directly by FlexRender. + +Source: `docs/framework/angular/guide/rendering.md` + +## API Discovery + +Inspect `node_modules/@tanstack/angular-table/src/index.ts`, `injectTable.ts`, `flexRender.ts`, and `flex-render/`; inspect optional feature APIs in installed `@tanstack/table-core/src/features/`. diff --git a/packages/angular-table/skills/migrate-v8-to-v9/SKILL.md b/packages/angular-table/skills/migrate-v8-to-v9/SKILL.md new file mode 100644 index 0000000000..d2facbd13a --- /dev/null +++ b/packages/angular-table/skills/migrate-v8-to-v9/SKILL.md @@ -0,0 +1,194 @@ +--- +name: migrate-v8-to-v9 +description: > + Complete Angular v8-to-v9 migration reference: injectTable and injection context, explicit features and row-model slots, signal/atom state, FlexRender directives, type generics, prototype methods, sorting, sizing, selection, and logical pinning. +metadata: + type: lifecycle + library: '@tanstack/angular-table' + framework: angular + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#migrate-v8-to-v9' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/angular/guide/migrating.md' + - 'TanStack/table:packages/angular-table/src/index.ts' + - 'TanStack/table:examples/angular/basic-inject-table' +--- + +Use this as the complete breaking-change checklist. V9 is the current API; construction, feature registration, state, rendering, and types must migrate together. + +Framework prerequisite: Angular 19 or newer (`@angular/core >=19`). + +## Recommended Migration Order + +1. Replace `createAngularTable` with `injectTable` inside an Angular injection context. +2. Hoist static/expensive features and columns outside the reactive initializer. +3. Move features, row models, and function registries into `tableFeatures`. +4. Update signal/atom state reads and FlexRender usage. +5. Apply every shared API and type rename below. +6. Treat `stockFeatures` as a temporary audit bridge; explicit features are the production target. + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +class TableCmp { + readonly table = injectTable(() => ({ + features, + columns, + data: this.data(), + })) +} +``` + +## Construction and Feature Registration + +| v8 | v9 | +| ----------------------------------- | ---------------------------------------------------------- | +| `createAngularTable(() => options)` | `injectTable(() => options)` in injection context | +| All features bundled | Required `features: tableFeatures({...})` | +| `getCoreRowModel()` option | Remove; core row model is automatic | +| `get*RowModel()` table options | `create*RowModel()` slots in `tableFeatures` | +| `sortingFns` table option | `sortFns` feature slot | +| Top-level `onStateChange` | Per-slice callbacks, external atoms, or store subscription | + +The initializer reruns when signals read inside it change and calls `setOptions`; do not rebuild columns or features there. + +Feature imports are `columnFilteringFeature`, `globalFilteringFeature`, `rowSortingFeature`, `rowPaginationFeature`, `rowSelectionFeature`, `rowExpandingFeature`, `rowPinningFeature`, `columnPinningFeature`, `columnVisibilityFeature`, `columnOrderingFeature`, `columnSizingFeature`, `columnResizingFeature`, `columnGroupingFeature`, and `columnFacetingFeature`. APIs are feature-gated. Put a feature before its dependent slot in the same `tableFeatures` call. + +### Row-model mapping + +| v8 option | v9 slot and factory | +| -------------------------- | ------------------------------------------------------------------- | +| `getFilteredRowModel()` | `filteredRowModel: createFilteredRowModel()` after column filtering | +| `getSortedRowModel()` | `sortedRowModel: createSortedRowModel()` after row sorting | +| `getPaginationRowModel()` | `paginatedRowModel: createPaginatedRowModel()` after pagination | +| `getExpandedRowModel()` | `expandedRowModel: createExpandedRowModel()` after expanding | +| `getGroupedRowModel()` | `groupedRowModel: createGroupedRowModel()` after grouping | +| `getFacetedRowModel()` | `facetedRowModel: createFacetedRowModel()` after faceting | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues: createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues: createFacetedUniqueValues()` | + +Factories take no arguments. `filterFns`, `sortFns`, and `aggregationFns` are sibling feature slots. + +## Angular State Migration + +- `table.getState().sorting` becomes `table.atoms.sorting.get()` for narrow signal-backed reads. +- Use `table.store.get()` only for a full flat snapshot/debug output. +- Derive selected slices with Angular `computed`; use `shallow` equality for recreated object/array slices when appropriate. +- Controlled Angular signals are read in `state` and updated through matching `on[State]Change` callbacks; resolve value-or-function updaters. +- Top-level `onStateChange` is removed. Use per-slice callbacks, external atoms, or `table.store.subscribe` for all changes. +- Prefer external atoms from `@tanstack/angular-store` through `atoms` for app-owned shared slices. Never provide both an atom and `state` for one slice. +- Treat `table.baseAtoms` as internal; prefer feature APIs or external atoms. + +## Angular Rendering and Composition + +- Import `FlexRender`/the current `*flexRender` directives from the adapter. +- Prefer `*flexRenderCell="cell; let value"`, `*flexRenderHeader="header; let value"`, and `*flexRenderFooter="footer; let value"`; they choose the definition and context automatically. +- General `*flexRender` supports primitives, `TemplateRef`, component types, and `flexRenderComponent(...)` wrappers. +- Column render functions run in an Angular injection context and may call `inject()` or use signals. +- Components mounted by FlexRender can call `injectFlexRenderContext()` for the render props. +- Use `flexRenderComponent(Component, { inputs, outputs, injector, bindings, directives })` for explicit component configuration; creation-time `bindings`/`directives` require the supported Angular version. +- `tableOptions(...)` composes partial options and may omit data, columns, or features until final assembly. +- `createTableHook` is optional for repeated application conventions; it returns `injectAppTable` and a feature-bound `createAppColumnHelper`. + +## Complete Shared Breaking-Change Map + +### Instance methods + +Row, cell, column, header, and related methods now live on shared prototypes and use `this`. Call them on their instances. Do not destructure/pass them bare or expect them in object spread, `Object.keys`, or JSON. Table methods are not affected. + +### Logical column pinning + +Beta.38 has no physical aliases. + +| old | new | +| -------------------------------------------------------------- | ------------------------------------------------------------- | +| `columnPinning.left` / `.right` | `.start` / `.end` | +| `column.pin('left' \| 'right')` | `column.pin('start' \| 'end')` | +| `getIsPinned() === 'left' \| 'right'` | `'start' \| 'end'` | +| `row.getLeftVisibleCells()` / `getRightVisibleCells()` | `getStartVisibleCells()` / `getEndVisibleCells()` | +| `getLeftHeaderGroups()` / `getRightHeaderGroups()` | `getStartHeaderGroups()` / `getEndHeaderGroups()` | +| `getLeftFooterGroups()` / `getRightFooterGroups()` | `getStartFooterGroups()` / `getEndFooterGroups()` | +| `getLeftFlatHeaders()` / `getRightFlatHeaders()` | `getStartFlatHeaders()` / `getEndFlatHeaders()` | +| `getLeftLeafHeaders()` / `getRightLeafHeaders()` | `getStartLeafHeaders()` / `getEndLeafHeaders()` | +| `getLeftLeafColumns()` / `getRightLeafColumns()` | `getStartLeafColumns()` / `getEndLeafColumns()` | +| `getLeftVisibleLeafColumns()` / `getRightVisibleLeafColumns()` | `getStartVisibleLeafColumns()` / `getEndVisibleLeafColumns()` | +| `getLeftTotalSize()` / `getRightTotalSize()` | `getStartTotalSize()` / `getEndTotalSize()` | +| `column.getStart('left')` | `column.getStart('start')` | +| `column.getAfter('right')` | `column.getAfter('end')` | +| `column.getIndex('left' \| 'right')` | `column.getIndex('start' \| 'end')` | + +Prefer CSS logical inset properties. Logical names do not set DOM direction. `columnResizeDirection` is unchanged. + +### Pinning, sizing, and resizing + +- `enablePinning` splits into `enableColumnPinning` and `enableRowPinning`. +- Interactive resizing requires `columnSizingFeature` and `columnResizingFeature`; fixed sizing needs only sizing. +- `columnSizingInfo` becomes `columnResizing`. +- `setColumnSizingInfo()` becomes `setColumnResizing()`. +- `onColumnSizingInfoChange` becomes `onColumnResizingChange`. + +### Sorting, rows, and selection + +| v8 | v9 | +| ------------------------------ | ----------------------------- | +| `sortingFn` | `sortFn` | +| `sortingFns` | `sortFns` | +| `getSortingFn()` | `getSortFn()` | +| `getAutoSortingFn()` | `getAutoSortFn()` | +| `SortingFn` / `SortingFns` | `SortFn` / `SortFns` | +| `row._getAllCellsByColumnId()` | `row.getAllCellsByColumnId()` | + +Other `_`-prefixed internals are removed, including `_getPinnedRows`, `_getFacetedRowModel`, `_getFacetedMinMaxValues`, and `_getFacetedUniqueValues`. + +`getIsSomeRowsSelected()` and `getIsSomePageRowsSelected()` mean at least one, including all. Use `getIsSomeRowsSelected() && !getIsAllRowsSelected()` or `getIsSomePageRowsSelected() && !getIsAllPageRowsSelected()` for indeterminate UI. + +## TypeScript Migration + +- Most types add `TFeatures` first: `Column`, `ColumnDef`, `Table`, `Row`, and `Cell`. +- Replace `createColumnHelper()` with `createColumnHelper()`; use `columnHelper.columns([...])` for inference. +- A `createTableHook` column helper already binds features and needs only ``. +- Use `StockFeatures` when using `stockFeatures`. +- Existing `TableMeta`/`ColumnMeta` declaration merging must add `TFeatures` first. Prefer per-table meta slots using `metaHelper`. +- Replace global `FilterFns`, `SortFns`, `AggregationFns`, and `FilterMeta` augmentation with registry slots and `filterMeta`; registered keys become typed string references. +- `RowData` is now `Record | Array` rather than `unknown`. + +## Common Migration Failures + +### CRITICAL: Calling injectTable outside injection context + +Create it in a component/directive/service field initializer or another valid Angular injection context so ownership and cleanup bind correctly. + +### HIGH: Rebuilding static inputs reactively + +Hoist `features` and `columns`; the initializer reruns for tracked signals. + +### HIGH: Leaving row models on table options + +Move each row model beside its prerequisite feature in `tableFeatures`. + +### HIGH: Destructuring instance methods + +Use `row.getValue('name')`; prototype methods require the original instance and are absent from shallow clones. + +## Final Checklist + +- [ ] `createAngularTable` is replaced by `injectTable` in injection context. +- [ ] Static inputs are stable outside the signal-tracked initializer. +- [ ] Features, row models, and registries are in `tableFeatures`; core row model is removed. +- [ ] State reads use atom-backed signals or store intentionally; `onStateChange` is gone. +- [ ] Controlled state and external atom ownership do not overlap. +- [ ] FlexRender directives/helpers are migrated and imported. +- [ ] Prototype methods, pinning, sizing/resizing, sorting, row, and selection changes are audited. +- [ ] Helpers, types, meta, registries, and `RowData` use v9 shapes. +- [ ] Temporary `stockFeatures` usage has an explicit removal plan. + +## API Discovery + +Inspect `node_modules/@tanstack/angular-table/src/index.ts`, `injectTable.ts`, and rendering sources. Verify feature slots and exact beta APIs in `node_modules/@tanstack/table-core/src`; do not reconstruct v9 from v8 memory. diff --git a/packages/angular-table/skills/table-state/SKILL.md b/packages/angular-table/skills/table-state/SKILL.md new file mode 100644 index 0000000000..90eafda25d --- /dev/null +++ b/packages/angular-table/skills/table-state/SKILL.md @@ -0,0 +1,197 @@ +--- +name: table-state +description: > + Use Angular-signal-backed table.atoms, direct template reads, computed selectors, controlled signals, value-or-updater callbacks, and external Angular Store atoms while accounting for injectTable initializer reruns. +metadata: + type: framework + library: '@tanstack/angular-table' + framework: angular + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - getting-started +sources: + - 'TanStack/table:docs/framework/angular/guide/table-state.md' + - 'TanStack/table:examples/angular/basic-external-state' + - 'TanStack/table:packages/angular-table/src/injectTable.ts' +--- + +This skill builds on `@tanstack/table-core#core` and `getting-started`. Read them first for state ownership and Angular 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` combines those atoms into one readonly flat store. + +The Angular adapter backs atoms with Angular signals. Reads participate in tracking inside templates, `computed`, and `effect`; signal reads inside the `injectTable` initializer also rerun that initializer and call `setOptions`. State is feature-based: missing pagination state or APIs indicate a missing `rowPaginationFeature`. Hoist stable `features` and `columns` outside the initializer, and return signal-backed `data` without mapping or slicing inline. + +## Setup + +```ts +import { computed, signal } from '@angular/core' +import { + injectTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowPaginationFeature }) +const columns = [{ accessorKey: 'name' }] + +export class TableComponent { + readonly data = signal([{ name: 'Ada' }]) + readonly table = injectTable(() => ({ features, columns, data: this.data() })) + readonly pageIndex = computed( + () => this.table.atoms.pagination.get().pageIndex, + ) +} +``` + +Angular-backed table atom reads are signal reads. Templates, `computed`, and `effect` track them directly. + +## Core Patterns + +### Prefer external atoms for cross-system state + +```ts +import { createAtom } from '@tanstack/angular-store' +import type { PaginationState } from '@tanstack/angular-table' + +readonly paginationAtom = createAtom({ pageIndex: 0, pageSize: 20 }) +readonly table = injectTable(() => ({ + features, columns, data: this.data(), atoms: { pagination: this.paginationAtom }, +})) +``` + +The atom can feed Query without making every state write rerun the Table initializer. + +### Resolve controlled signal updaters + +```ts +readonly pagination = signal({ pageIndex: 0, pageSize: 20 }) +readonly table = injectTable(() => ({ + features, columns, data: this.data(), state: { pagination: this.pagination() }, + onPaginationChange: next => typeof next === 'function' ? this.pagination.update(next) : this.pagination.set(next), +})) +``` + +## Choose State Ownership + +Use one owner for each slice: + +- Prefer internal state and feature APIs for local interaction. +- Use `initialState` for starting/reset values; later changes do not reset current state. +- Prefer a stable atom from `@tanstack/angular-store` in `atoms` for Query or other cross-system state. Table APIs update it without a change callback. +- Use an Angular signal read in `state.` plus the matching callback for simple controlled state. Handle raw values and updater functions. + +External atoms win over controlled `state`, which syncs into the internal base atom. Do not give a slice two owners. The global v8 `onStateChange` option is gone; subscribe to `table.store` when all state changes must be observed. + +## Initialize, Update, and Reset + +Prefer feature methods such as `setSorting`, `nextPage`, `toggleVisibility`, and `toggleSelected`. Direct base-atom writes are a rare escape hatch for internal state; write the external atom when it owns a slice. + +```ts +this.table.resetSorting() +this.table.resetPagination() +this.table.resetPagination(true) +``` + +Feature resets use `table.initialState` unless `true` requests the feature default, and can update external owners. Core `table.reset()` resets internal base atoms only. Use feature-specific types such as `PaginationState`; use `TableState` when the complete registered state type is needed. + +## Common Mistakes + +### MEDIUM Wrapping atoms redundantly + +Wrong: + +```ts +readonly pagination = computed(() => computed(() => this.table.atoms.pagination.get())()) +``` + +Correct: + +```ts +readonly pagination = computed(() => this.table.atoms.pagination.get()) +``` + +Table atoms already bridge to Angular signals; one tracked read is sufficient. + +Source: `docs/framework/angular/guide/table-state.md` + +### HIGH Ignoring initializer reruns + +Wrong: + +```ts +injectTable(() => ({ + features: tableFeatures({ rowPaginationFeature }), + columns: makeColumns(), + state: { pagination: this.pagination() }, + data, +})) +``` + +Correct: + +```ts +injectTable(() => ({ + features, + columns, + state: { pagination: this.pagination() }, + data, +})) +``` + +Controlled signal writes rerun the initializer, so static work must remain outside it. + +Source: `packages/angular-table/src/injectTable.ts` + +### HIGH Storing updater functions + +Wrong: + +```ts +onPaginationChange: (next) => this.pagination.set(next) +``` + +Correct: + +```ts +onPaginationChange: (next) => + typeof next === 'function' + ? this.pagination.update(next) + : this.pagination.set(next) +``` + +Callbacks receive a value or updater; assigning the function corrupts owned state. + +Source: `examples/angular/basic-external-state/src/app/app.ts` + +### MEDIUM Giving one slice multiple owners + +Wrong: + +```ts +{ initialState: { pagination: start }, atoms: { pagination: this.paginationAtom } } +``` + +Correct: + +```ts +{ + atoms: { + pagination: this.paginationAtom + } +} +``` + +External atoms/state override initial state; choose one owner for each slice. + +Source: `docs/framework/angular/guide/table-state.md` + +## API Discovery + +Inspect `node_modules/@tanstack/angular-table/src/injectTable.ts` and `reactivity.ts`; inspect `@tanstack/angular-store/src` for external atoms and installed core feature source for state APIs. diff --git a/packages/angular-table/skills/with-tanstack-query/SKILL.md b/packages/angular-table/skills/with-tanstack-query/SKILL.md new file mode 100644 index 0000000000..90a14d249a --- /dev/null +++ b/packages/angular-table/skills/with-tanstack-query/SKILL.md @@ -0,0 +1,142 @@ +--- +name: with-tanstack-query +description: > + Compose Angular Query with signal-owned Table filtering, sorting, and pagination state using reactive query options, manual row-model boundaries, direct query data, server counts, and valid injection context. +metadata: + type: composition + library: '@tanstack/angular-table' + framework: angular + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#client-vs-server' + - getting-started + - table-state +sources: + - 'TanStack/table:examples/angular/with-tanstack-query' + - 'TanStack/table:docs/framework/angular/guide/table-state.md' + - 'TanStack/table:docs/framework/angular/guide/pagination.md' +--- + +This skill builds on `@tanstack/table-core#client-vs-server`, `getting-started`, and `table-state`. Decide the server-owned stages and dataset before wiring Query. + +## Setup + +```ts +import { + injectQuery, + keepPreviousData, +} from '@tanstack/angular-query-experimental' +import { signal } from '@angular/core' +import { + injectTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/angular-table' + +const features = tableFeatures({ rowPaginationFeature }) +const EMPTY_ROWS: never[] = [] + +export class PeopleTable { + readonly pagination = signal({ pageIndex: 0, pageSize: 20 }) + readonly query = injectQuery(() => ({ + queryKey: [ + 'people', + this.pagination().pageIndex, + this.pagination().pageSize, + ], + queryFn: () => + fetch( + `/api/people?page=${this.pagination().pageIndex}&size=${this.pagination().pageSize}`, + ).then((r) => r.json()), + placeholderData: keepPreviousData, + })) + readonly table = injectTable(() => ({ + features, + columns, + data: this.query.data()?.rows ?? EMPTY_ROWS, + rowCount: this.query.data()?.rowCount ?? 0, + manualPagination: true, + state: { pagination: this.pagination() }, + onPaginationChange: (next) => + typeof next === 'function' + ? this.pagination.update(next) + : this.pagination.set(next), + })) +} +``` + +## Core Patterns + +### Track every server-owned input + +Read pagination, sorting, and filtering signals inside `injectQuery(() => ...)` and include them in the query key. Return data already processed for each manual stage. + +### Keep Query data authoritative + +Read the Query signal directly in `injectTable`. Create another signal only for a deliberate editable draft with an explicit cache synchronization policy. + +## Common Mistakes + +### HIGH Capturing query inputs outside tracking + +Wrong: + +```ts +const page = this.pagination().pageIndex +readonly query = injectQuery(() => ({ queryKey: ['people', page], queryFn })) +``` + +Correct: + +```ts +readonly query = injectQuery(() => ({ queryKey: ['people', this.pagination().pageIndex], queryFn })) +``` + +Signal reads inside the options function establish refetch dependencies. + +Source: `examples/angular/with-tanstack-query/src/app/app.ts` + +### HIGH Expecting manual mode to request + +Wrong: + +```ts +injectTable(() => ({ features, columns, data, manualPagination: true })) +``` + +Correct: + +```ts +injectTable(() => ({ + features, + columns, + data: this.query.data()?.rows ?? EMPTY_ROWS, + manualPagination: true, +})) +``` + +Manual flags only bypass client row processing; Query performs network work. + +Source: `examples/angular/with-tanstack-query/src/app/app.ts` + +### HIGH Omitting total counts + +Wrong: + +```ts +{ data: this.query.data()?.rows ?? EMPTY_ROWS, manualPagination: true } +``` + +Correct: + +```ts +{ data: this.query.data()?.rows ?? EMPTY_ROWS, rowCount: this.query.data()?.rowCount ?? 0, manualPagination: true } +``` + +Table needs `rowCount` or `pageCount` to constrain navigation across server pages. + +Source: `docs/framework/angular/guide/pagination.md` + +## API Discovery + +Inspect installed `@tanstack/angular-table/src/injectTable.ts`, the relevant core feature source, and installed Angular Query source for the exact `injectQuery` package/version contract. diff --git a/packages/angular-table/skills/with-tanstack-virtual/SKILL.md b/packages/angular-table/skills/with-tanstack-virtual/SKILL.md new file mode 100644 index 0000000000..b404256d95 --- /dev/null +++ b/packages/angular-table/skills/with-tanstack-virtual/SKILL.md @@ -0,0 +1,126 @@ +--- +name: with-tanstack-virtual +description: > + Virtualize Angular Table final row or column models inside the correct injection and reactive lifecycle with signal counts, scroll elements, keys, measurement, transforms, sticky regions, grid/flex sizing, and infinite data. +metadata: + type: composition + library: '@tanstack/angular-table' + framework: angular + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/angular/guide/virtualization.md' + - 'TanStack/table:examples/angular/virtualized-rows' + - 'TanStack/table:examples/angular/virtualized-columns' + - 'TanStack/table:examples/angular/virtualized-infinite-scrolling' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Virtual is a rendering concern over final Table models, not a feature plugin. + +## Setup + +```ts +import { computed, viewChild } from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' +import type { ElementRef } from '@angular/core' + +export class VirtualTable { + readonly scrollElement = + viewChild>('scrollElement') + readonly rows = computed(() => this.table.getRowModel().rows) + readonly rowVirtualizer = injectVirtualizer(() => ({ + count: this.rows().length, + scrollElement: this.scrollElement()?.nativeElement, + estimateSize: () => 34, + getItemKey: (index) => this.rows()[index]!.id, + overscan: 5, + })) + readonly virtualRows = computed(() => this.rowVirtualizer.getVirtualItems()) + readonly totalSize = computed(() => this.rowVirtualizer.getTotalSize()) +} +``` + +## Core Patterns + +### Derive reactive models + +Use `computed(() => table.getRowModel().rows)` and `computed(() => table.getVisibleLeafColumns())`. `injectVirtualizer` tracks signal reads in its initializer and requires injection context. + +### Own the layout contract + +Give the scroll element bounded overflow, create a total-size spacer, and translate or measure each virtual item. Use grid/flex widths for dynamic rows/columns and keep sticky headers inside the correct scroll geometry. + +### Gate infinite fetches + +When the last virtual item approaches fetched length, fetch only if more server rows exist and a request is not active. Manual sorting requires server-sorted pages and a reset/refetch policy. + +## Common Mistakes + +### CRITICAL Constructing outside injection context + +Wrong: + +```ts +export function virtualize(options) { + return injectVirtualizer(() => options) +} +``` + +Correct: + +```ts +export class VirtualTable { + readonly virtualizer = injectVirtualizer(() => options()) +} +``` + +The Angular virtualizer follows DI lifecycle rules just like `injectTable`. + +Source: `examples/angular/virtualized-rows/src/app/app.ts` + +### HIGH Virtualizing raw data + +Wrong: + +```ts +readonly rows = computed(() => this.data()) +``` + +Correct: + +```ts +readonly rows = computed(() => this.table.getRowModel().rows) +``` + +Raw data bypasses Table filtering, sorting, expansion, grouping, and pagination. + +Source: `docs/framework/angular/guide/virtualization.md` + +### HIGH Forgetting spacer and transforms + +Wrong: + +```html +@for (item of virtualRows(); track item.key) { +
{{ rows()[item.index].id }}
+} +``` + +Correct: + +```html +
+
+
+``` + +Virtual computes ranges and sizes but does not apply DOM geometry, sticky CSS, or column widths. + +Source: `examples/angular/virtualized-columns/src/app/app.ts` + +## API Discovery + +Inspect installed `@tanstack/angular-table/src`, installed `@tanstack/angular-virtual/src`, and the maintained Angular examples for current row, column, measurement, and infinite patterns. diff --git a/packages/lit-table/skills/create-table-hook/SKILL.md b/packages/lit-table/skills/create-table-hook/SKILL.md new file mode 100644 index 0000000000..fcf02cd990 --- /dev/null +++ b/packages/lit-table/skills/create-table-hook/SKILL.md @@ -0,0 +1,94 @@ +--- +name: create-table-hook +description: > + Create a reusable Lit useAppTable/createAppColumnHelper layer with host-backed controllers, shared features/defaults, typed cell/header renderers, App wrappers, and useTableContext for custom-element controls. Load when multiple Lit tables share infrastructure or prop drilling obscures table context. +metadata: + type: framework + library: '@tanstack/lit-table' + framework: lit + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/lit/guide/composable-tables.md' + - 'TanStack/table:examples/lit/composable-tables' + - 'TanStack/table:packages/lit-table/src/createTableHook.ts' +--- + +This skill builds on @tanstack/table-core#core plus this package's getting-started and table-state skills. + +## Setup + +```ts +import { + createSortedRowModel, + createTableHook, + rowSortingFeature, + tableFeatures, +} from '@tanstack/lit-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) + +export const { createAppColumnHelper, useAppTable, useTableContext } = + createTableHook({ + features, + getRowId: (row: { id: string }) => row.id, + }) +``` + +Use `createAppColumnHelper()` to define app columns. In a `LitElement`, capture the host in a local variable and initialize `useAppTable(host, options, selector)` once as a field; call the returned `table()` function during render. The maintained composable-tables example shows the complete host/getter shape. + +## Core Patterns + +### Bind shared features, not table-specific data + +Put feature plugins, row-model factories, default options, row IDs, and shared render conventions in the factory. Pass each table's columns, data, and controlled state to `useAppTable`. + +### Add component registries only for real conventions + +Register common cell/header functions when multiple tables use them. Render them through `table.AppCell` and `table.AppHeader`; ordinary tables can use the returned table instance without registries. + +### Consume table context in custom elements + +Table-level controls can call the returned `useTableContext(this)` from a custom element. This preserves the factory's feature and data types without prop drilling. + +## Common Mistakes + +### MEDIUM Factory used for one table + +Wrong: add an app hook and registries for a single isolated table. + +Correct: use `TableController` directly until features, defaults, or UI conventions genuinely repeat. + +The factory adds an application abstraction; it does not replace the simpler standalone path. + +Source: TanStack/table:docs/framework/lit/guide/composable-tables.md + +### HIGH Prop drilling replaces typed context + +Wrong: pass the stable table instance through every custom-element property boundary. + +Correct: call the `useTableContext` returned by the same `createTableHook` in the nearest registered/custom control. + +The returned context is bound to the factory's exact features and components. + +Source: TanStack/table:packages/lit-table/src/createTableHook.ts + +### HIGH Recreating useAppTable each render + +Wrong: call `useAppTable(this, options)` afresh inside `render`. + +Correct: initialize it once as a host field and call `this.appTable.table()` during render. + +The helper owns a TableController and context provider tied to the host lifecycle. + +Source: TanStack/table:examples/lit/composable-tables + +## API Discovery + +Inspect `node_modules/@tanstack/lit-table/src/createTableHook.ts`. Use the matching installed implementation rather than assuming JSX-adapter component APIs exist in Lit. diff --git a/packages/lit-table/skills/getting-started/SKILL.md b/packages/lit-table/skills/getting-started/SKILL.md new file mode 100644 index 0000000000..4ca95f66f8 --- /dev/null +++ b/packages/lit-table/skills/getting-started/SKILL.md @@ -0,0 +1,154 @@ +--- +name: getting-started +description: > + Create a TanStack Lit Table v9 table with a stable TableController host field, explicit tableFeatures, controller.table(options, selector) during render, and headless Lit templates. Load for first-table setup, TableController lifecycle, FlexRender, or adapting a React example to Lit. +metadata: + type: framework + library: '@tanstack/lit-table' + framework: lit + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - '@tanstack/table-core#table-features' +sources: + - 'TanStack/table:docs/framework/lit/guide/migrating.md' + - 'TanStack/table:examples/lit/basic-table-controller' + - 'TanStack/table:packages/lit-table/src/index.ts' +--- + +This skill builds on @tanstack/table-core#core and @tanstack/table-core#table-features. Read them first for the headless and feature-plugin model. + +## Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { repeat } from 'lit/directives/repeat.js' +import { + FlexRender, + TableController, + createColumnHelper, + tableFeatures, +} from '@tanstack/lit-table' + +type Person = { id: string; name: string } + +const features = tableFeatures({}) +const columnHelper = createColumnHelper() +const columns = columnHelper.columns([ + columnHelper.accessor('name', { header: 'Name' }), +]) + +@customElement('people-table') +export class PeopleTable extends LitElement { + @state() private people: Array = [{ id: '1', name: 'Ada' }] + private tableController = new TableController(this) + + protected render() { + const table = this.tableController.table({ + features, + columns, + data: this.people, + getRowId: (row) => row.id, + }) + + return html` + + ${repeat( + table.getHeaderGroups(), + (group) => group.id, + (group) => + html` + ${repeat( + group.headers, + (header) => header.id, + (header) => + html``, + )} + `, + )} + + + ${repeat( + table.getRowModel().rows, + (row) => row.id, + (row) => + html` + ${repeat( + row.getAllCells(), + (cell) => cell.id, + (cell) => html``, + )} + `, + )} + +
+ ${header.isPlaceholder ? null : FlexRender({ header })} +
${FlexRender({ cell })}
` + } +} +``` + +## Core Patterns + +### Keep static table infrastructure outside render + +Create `features`, column helpers, and static columns at module scope. Keep one `TableController` as a host field; call its `table` method during each render with current options. + +### Select only state the host renders + +```ts +const table = this.tableController.table( + { features, columns, data: this.people }, + (state) => ({ pagination: state.pagination }), +) +``` + +Use the default selector for simple tables. Narrow it only when host updates are measurably expensive. + +### Treat markup and CSS as application code + +Table supplies models and render values. Use semantic elements, accessibility behavior, widths, sticky positioning, and design-system components in the Lit template. + +## Common Mistakes + +### HIGH Recreating the controller during render + +Wrong: + +```ts +protected render() { + const controller = new TableController(this) + return html`${controller.table({ features, columns, data: this.people }).getRowModel().rows.length}` +} +``` + +Correct: keep `private tableController = new TableController(this)` as a class field and reuse it. + +Each controller registers with the host and owns subscriptions; recreating it leaks lifecycle work and loses stable table state. + +Source: TanStack/table:packages/lit-table/src/TableController.ts + +### HIGH Passing v8 options to the constructor + +Wrong: `new TableController(this, () => ({ data, columns }))`. + +Correct: construct with the host only, then call `this.tableController.table({ features, data, columns })` during render. + +The v9 controller receives current options through `table`, not a constructor thunk. + +Source: TanStack/table:docs/framework/lit/guide/migrating.md + +### HIGH Expecting feature state to render UI + +Wrong: enable column pinning and assume cells become sticky. + +Correct: render the appropriate start/center/end collections and apply sticky offsets and CSS in the template. + +TanStack Table is headless; state and models never inject markup or styles. + +Source: TanStack/table:docs/overview.md + +## API Discovery + +Inspect `node_modules/@tanstack/lit-table/src/index.ts` and the exported implementation. Core table and feature APIs are in `node_modules/@tanstack/table-core/src/`. diff --git a/packages/lit-table/skills/lit/compose-with-tanstack-virtual/SKILL.md b/packages/lit-table/skills/lit/compose-with-tanstack-virtual/SKILL.md deleted file mode 100644 index 9d4e385e43..0000000000 --- a/packages/lit-table/skills/lit/compose-with-tanstack-virtual/SKILL.md +++ /dev/null @@ -1,267 +0,0 @@ ---- -name: lit/compose-with-tanstack-virtual -description: > - TanStack Table does NOT include virtualization — pair with - `@tanstack/lit-virtual`. The standard pattern: get the row array from - `table.getRowModel().rows`, construct a `VirtualizerController(host, opts)` - alongside `TableController`, feed `rows.length` as the virtualizer count - inside `render()`, and render only `virtualizer.getVirtualItems()` with each - row absolutely positioned via `transform: translateY(...)`. Routing keywords: - lit-virtual, VirtualizerController, virtualization, virtualized-rows, lit - table. -type: composition -library: tanstack-table -framework: lit -library_version: '9.0.0-alpha.48' -requires: - - lit/table-state - - row-expanding -sources: - - TanStack/table:docs/guide/virtualization.md - - TanStack/table:examples/lit/virtualized-rows/src/main.ts - - TanStack/table:examples/react/virtualized-rows/ - - TanStack/table:examples/react/virtualized-columns/ ---- - -> **Maintainer note:** the Lit adapter is scheduled for a rewrite alongside TanStack Lit Store during the v9 beta cycle. APIs in this skill may change in a future beta. The patterns below match `9.0.0-alpha.48`. - -TanStack Table is headless — it does not virtualize rows or columns. For long lists, pair the table with `@tanstack/lit-virtual`, which ships `VirtualizerController` — a `ReactiveController` like `TableController`. - -## Install - -```bash -npm install @tanstack/lit-table @tanstack/lit-virtual -``` - -## The Pattern (Row Virtualization) - -1. Build the table with `TableController` as usual. -2. Construct a `VirtualizerController(host, opts)` once. Capture a `Ref` for the scroll element. -3. Inside `render()`, get `rows = table.getRowModel().rows`, then call `virtualizer.setOptions({ ..., count: rows.length })`. -4. Render only `virtualizer.getVirtualItems()`. Each virtual row is absolutely positioned via `transform: translateY(${item.start}px)`. -5. Attach `ref` on each row to call `virtualizer.measureElement(...)` for dynamic sizing. - -```ts -import { LitElement, html } from 'lit' -import { customElement, state } from 'lit/decorators.js' -import { repeat } from 'lit/directives/repeat.js' -import { styleMap } from 'lit/directives/style-map.js' -import { Ref, createRef, ref } from 'lit/directives/ref.js' -import { VirtualizerController } from '@tanstack/lit-virtual' -import { - FlexRender, - TableController, - columnSizingFeature, - createSortedRowModel, - rowSortingFeature, - sortFns, - tableFeatures, - type ColumnDef, -} from '@tanstack/lit-table' - -const features = tableFeatures({ - columnSizingFeature, - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -const columns: Array> = [ - { accessorKey: 'id', header: 'ID', size: 60 }, - { - accessorKey: 'firstName', - header: 'First', - cell: (info) => info.getValue(), - }, - // … -] - -@customElement('virtualized-table') -class VirtualizedTable extends LitElement { - @state() - private _data: Person[] = makeData(50_000) - - private tableController = new TableController(this) - private rowVirtualizerController!: VirtualizerController - private tableContainerRef: Ref = createRef() - - connectedCallback() { - this.rowVirtualizerController = new VirtualizerController(this, { - count: this._data.length, - getScrollElement: () => this.tableContainerRef.value!, - estimateSize: () => 33, - overscan: 5, - }) - super.connectedCallback() - } - - protected render() { - const table = this.tableController.table( - { - features, - columns, - data: this._data, - }, - () => ({}), - ) - - const { rows } = table.getRowModel() - - // Sync the virtualizer count with the post-feature rows. - const virtualizer = this.rowVirtualizerController.getVirtualizer() - virtualizer.setOptions({ ...virtualizer.options, count: rows.length }) - - return html` -
- - - ${repeat( - table.getHeaderGroups(), - (hg) => hg.id, - (hg) => html` - - ${repeat( - hg.headers, - (h) => h.id, - (h) => html` - - `, - )} - - `, - )} - - - ${repeat( - virtualizer.getVirtualItems(), - (item) => item.key, - (item) => { - const row = rows[item.index] - return html` - virtualizer.measureElement(node ?? null))} - > - ${repeat( - row.getAllCells(), - (c) => c.id, - (cell) => html` - - `, - )} - - ` - }, - )} - -
- ${FlexRender({ header: h })} -
- ${FlexRender({ cell })} -
-
- ` - } -} -``` - -Source: `examples/lit/virtualized-rows/src/main.ts`. - -## Column Virtualization - -Same shape, but the virtualizer's `count` is `columns.length` (or visible columns) and you index visible columns inside each row. Useful for wide kitchen-sink tables. The horizontal virtualizer's options include `horizontal: true`. - -## With Pagination / Filtering - -Always use `table.getRowModel().rows.length` as the count — that's the post-feature row array (sorted, filtered, paginated). The virtualizer should never wrap the raw `data` array. - -## With `@tanstack/lit-table`'s `subscribe` - -`subscribe` is an [async Lit directive](https://lit.dev/docs/api/custom-directives/#AsyncDirective) that enables fine-grained reactivity by updating only when the selected part of the store or atom changes. Rather than triggering a full re-render whenever the table state updates, you can use `subscribe` to watch specific slices of state and update only the affected parts of the template. This reduces unnecessary renders and improves performance for large tables or complex UI hierarchies. -Source: `packages/lit-table/src/subscribe-directive.ts`. - -## Common Mistakes - -### CRITICAL Reimplementing virtualization by hand - -Wrong: manual slicing + intersection observers + per-row offset math. - -Correct: use `@tanstack/lit-virtual`'s `VirtualizerController`. It handles measurement, overscan, scroll alignment, and dynamic sizing. -Source: `docs/guide/virtualization.md`. - -### HIGH Using the wrong row source - -Wrong: - -```ts -new VirtualizerController(this, { count: this._data.length /* … */ }) -``` - -Correct: - -```ts -const { rows } = table.getRowModel() -virtualizer.setOptions({ ...virtualizer.options, count: rows.length }) -``` - -Always count post-feature rows, not raw data. -Source: `examples/lit/virtualized-rows/src/main.ts`. - -### HIGH Constructing `VirtualizerController` inside `render()` - -Wrong: new controller per frame. - -Correct: construct once (typically in `connectedCallback`) and call `setOptions` per render to sync `count`. -Source: `examples/lit/virtualized-rows/src/main.ts` (lines 77–85). - -### HIGH Forgetting `position: relative` on the scroll parent / `position: absolute` on rows - -Wrong: rows stack at the top because there's no positioned ancestor with the total height. - -Correct: scroll parent uses `position: relative`, `
- - ${repeat( - table.getHeaderGroups(), - (hg) => hg.id, - (hg) => html` - - ${repeat( - hg.headers, - (h) => h.id, - (h) => html` - - `, - )} - - `, - )} - - - ${repeat( - table.getRowModel().rows, - (r) => r.id, - (row) => html` - - ${repeat( - row.getAllCells(), - (c) => c.id, - (cell) => html` `, - )} - - `, - )} - - - ${repeat( - table.getFooterGroups(), - (fg) => fg.id, - (fg) => html` - - ${repeat( - fg.headers, - (h) => h.id, - (h) => html` - - `, - )} - - `, - )} - -
- ${h.isPlaceholder ? null : FlexRender({ header: h })} -
${FlexRender({ cell })}
- ${h.isPlaceholder ? null : FlexRender({ footer: h })} -
- ` - } -} -``` - -Source: `examples/lit/basic-table-controller/src/main.ts` (lines 53–162). - -## Step 5 — Drive features with feature APIs - -```ts - - -``` - -For starting values, use `initialState`. For controlled slices, use `atoms` or `state` + `on*Change` — see `tanstack-table/lit/table-state`. - -## Common Mistakes - -### CRITICAL `new TableController(this)` inside `render()` - -Wrong: - -```ts -protected render() { - const controller = new TableController(this) // every frame - const table = controller.table({ /* … */ }) -} -``` - -Correct: - -```ts -private tableController = new TableController(this) // once per host -``` - -A new controller per render registers a new subscription and resets table state every frame. -Source: `packages/lit-table/src/TableController.ts`. - -### CRITICAL Calling a feature API when the feature is not in `features` - -Wrong: - -```ts -const features = tableFeatures({}) // no rowPaginationFeature -const table = this.tableController.table({ - features, - columns, - data: this.data, -}) -table.setPageIndex(0) // TypeScript error AND runtime no-op -``` - -Correct: - -```ts -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), -}) -const table = this.tableController.table({ - features, - columns, - data: this.data, -}) -``` - -v9 generates feature APIs and state slices only for registered features. #1 v9 trap. -Source: `docs/guide/features.md`. - -### HIGH Forgetting the matching row-model factory slot - -Wrong: - -```ts -const features = tableFeatures({ rowSortingFeature }) // no sortedRowModel slot -const table = this.tableController.table({ features /* … */ }) -table.setSorting([{ id: 'age', desc: true }]) -// rows are NOT sorted — no sortedRowModel registered -``` - -Correct: - -```ts -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = this.tableController.table({ - features, - /* … */ -}) -``` - -### HIGH Building `features` / `columns` / `data` inside `render()` - -Wrong: re-creating these every frame busts internal memos. - -Correct: `features` and `columns` at module scope; `data` from a `@state()` field on the element. -Source: `docs/framework/lit/guide/table-state.md` (FAQ #1). - -### HIGH Reimplementing built-in feature logic - -Wrong: hand-rolled sorting / filtering / pagination outside the table. - -Correct: register the feature + factory and use feature APIs. v9 ships built-ins for sorting, filtering, pagination, grouping, expanding, faceting, row selection, column visibility/order/pinning/sizing, and row pinning. -Source: `docs/guide/features.md`. - -### MEDIUM Using v8 hook names (`useReactTable`, `getCoreRowModel`, `flexRender(def, ctx)`) - -Wrong: v8 imports from a Lit context. - -Correct: `@tanstack/lit-table` exposes `TableController` + `FlexRender({ cell | header | footer })`. There is no `useReactTable` here. See `tanstack-table/lit/migrate-v8-to-v9`. - -## See Also - -- `tanstack-table/lit/table-state` — atoms, Subscribe, createTableHook. -- `tanstack-table/lit/lit-table-controller` — controller lifecycle in depth. -- `tanstack-table/lit/migrate-v8-to-v9` — moving an existing v8 codebase over. diff --git a/packages/lit-table/skills/lit/lit-table-controller/SKILL.md b/packages/lit-table/skills/lit/lit-table-controller/SKILL.md deleted file mode 100644 index f7867542bb..0000000000 --- a/packages/lit-table/skills/lit/lit-table-controller/SKILL.md +++ /dev/null @@ -1,311 +0,0 @@ ---- -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 `coreReactivityFeature`, 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/subscribe-directive.ts - - TanStack/table:packages/lit-table/src/reactivity.ts - - TanStack/table:examples/lit/basic-table-controller/src/main.ts ---- - -> **Maintainer note:** the Lit adapter now runs on `@tanstack/lit-store`. `TableController` still wires host invalidation to the full `table.store`/`table.optionsStore`; the new `table.subscribe(source, selector?, template)` directive (replacing the old `table.Subscribe` helper) is what narrows re-rendering to a single template region. Further changes may land during the v9 beta cycle. - -`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 - -```ts -export class TableController implements ReactiveController { - constructor(host: ReactiveControllerHost) { - ;(this.host = host).addController(this) // registers controller on host - } - - public table(tableOptions, selector?) { - if (!this._table) { - // First call: build the core table with the Lit reactivity bindings. - this._table = constructTable({ - ...tableOptions, - features: { - coreReactivityFeature: litReactivity(), - ...tableOptions.features, - }, - mergeOptions: (def, next) => ({ ...def, ...next }), - }) - this._setupSubscriptions() - } - // Subsequent calls: merge new options. - this._table.setOptions((prev) => ({ ...prev, ...tableOptions })) - - return { - /* ...this._table, Subscribe, FlexRender, get state() {...} */ - } - } - - 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: - -1. **One core table per controller.** The first `.table(options)` call constructs it; later calls merge options into the same instance. -2. **Two subscriptions:** `table.store` (state) and `table.optionsStore` (options). Both call `host.requestUpdate()`. -3. **Subscriptions are torn down on `hostDisconnected`** and reset on `hostConnected`. -4. **The host re-renders on any store change.** `TableController` itself does not split invalidation by source. To narrow re-rendering to a single template region, wrap it in the `table.subscribe(source, selector?, template)` directive, which updates only that region when its selected value's reference changes (identity comparison). See the `lit/table-state` skill, Core Pattern 4. - -## Lifecycle Diagram - -```text - 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 - -```ts -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({}) // module scope - -const columns: Array> = [ - /* … module scope … */ -] - -@customElement('people-table') -class PeopleTable extends LitElement { - // ONE controller, constructed as a class field. - private tableController = new TableController(this) - - @state() - private data: Person[] = [] - - protected render() { - const table = this.tableController.table( - { - features, - columns, - data: this.data, - }, - () => ({}), // selector — empty when you don't need to project state - ) - - return html` - - - ${repeat( - table.getHeaderGroups(), - (hg) => hg.id, - (hg) => html` - - ${repeat( - hg.headers, - (h) => h.id, - (h) => html``, - )} - - `, - )} - - - ${repeat( - table.getRowModel().rows, - (r) => r.id, - (row) => html` - - ${repeat( - row.getAllCells(), - (c) => c.id, - (cell) => html``, - )} - - `, - )} - -
${FlexRender({ header: h })}
${FlexRender({ cell })}
- ` - } -} -``` - -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. - -```ts -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` - - - ` - } -} -``` - -## 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..get()` and `table.state.` are current-value reads; reactivity comes from the host invalidation subscriptions the controller already wires up. - -```ts -// Inside render(): -const pagination = table.atoms.pagination.get() // current value -const snapshot = table.state // current full state -const selected = table.state // projected via the selector you passed to .table() -``` - -## Common Mistakes - -### CRITICAL Creating a new `TableController` per render - -Wrong: - -```ts -protected render() { - const controller = new TableController(this) // every frame - const table = controller.table({ /* … */ }) -} -``` - -Correct: construct the controller once as a class field. - -```ts -private tableController = new TableController(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: - -```ts -connectedCallback() { - super.connectedCallback() - this.cachedTable = this.tableController.table({ features, 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. - -```ts -protected render() { - const table = this.tableController.table({ features, columns, data: this.data }) - return html`${table.getRowModel().rows.map(/* … */)}` -} -``` - -Source: `packages/lit-table/src/TableController.ts`. - -### HIGH Expecting `table.subscribe` to stop the host from re-rendering - -Wrong: assuming `table.subscribe(table.atoms.rowSelection, …)` makes the host invalidate only on selection changes. - -Correct: the host's `requestUpdate()` is wired to the full `table.store` and `table.optionsStore`, so `render()` still runs on every store change. What `table.subscribe` narrows is the _wrapped template region_: it skips re-rendering that region (and re-running its template function) when its selected slice is unchanged. Wrap expensive regions (large `tbody` lists) in `table.subscribe` with a stable selector to get the benefit. -Source: `packages/lit-table/src/TableController.ts` (`_setupSubscriptions`); `packages/lit-table/src/subscribe-directive.ts`. - -### HIGH Building `features` inside `render()` - -Wrong: - -```ts -protected render() { - const features = tableFeatures({ rowSortingFeature }) // new each frame - const table = this.tableController.table({ features, /* … */ }) -} -``` - -Correct: declare `features` at module scope (or once on the class, frozen). Identity drives internal memos. - -```ts -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`. diff --git a/packages/lit-table/skills/lit/migrate-v8-to-v9/SKILL.md b/packages/lit-table/skills/lit/migrate-v8-to-v9/SKILL.md deleted file mode 100644 index ebd2130991..0000000000 --- a/packages/lit-table/skills/lit/migrate-v8-to-v9/SKILL.md +++ /dev/null @@ -1,309 +0,0 @@ ---- -name: lit/migrate-v8-to-v9 -description: > - Mechanical breaking-change migration from TanStack Table v8 to v9 for - `@tanstack/lit-table`. v8's `TableController(host, () => options)` shape - collapses to v9's `new TableController(host)` + `.table(options, selector?)`; - per-row-model `get*RowModel` options become `tableFeatures({...})` slots - (row model factories live on the features object, not a separate `rowModels` - map); `flexRender(def, ctx)` becomes `FlexRender({ cell|header|footer })`; core - types gain a `TFeatures` first generic. Routing keywords: lit v8 to v9, - migration, TableController v8, get*RowModel, features lit. -type: lifecycle -library: tanstack-table -framework: lit -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - column-definitions -sources: - - TanStack/table:docs/framework/lit/lit-table.md - - TanStack/table:docs/framework/lit/guide/table-state.md - - TanStack/table:docs/framework/react/guide/use-legacy-table.md - - TanStack/table:packages/lit-table/src/TableController.ts ---- - -> **Maintainer note:** the Lit adapter is scheduled for a rewrite alongside TanStack Lit Store during the v9 beta cycle. APIs in this skill may change in a future beta. The patterns below match `9.0.0-alpha.48`. - -The Lit v9 adapter mirrors v9's React surface (atoms, `features` with row model factory slots, FlexRender) wrapped in a `ReactiveController`. There is no `useLegacyTable` shim; migrate directly. - -## The Core Mapping - -| v8 (`@tanstack/lit-table`) | v9 (`@tanstack/lit-table`) | -| ---------------------------------------------------- | --------------------------------------------------------------------------------------- | -| `new TableController(host, () => options)` | `new TableController(host)` then `.table(options, selector?)` | -| `controller.table` (property) | `controller.table(opts, selector?)` (method, called each `render()`) | -| `getCoreRowModel: getCoreRowModel()` option | core row model included by default — no extra slot needed | -| `getSortedRowModel: getSortedRowModel()` | `tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns })` | -| `getFilteredRowModel`, `getPaginationRowModel`, etc. | matching `*Feature` + matching row model factory slot in `tableFeatures({...})` | -| `flexRender(def, ctx)` | `FlexRender({ cell })` / `FlexRender({ header })` / `FlexRender({ footer })` | -| `state` + `on*Change` only | still supported; new `atoms` per-slice option preferred | -| `createColumnHelper()` | `createColumnHelper()` | -| `ColumnDef` | `ColumnDef` | -| `Table` | `Table` | - -Source: `docs/framework/lit/lit-table.md`; `packages/lit-table/src/TableController.ts`. - -## Migration Steps - -### 1. Update the controller construction - -```ts -// v8 — controller takes a thunk that returns options. -private tableController = new TableController(this, () => ({ - columns, - data: this.data, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), -})) - -protected render() { - const table = this.tableController.table // property - return html`...` -} -``` - -```ts -// v9 — controller takes only the host. Options pass to .table(...) inside render. -private tableController = new TableController(this) - -protected render() { - const table = this.tableController.table( - { - features, - columns, - data: this.data, - }, - () => ({}), // optional selector - ) - - return html`...` -} -``` - -### 2. Replace `get*RowModel` options with `tableFeatures({...})` slots - -```ts -// v8 -{ - columns, data, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), -} - -// v9 — row model factories and fn maps live on the features object -const features = tableFeatures({ - rowSortingFeature, - columnFilteringFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, - filterFns, -}) - -{ - features, - columns, data, -} -``` - -Move `features` to module scope. Reference identity matters. -Source: `docs/framework/lit/lit-table.md`. - -### 3. Update column types and helpers - -```ts -// v8 -const columnHelper = createColumnHelper() -const columns: ColumnDef[] = columnHelper.columns([ - /* … */ -]) - -// v9 -const columnHelper = createColumnHelper() -const columns: Array> = columnHelper.columns( - [ - /* … */ - ], -) -``` - -`TFeatures` is now the first generic on `ColumnDef`, `Table`, and `createColumnHelper`. - -### 4. Replace `flexRender` calls - -```ts -// v8 -
${flexRender(cell.column.columnDef.cell, cell.getContext())}${FlexRender({ cell })}${FlexRender({ header })}${FlexRender({ footer: header })}
- - ${repeat( - table.getHeaderGroups(), - (hg) => hg.id, - (hg) => html` - - ${repeat( - hg.headers, - (h) => h.id, - (h) => html` - - `, - )} - - `, - )} - - - ${repeat( - table.getRowModel().rows, - (r) => r.id, - (row) => html` - - ${repeat( - row.getAllCells(), - (c) => c.id, - (cell) => html` `, - )} - - `, - )} - -
- ${h.isPlaceholder ? null : FlexRender({ header: h })} -
${FlexRender({ cell })}
- ` - } -} -``` - -Source: `examples/lit/basic-table-controller/src/main.ts`. - -## Core Patterns - -### 1. `TableController` lifecycle - -- Construct once per host (typically as a class field). The constructor calls `host.addController(this)`. -- Call `.table(options, selector?)` inside `render()` (or any place you have a fresh `options` ready). The first call constructs the underlying core table and subscribes the host to `table.store` and `table.optionsStore`. Subsequent calls merge options and return the same logical table instance. -- `hostConnected` re-establishes subscriptions; `hostDisconnected` tears them down. - -Source: `packages/lit-table/src/TableController.ts`. - -### 2. `.table(options, selector?)` second argument - -The selector is a function from full table state to whatever you want exposed on `table.state`. Default is full state. Narrowing documents the host's actual data dependencies. Note this selector only shapes `table.state`; **the host `render()` still re-runs on every `table.store` change** because `TableController` subscribes to the full store. To skip re-rendering an expensive template region when its slice is unchanged, wrap it in the `table.subscribe` directive (Core Pattern 4). - -```ts -const table = this.tableController.table( - { features, columns, data: this._data }, - (state) => ({ pagination: state.pagination }), -) - -table.state.pagination -``` - -Source: `docs/framework/lit/guide/table-state.md`. - -### 3. Reading state without subscribing - -Direct atom / store reads return the current value without subscribing to changes. The controller already subscribes the host to the full store, so these reads stay reactive through the host's invalidation. - -```ts -const pagination = table.atoms.pagination.get() -const sorting = table.atoms.sorting.get() -const snapshot = table.state -``` - -### 4. `table.subscribe` directive for fine-grained updates - -`table.subscribe` is a Lit async directive (backed by `@tanstack/lit-store`'s `TanStackStoreSelector`). It subscribes the wrapped template to a single source and re-renders **only that template region**, leaving the rest of the host template untouched. The region re-renders whenever the selected value's reference changes (identity comparison). Return a specific slice (e.g. `state => state.pagination`) to update only when that slice changes; a selector that builds a fresh object every call re-renders on every store emit. Unlike `table.state`, it can update its slice without re-running the whole host template. - -Two call signatures: - -```ts -// 1. Selected slice — re-renders only when the `pagination` reference changes -${table.subscribe( - table.store, - (state) => state.pagination, - (pagination) => html`Page ${pagination.pageIndex + 1}`, -)} - -// 2. Whole source — any source mutation re-renders the region -${table.subscribe( - table.atoms.rowSelection, - (rowSelection) => html`${Object.keys(rowSelection).length} selected`, -)} -``` - -`source` is any store or atom: `table.store` (full table state), `table.atoms.`, or an external atom you own. - -**Use a stable selector reference.** The directive memoizes on the identity of the `source` and `selector` you pass. Declare selectors as class fields/methods so the same reference is passed every render: - -```ts -private getBodyState = (state: ReturnType) => ({ - columnFilters: state.columnFilters, - pagination: state.pagination, -}) - -// ...in render(): -${this.table.subscribe(this.table.store, this.getBodyState, () => html`…`)} -``` - -An inline `(state) => ({ … })` allocates a new function every render, which forces the directive to re-subscribe and re-render every time, defeating the optimization. - -Source: `packages/lit-table/src/subscribe-directive.ts`; `examples/lit/basic-subscribe/src/main.ts`. - -### 5. External atoms with `createAtom` + `options.atoms` - -Move slice ownership to a TanStack Store atom. The table writes to your atom when you call `table.setSorting(...)` etc. — no `on*Change` handler is needed. - -Precedence: `options.atoms[key]` > `options.state[key]` > internal `baseAtoms[key]`. - -```ts -import { createAtom } from '@tanstack/lit-store' -import { - TableController, - rowPaginationFeature, - tableFeatures, - type PaginationState, -} from '@tanstack/lit-table' - -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), -}) - -// Module-scope atoms — stable identity, shareable across components. -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) - -@customElement('my-table') -class MyTable extends LitElement { - private tableController = new TableController(this) - - protected render() { - const table = this.tableController.table({ - features, - columns, - data: this._data, - atoms: { pagination: paginationAtom }, - }) - - const { pageIndex } = paginationAtom.get() - // ... - } -} -``` - -Source: `examples/lit/basic-external-atoms/src/main.ts`. - -### 6. External state with `state` + `on*Change` - -Classic integration with `@state()` properties. Less atomic than external atoms. - -```ts -@state() -private _sorting: SortingState = [] - -protected render() { - const table = this.tableController.table({ - features, - columns, - data: this._data, - state: { sorting: this._sorting }, - onSortingChange: (updater) => { - this._sorting = updater instanceof Function ? updater(this._sorting) : updater - }, - }) -} -``` - -Source: `docs/framework/lit/guide/table-state.md`. - -### 7. `createTableHook` for reusable shared config - -Bundle `features` (including row model factories), default options, and pre-bound cell/header components. You get `useAppTable(host, options, selector?)`, `createAppColumnHelper`, and `useTableContext` / `useCellContext` / `useHeaderContext` (Lit Context consumers). - -```ts -const { useAppTable, createAppColumnHelper } = createTableHook({ - features: tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, - }), -}) - -const columnHelper = createAppColumnHelper() -const columns = columnHelper.columns([ - /* … */ -]) - -@customElement('users-table') -class UsersTable extends LitElement { - @state() private data: Person[] = [] - - // NOTE: capture `this` before the options getter — inside the getter `this` - // refers to the options object. - private appTable = (() => { - const host = this - return useAppTable(this, { - columns, - get data() { - return host.data - }, - }) - })() - - protected render() { - const table = this.appTable.table() - - return html` - - - ${table.getRowModel().rows.map( - (row) => html` - - ${row - .getAllCells() - .map((c) => - table.AppCell( - c, - (cell) => html``, - ), - )} - - `, - )} - -
${cell.FlexRender()}
- ` - } -} -``` - -Source: `examples/lit/basic-app-table/src/main.ts`; `packages/lit-table/src/createTableHook.ts`. - -## Common Mistakes - -### CRITICAL Creating a new `TableController` every render - -Wrong: - -```ts -protected render() { - const controller = new TableController(this) // new instance every render - const table = controller.table({ /* … */ }) -} -``` - -Correct: - -```ts -class MyTable extends LitElement { - private tableController = new TableController(this) // once - - protected render() { - const table = this.tableController.table({ - /* … */ - }) - } -} -``` - -Each `new TableController(host)` registers another controller on the host. The original table is discarded; the new one resubscribes; state is reset every render. -Source: `packages/lit-table/src/TableController.ts`. - -### CRITICAL Calling a feature API when the feature is not in `features` - -Wrong: - -```ts -const features = tableFeatures({}) // no rowPaginationFeature -const table = this.tableController.table({ - features, - columns, - data: this._data, -}) -table.setPageIndex(0) // TypeScript error AND runtime no-op -``` - -Correct: - -```ts -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), -}) -const table = this.tableController.table({ - features, - columns, - data: this._data, -}) -``` - -v9 generates feature APIs and state slices only for registered features. The missing-feature failure is the #1 v9 trap. -Source: `docs/guide/features.md`. - -### HIGH Passing an inline selector to `table.subscribe` - -Wrong: - -```ts -${table.subscribe( - table.store, - (state) => ({ columnFilters: state.columnFilters, pagination: state.pagination }), // new fn every render - () => html`…`, -)} -``` - -Correct: declare the selector as a stable class field/method and pass that reference. The directive memoizes on `source` + `selector` identity; a fresh inline function forces it to re-subscribe and re-render every host update, so the wrapped region never gets skipped. Note also that the host `render()` itself still runs on every `table.store` change — `table.subscribe` only spares the _wrapped_ region from re-rendering, it does not stop the host update. -Source: `packages/lit-table/src/subscribe-directive.ts`; `examples/lit/basic-subscribe/src/main.ts`. - -### HIGH `this` binding in the options getter - -Wrong: - -```ts -private appTable = useAppTable(this, { - columns, - get data() { return this.data }, // `this` refers to the options object → infinite recursion -}) -``` - -Correct: - -```ts -private appTable = (() => { - const host = this - return useAppTable(this, { columns, get data() { return host.data } }) -})() -``` - -Source: `examples/lit/basic-app-table/src/main.ts` (lines 77–90). - -### HIGH Unstable `features` / `columns` / `data` references - -Wrong: building `features` or `columns` inside `render()` so a new array/object is allocated every frame. - -Correct: declare at module scope. For `data`, prefer a `@state()` field; for derived data, memoize where the dependency actually changes. -Source: `docs/framework/lit/guide/table-state.md` (FAQ #1). - -### HIGH Reimplementing built-in feature logic by hand - -Wrong: hand-rolled sorting / filtering / pagination outside the table. - -Correct: register the matching `*Feature` and its row model factory in `tableFeatures({...})`, then use the feature APIs (`setSorting`, `setColumnFilters`, etc.). This is the #1 AI tell. -Source: `docs/guide/features.md`. - -### MEDIUM Passing the same slice via `atoms` AND `state` - -Wrong: - -```ts -this.tableController.table({ - /* … */, - atoms: { pagination: paginationAtom }, - state: { pagination: this._pagination }, // silently ignored - onPaginationChange: (u) => { /* never runs */ }, // silently ignored -}) -``` - -Correct: pick exactly one ownership path per slice. - -## See Also - -- `tanstack-table/lit/lit-table-controller` — TableController lifecycle in depth. -- `tanstack-table/lit/getting-started` — first-table walkthrough. -- `tanstack-table/lit/migrate-v8-to-v9` — moving an existing v8 codebase over. -- `tanstack-table/lit/compose-with-tanstack-virtual` — pairing with `@tanstack/lit-virtual`. diff --git a/packages/lit-table/skills/migrate-v8-to-v9/SKILL.md b/packages/lit-table/skills/migrate-v8-to-v9/SKILL.md new file mode 100644 index 0000000000..0fe32eef27 --- /dev/null +++ b/packages/lit-table/skills/migrate-v8-to-v9/SKILL.md @@ -0,0 +1,198 @@ +--- +name: migrate-v8-to-v9 +description: > + Complete Lit v8-to-v9 migration reference: TableController construction, explicit features and row-model slots, selected/atom state, FlexRender, createTableHook, type generics, prototype methods, sorting, sizing, selection, and logical pinning. +metadata: + type: lifecycle + library: '@tanstack/lit-table' + framework: lit + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#migrate-v8-to-v9' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/lit/guide/migrating.md' + - 'TanStack/table:packages/lit-table/src/index.ts' + - 'TanStack/table:examples/lit/basic-table-controller' +--- + +Use this as the complete breaking-change checklist. V9 is the current API. The central Lit change is a stable controller constructed with the host, while current options are passed to `.table(...)` during render. + +Framework prerequisites: Lit 3.1.3 or newer within v3 (`lit ^3.1.3`) and +`@lit/context ^1.1.0`. + +## Recommended Migration Order + +1. Replace the v8 controller options thunk with a host-only typed `TableController`. +2. Pass current options to `controller.table(options, selector?)` in render. +3. Move features, row models, and registries into `tableFeatures`. +4. Update selected state, controlled ownership, and rendering. +5. Apply every shared API and type rename below. +6. Treat `stockFeatures` as a temporary audit bridge; explicit features are the production target. + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +class PeopleTable extends LitElement { + private controller = new TableController(this) + + protected render() { + const table = this.controller.table({ features, columns, data: this.data }) + return html`${table.getRowModel().rows.length} rows` + } +} +``` + +## Construction and Feature Registration + +| v8 | v9 | +| ------------------------------------------ | ---------------------------------------------------------- | +| `new TableController(this, () => options)` | `new TableController(this)` | +| `controller.table` property | `controller.table(options, selector?)` call in render | +| All features bundled | Required `features: tableFeatures({...})` | +| `getCoreRowModel()` option | Remove; core row model is automatic | +| `get*RowModel()` table options | `create*RowModel()` slots in `tableFeatures` | +| `sortingFns` table option | `sortFns` feature slot | +| Top-level `onStateChange` | Per-slice callbacks, external atoms, or store subscription | + +Feature imports are `columnFilteringFeature`, `globalFilteringFeature`, `rowSortingFeature`, `rowPaginationFeature`, `rowSelectionFeature`, `rowExpandingFeature`, `rowPinningFeature`, `columnPinningFeature`, `columnVisibilityFeature`, `columnOrderingFeature`, `columnSizingFeature`, `columnResizingFeature`, `columnGroupingFeature`, and `columnFacetingFeature`. APIs are feature-gated. Put a feature before its dependent slot in one `tableFeatures` call. + +### Row-model mapping + +| v8 option | v9 slot and factory | +| -------------------------- | ------------------------------------------------------------------- | +| `getFilteredRowModel()` | `filteredRowModel: createFilteredRowModel()` after column filtering | +| `getSortedRowModel()` | `sortedRowModel: createSortedRowModel()` after row sorting | +| `getPaginationRowModel()` | `paginatedRowModel: createPaginatedRowModel()` after pagination | +| `getExpandedRowModel()` | `expandedRowModel: createExpandedRowModel()` after expanding | +| `getGroupedRowModel()` | `groupedRowModel: createGroupedRowModel()` after grouping | +| `getFacetedRowModel()` | `facetedRowModel: createFacetedRowModel()` after faceting | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues: createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues: createFacetedUniqueValues()` | + +Factories take no arguments. Register `filterFns`, `sortFns`, and `aggregationFns` as sibling feature slots. + +## Lit State Migration + +- `table.getState().sorting` becomes `table.state.sorting`, `table.store.state.sorting`, or narrow `table.atoms.sorting.get()`. +- `table.state` contains all registered slices by default. Pass a second-argument selector to `controller.table(...)` only to narrow the render-selected surface. +- Use `table.subscribe(table.store, stableSelector, renderCallback)` for selected template state. Keep the selector reference stable outside render. +- Controlled state uses Lit `@state()` fields and matching `on[State]Change` callbacks that resolve value-or-function updaters. +- Top-level `onStateChange` is removed. Use per-slice callbacks, external atoms, or `table.store.subscribe` for all changes. +- External atoms come from `@tanstack/store` and are provided through `atoms`. Never provide both `atoms.pagination` and `state.pagination`. +- The controller requests host updates for table and option-store changes; do not create a new controller in render. +- Treat `table.baseAtoms` as internal writable state; prefer feature APIs or external atoms. + +## Rendering and Composition + +| v8 | v9 | +| -------------------------- | ----------------------------------------------------------------------------- | +| `flexRender(def, context)` | `FlexRender({ cell })`, `FlexRender({ header })`, or `FlexRender({ footer })` | +| Standalone helper only | `table.FlexRender({ cell })` is also available | +| Repeated raw options | `tableOptions(...)` composition | +| Repeated conventions | `createTableHook({ features, ... })` | + +`createTableHook` returns a host-bound app table helper and pre-bound column helper. Construct the app helper with the Lit host, then call its `.table()` during render. It is optional and intended for recurring application conventions. + +## Complete Shared Breaking-Change Map + +### Instance methods + +Row, cell, column, header, and related methods now live on shared prototypes and use `this`. Call them on their instances. Do not destructure/pass them bare or expect them in object spread, `Object.keys`, or JSON. Table methods are not affected. + +### Logical column pinning + +Beta.38 has no `left`/`right` aliases. + +| old | new | +| -------------------------------------------------------------- | ------------------------------------------------------------- | +| `columnPinning.left` / `.right` | `.start` / `.end` | +| `column.pin('left' \| 'right')` | `column.pin('start' \| 'end')` | +| `getIsPinned() === 'left' \| 'right'` | `'start' \| 'end'` | +| `row.getLeftVisibleCells()` / `getRightVisibleCells()` | `getStartVisibleCells()` / `getEndVisibleCells()` | +| `getLeftHeaderGroups()` / `getRightHeaderGroups()` | `getStartHeaderGroups()` / `getEndHeaderGroups()` | +| `getLeftFooterGroups()` / `getRightFooterGroups()` | `getStartFooterGroups()` / `getEndFooterGroups()` | +| `getLeftFlatHeaders()` / `getRightFlatHeaders()` | `getStartFlatHeaders()` / `getEndFlatHeaders()` | +| `getLeftLeafHeaders()` / `getRightLeafHeaders()` | `getStartLeafHeaders()` / `getEndLeafHeaders()` | +| `getLeftLeafColumns()` / `getRightLeafColumns()` | `getStartLeafColumns()` / `getEndLeafColumns()` | +| `getLeftVisibleLeafColumns()` / `getRightVisibleLeafColumns()` | `getStartVisibleLeafColumns()` / `getEndVisibleLeafColumns()` | +| `getLeftTotalSize()` / `getRightTotalSize()` | `getStartTotalSize()` / `getEndTotalSize()` | +| `column.getStart('left')` | `column.getStart('start')` | +| `column.getAfter('right')` | `column.getAfter('end')` | +| `column.getIndex('left' \| 'right')` | `column.getIndex('start' \| 'end')` | + +Prefer CSS logical inset properties; logical names do not set DOM direction. `columnResizeDirection` is unchanged. + +### Pinning, sizing, and resizing + +- `enablePinning` splits into `enableColumnPinning` and `enableRowPinning`. +- Interactive resizing requires `columnSizingFeature` and `columnResizingFeature`; fixed sizing needs only sizing. +- `columnSizingInfo` becomes `columnResizing`. +- `setColumnSizingInfo()` becomes `setColumnResizing()`. +- `onColumnSizingInfoChange` becomes `onColumnResizingChange`. + +### Sorting, rows, and selection + +| v8 | v9 | +| ------------------------------ | ----------------------------- | +| `sortingFn` | `sortFn` | +| `sortingFns` | `sortFns` | +| `getSortingFn()` | `getSortFn()` | +| `getAutoSortingFn()` | `getAutoSortFn()` | +| `SortingFn` / `SortingFns` | `SortFn` / `SortFns` | +| `row._getAllCellsByColumnId()` | `row.getAllCellsByColumnId()` | + +Other `_`-prefixed internals are removed, including `_getPinnedRows`, `_getFacetedRowModel`, `_getFacetedMinMaxValues`, and `_getFacetedUniqueValues`. + +`getIsSomeRowsSelected()` and `getIsSomePageRowsSelected()` mean at least one, including all. Use `getIsSomeRowsSelected() && !getIsAllRowsSelected()` or `getIsSomePageRowsSelected() && !getIsAllPageRowsSelected()` for indeterminate UI. + +## TypeScript Migration + +- Add `TFeatures` first: `ColumnDef`, `Column`, `Row`, `Table`. +- Replace `createColumnHelper()` with `createColumnHelper()`; use `columnHelper.columns([...])` for inference. +- Use `StockFeatures` when using `stockFeatures`. +- Existing `TableMeta`/`ColumnMeta` declaration merging must add `TFeatures` first. Prefer per-table `tableMeta`/`columnMeta: metaHelper<...>()` slots. +- Replace global `FilterFns`, `SortFns`, `AggregationFns`, and `FilterMeta` augmentation with registry slots and `filterMeta: metaHelper<...>()`; registered keys become valid strings. +- `RowData` is restricted to records or arrays; prefer explicit object row types. + +## Common Migration Failures + +### CRITICAL: Keeping the v8 controller shape + +The controller constructor takes only the host in v9. Pass options to `.table(...)` while rendering. + +### HIGH: Recreating the controller in render + +Keep one stable controller field so subscriptions and host lifecycle remain attached. + +### HIGH: Leaving row models on table options + +Move each row model beside its prerequisite feature in `tableFeatures`. + +### HIGH: Unstable table.subscribe selector + +Define the selector as a class field or outside render to prevent avoidable update churn. + +### HIGH: Destructuring instance methods + +Use `row.getValue('name')`; prototype methods require the original instance and are absent from shallow clones. + +## Final Checklist + +- [ ] `TableController` is host-only, stable, typed with features/data, and options move to `.table(...)`. +- [ ] Features, row models, and registries are in `tableFeatures`; core row model is removed. +- [ ] State reads use selected state, atoms, or store intentionally; selectors are stable. +- [ ] `onStateChange` is replaced; controlled and external-atom ownership do not overlap. +- [ ] Rendering uses the v9 `FlexRender` object helpers. +- [ ] Prototype methods, pinning, sizing/resizing, sorting, row, and selection changes are audited. +- [ ] Helpers, types, meta, registries, and `RowData` use v9 shapes. +- [ ] Temporary `stockFeatures` usage has an explicit removal plan. + +## API Discovery + +Inspect `node_modules/@tanstack/lit-table/src/index.ts` and `TableController.ts`. Verify feature slots and exact beta APIs in `node_modules/@tanstack/table-core/src`; do not reconstruct v9 from v8 memory. diff --git a/packages/lit-table/skills/table-state/SKILL.md b/packages/lit-table/skills/table-state/SKILL.md new file mode 100644 index 0000000000..40da6163c1 --- /dev/null +++ b/packages/lit-table/skills/table-state/SKILL.md @@ -0,0 +1,155 @@ +--- +name: table-state +description: > + Use TableController-selected table.state, table.atoms/store, stable table.subscribe selectors, controlled reactive properties plus on*Change, and external TanStack Store atoms. Load when Lit state appears stale, host updates are too broad, or table state must be shared outside the element. +metadata: + type: framework + library: '@tanstack/lit-table' + framework: lit + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - getting-started +sources: + - 'TanStack/table:docs/framework/lit/guide/table-state.md' + - 'TanStack/table:examples/lit/basic-external-state' + - 'TanStack/table:packages/lit-table/src/TableController.ts' +--- + +This skill builds on @tanstack/table-core#core and this package's getting-started skill. + +## State Mental Model + +TanStack Table is primarily a state coordinator. Keep state internal unless another subsystem needs to read, persist, or drive it. Without `initialState`, `atoms`, `state`, or `on[State]Change`, the table owns every registered slice. + +- `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` combines those atoms into one readonly flat store. +- `table.state` is only the value selected by the second `controller.table` argument. + +`TableController` subscribes to state/options and requests host updates. State is feature-based: if pagination is missing from options, atoms, or types, register `rowPaginationFeature`. Keep `features`, `columns`, and `data` references stable across `render()` calls; never create arrays or derive rows inline in the `controller.table` options. + +## Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement, state } from 'lit/decorators.js' +import { + TableController, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/lit-table' +import type { PaginationState } from '@tanstack/lit-table' + +type Item = { id: string } +const features = tableFeatures({ rowPaginationFeature }) +const columns = [{ accessorKey: 'id' }] +const data: Item[] = [{ id: '1' }] + +@customElement('paged-items') +export class PagedItems extends LitElement { + @state() private pagination: PaginationState = { pageIndex: 0, pageSize: 10 } + private controller = new TableController(this) + + protected render() { + const table = this.controller.table( + { + features, + columns, + data, + state: { pagination: this.pagination }, + onPaginationChange: (updater) => { + this.pagination = + typeof updater === 'function' ? updater(this.pagination) : updater + }, + }, + (state) => ({ pagination: state.pagination }), + ) + + return html`` + } +} +``` + +## Core Patterns + +### Read selected render state from table.state + +The second argument to `controller.table` defines the shape of `table.state`. Its selector should be stable when reused across renders. + +### Read one current slice from its atom + +`table.atoms.pagination.get()` returns the current pagination snapshot. Use it in event handlers; use selected state or `table.subscribe` when a template region must update reactively. + +### Put narrow reactive islands in table.subscribe + +```ts +const page = table.subscribe( + table.store, + (state) => state.pagination.pageIndex, + (pageIndex) => html`Page ${pageIndex + 1}`, +) +``` + +Keep the selector reference stable for repeated subscription sites. + +## Choose State Ownership + +Use exactly one owner per slice: + +- Prefer internal state and feature APIs for table-local behavior. +- Use `initialState` for starting/reset values. Changing it later does not reset current state. +- Prefer a stable external TanStack Store atom in `atoms` when state is shared. Feature APIs write it directly, so omit `on[State]Change`. +- Use a reactive host property in `state.` plus its matching callback for simple controlled state. Write raw values and resolved updater functions back to the property. + +External atoms take precedence over external `state`, which syncs into the internal base atom. Do not configure multiple owners. The global v8 `onStateChange` option is gone; subscribe to `table.store` if all state changes must be observed. + +## Initialize, Update, and Reset + +Prefer `setSorting`, `nextPage`, `toggleVisibility`, `toggleSelected`, and other feature methods. Direct `baseAtoms` writes are a rare escape hatch for internal state; write the supplied external atom when it owns the slice. + +```ts +table.resetSorting() +table.resetPagination() +table.resetPagination(true) +``` + +Feature resets use `table.initialState` unless `true` requests the feature default and can update external owners. Core `table.reset()` resets internal base atoms only. Use slice types such as `PaginationState` and use `TableState` for the complete feature-inferred state. + +## Common Mistakes + +### HIGH Callback freezes controlled state + +Wrong: provide `onPaginationChange` but omit `state.pagination`. + +Correct: pass the current controlled value and write both direct values and updater functions back to the owning property. + +An on-change callback marks the slice as externally managed; failing to feed the next value back leaves the table reading the old value. + +Source: TanStack/table:docs/framework/lit/guide/table-state.md + +### HIGH Snapshot read mistaken for subscription + +Wrong: cache `const page = table.store.state.pagination.pageIndex` outside render and expect it to change. + +Correct: read selected `table.state` during render or create a `table.subscribe` template island. + +A store snapshot is current data, not a continuing Lit subscription. + +Source: TanStack/table:packages/lit-table/src/TableController.ts + +### MEDIUM Unstable selector defeats update gating + +Wrong: create unrelated selector closures throughout the render tree. + +Correct: define reusable selectors as stable class fields or module functions. + +TableController shallow-compares selected output; stable selector intent avoids unnecessary subscription churn. + +Source: TanStack/table:docs/framework/lit/guide/table-state.md + +## API Discovery + +Inspect `node_modules/@tanstack/lit-table/src/TableController.ts`, then `node_modules/@tanstack/table-core/src/core/table/` for state precedence and updater behavior. diff --git a/packages/lit-table/skills/with-tanstack-virtual/SKILL.md b/packages/lit-table/skills/with-tanstack-virtual/SKILL.md new file mode 100644 index 0000000000..1e0657bb3a --- /dev/null +++ b/packages/lit-table/skills/with-tanstack-virtual/SKILL.md @@ -0,0 +1,147 @@ +--- +name: with-tanstack-virtual +description: > + Virtualize Lit Table final row or column models with @tanstack/lit-virtual VirtualizerController, host lifecycle-aware counts, scroll refs, stable keys, dynamic measurement, sticky CSS, grid/flex sizing, and infinite data. Load for large rendered tables; Virtual is renderer composition, not tableFeatures. +metadata: + type: composition + library: '@tanstack/lit-table' + framework: lit + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/lit/guide/virtualization.md' + - 'TanStack/table:examples/lit/virtualized-rows' + - 'TanStack/table:examples/lit/virtualized-columns' + - 'TanStack/table:examples/lit/virtualized-infinite-scrolling' +--- + +This skill builds on @tanstack/table-core#core plus this package's getting-started and table-state skills. + +## Setup + +```ts +import { LitElement, html } from 'lit' +import { customElement } from 'lit/decorators.js' +import { createRef, ref } from 'lit/directives/ref.js' +import { VirtualizerController } from '@tanstack/lit-virtual' +import { + TableController, + tableFeatures, + type ColumnDef, +} from '@tanstack/lit-table' + +type Item = { id: string; name: string } +const features = tableFeatures({}) +const columns: Array> = [ + { accessorKey: 'name', header: 'Name' }, +] +const data: Array = Array.from({ length: 10_000 }, (_, id) => ({ + id: String(id), + name: `Item ${id}`, +})) + +@customElement('virtual-items') +export class VirtualItems extends LitElement { + private controller = new TableController(this) + private scroller = createRef() + private virtualizer = new VirtualizerController(this, { + count: data.length, + getScrollElement: () => this.scroller.value, + estimateSize: () => 32, + overscan: 5, + }) + + protected render() { + const table = this.controller.table({ + features, + columns, + data, + getRowId: (row) => row.id, + }) + const rows = table.getRowModel().rows + const v = this.virtualizer.getVirtualizer() + v.setOptions({ ...v.options, count: rows.length }) + + return html`
+
+ ${v + .getVirtualItems() + .map( + (item) => + html`
+ ${rows[item.index].getValue('name')} +
`, + )} +
+
` + } +} +``` + +## Core Patterns + +### Virtualize the final row model + +Set the virtualizer count from `table.getRowModel().rows.length`; index virtual items into that same array so filtering, sorting, expansion, and pagination are respected. + +### Keep geometry sources consistent + +For column virtualization, estimate from `column.getSize()` and remeasure when columnSizing changes. Render widths from the same sizing state. + +### Treat unsupported combinations as user composition + +Drag-and-drop plus virtualization is not a maintained Table recipe. Start from the maintained Virtual example and reconcile both libraries' transforms, measurement, and auto-scroll contracts explicitly. + +## Common Mistakes + +### HIGH Virtualizing raw data + +Wrong: index virtual items into the original `data` array. + +Correct: index into `table.getRowModel().rows`. + +Raw data ignores active Table processing and produces mismatched rows after sorting or filtering. + +Source: TanStack/table:examples/lit/virtualized-rows + +### HIGH Count and rows come from different models + +Wrong: use `data.length` after filters change but render filtered rows. + +Correct: update the virtualizer count from the same current `rows` array being rendered. + +Mismatched geometry can index undefined rows or leave blank scroll space. + +Source: TanStack/table:examples/lit/virtualized-rows + +### HIGH Recreating columns during host renders + +Wrong: pass `columns: [...columns]` from `render()` to work around a readonly tuple. + +Correct: declare a stable mutable `Array>` once and pass `columns` directly. + +Lit host updates can be driven by scrolling and measurement. A fresh column reference rebuilds Table's column pipeline on every such render. + +Source: TanStack/table:examples/lit/virtualized-rows + +### HIGH Sticky and sizing CSS assumed automatic + +Wrong: add a VirtualizerController and expect sticky headers, widths, and transforms. + +Correct: implement the scroll container, total-size spacer, absolute row transforms, sticky regions, and widths in Lit CSS/templates. + +Both libraries are headless over rendering geometry. + +Source: TanStack/table:docs/framework/lit/guide/virtualization.md + +## API Discovery + +Inspect `node_modules/@tanstack/lit-table/src/index.ts` and `node_modules/@tanstack/lit-virtual/src/`. Use maintained examples for geometry; do not register Virtual in `tableFeatures`. diff --git a/packages/match-sorter-utils/package.json b/packages/match-sorter-utils/package.json index a8f80b42bc..e0fe0618c9 100644 --- a/packages/match-sorter-utils/package.json +++ b/packages/match-sorter-utils/package.json @@ -24,7 +24,8 @@ "utils", "split", "separate", - "datagrid" + "datagrid", + "tanstack-intent" ], "type": "module", "types": "./dist/index.d.cts", @@ -43,7 +44,8 @@ }, "files": [ "dist", - "src" + "src", + "skills" ], "scripts": { "clean": "rimraf ./build && rimraf ./dist", diff --git a/packages/match-sorter-utils/skills/fuzzy-ranking/SKILL.md b/packages/match-sorter-utils/skills/fuzzy-ranking/SKILL.md new file mode 100644 index 0000000000..b17f3d2d1c --- /dev/null +++ b/packages/match-sorter-utils/skills/fuzzy-ranking/SKILL.md @@ -0,0 +1,94 @@ +--- +name: fuzzy-ranking +description: > + Rank fuzzy matches with rankItem, filter with RankingInfo.passed, compare saved ranking metadata with compareItems, and configure rankings, thresholds, accessors, min/max bounds, or diacritics. Load for @tanstack/match-sorter-utils itself or fuzzy Table filterMeta wiring. +metadata: + type: core + library: '@tanstack/match-sorter-utils' + library_version: '9.0.0-beta.37' +sources: + - 'TanStack/table:packages/match-sorter-utils/src/index.ts' + - 'TanStack/table:docs/framework/react/guide/fuzzy-filtering.md' + - 'TanStack/table:examples/react/filters-fuzzy' +--- + +## Setup + + + +```ts +import { compareItems, rankItem, rankings } from '@tanstack/match-sorter-utils' + +const query = 'tan' +const values = ['table', 'tanner', 'router'] +export const ranked = values + .map((value) => ({ + value, + info: rankItem(value, query, { threshold: rankings.MATCHES }), + })) + .filter((entry) => entry.info.passed) + .sort((a, b) => compareItems(a.info, b.info)) +``` + +## Core Patterns + +### Separate ranking, filtering, and sorting + +Call `rankItem` once, filter on `info.passed`, retain the `RankingInfo`, then order matching results with `compareItems`. + +### Rank object fields through accessors + +```ts +type Person = { name: string; email: string } +const person: Person = { name: 'Ada Lovelace', email: 'ada@example.test' } +const info = rankItem(person, 'lov', { + accessors: [(item) => item.name, (item) => item.email], +}) +``` + +Accessor options can set a per-accessor threshold plus minRanking/maxRanking bounds. + +### Store ranking as Table filter metadata + +In a Table filterFn, call `addMeta?.({ itemRank })`. Register the corresponding +meta shape with `filterMeta: metaHelper<{ itemRank: RankingInfo }>()`. A related +sortFn reads `row.columnFiltersMeta[columnId]?.itemRank` and uses `compareItems`, +falling back to an ordinary comparator for ties or absent metadata. For the +primary Table composition, load `@tanstack/table-core#global-filtering` and +register the fuzzy filter under `filterFns` for `globalFilterFn: 'fuzzy'`. + +## Common Mistakes + +### HIGH Numeric rank used as pass flag + +Wrong: `if (rankItem(value, query).rank) include(value)`. + +Correct: test `rankItem(value, query).passed`. + +Ranks below the configured threshold can still be nonzero; `passed` records the threshold decision. + +Source: TanStack/table:packages/match-sorter-utils/src/index.ts + +### HIGH Ranking recomputed during sorting + +Wrong: call `rankItem` for both rows on every comparator invocation. + +Correct: retain RankingInfo during filtering and pass the stored values to `compareItems`. + +Sorting is called many times; recomputing also risks using different query/options than the filter decision. + +Source: TanStack/table:docs/framework/react/guide/fuzzy-filtering.md + +### MEDIUM compareItems direction reversed twice + +Wrong: negate `compareItems(a, b)` because a larger rank should appear first. + +Correct: use `compareItems(a, b)` directly for ascending fuzzy relevance. + +The function already returns -1 when `a.rank` is greater than `b.rank`. + +Source: TanStack/table:packages/match-sorter-utils/src/index.ts + +## API Discovery + +Inspect `node_modules/@tanstack/match-sorter-utils/src/index.ts` for the installed `RankItemOptions`, accessor attributes, ranking constants, and comparator behavior. For TanStack Table metadata integration, load the global-filtering and sorting skills. diff --git a/packages/preact-table-devtools/skills/devtools/SKILL.md b/packages/preact-table-devtools/skills/devtools/SKILL.md new file mode 100644 index 0000000000..0700ee4699 --- /dev/null +++ b/packages/preact-table-devtools/skills/devtools/SKILL.md @@ -0,0 +1,85 @@ +--- +name: devtools +description: > + Connect native Preact Table v9 instances to Devtools with tableDevtoolsPlugin and useTanStackTableDevtools. Load for React-package mixups, missing targets, required options.key, enabled state, cleanup, or development gating. +metadata: + type: framework + library: '@tanstack/preact-table-devtools' + framework: preact + library_version: '9.0.0-beta.40' +requires: + - '@tanstack/table-core#core' + - '@tanstack/table-devtools#devtools' +sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/preact-table-devtools/src/index.ts' + - 'TanStack/table:packages/preact-table-devtools/src/useTanStackTableDevtools.ts' +--- + +This skill builds on @tanstack/table-core#core and @tanstack/table-devtools#devtools. + +## Setup + +```tsx +import { TanStackDevtools } from '@tanstack/preact-devtools' +import { useTable, tableFeatures } from '@tanstack/preact-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/preact-table-devtools' + +const features = tableFeatures({}) +const columns = [{ accessorKey: 'id' }] +const data: Array<{ id: string }> = [] + +export function App() { + const table = useTable({ + key: 'users-table', + features, + columns, + data, + }) + useTanStackTableDevtools(table) + return +} +``` + +## Hooks and Components + +Use the native Preact packages throughout. Pass `{ enabled }` rather than conditionally calling the registration hook. + +## Common Mistakes + +### HIGH React Devtools package mixed into Preact + +Wrong: import the hook from `@tanstack/react-table-devtools` through compat. + +Correct: use `@tanstack/preact-table-devtools` with `@tanstack/preact-devtools`. + +The native adapter owns Preact effect lifecycle and plugin types. + +Source: TanStack/table:docs/devtools.md + +### HIGH Missing or duplicate key + +Wrong: omit `key` or reuse `users-table` for multiple mounted tables. + +Correct: give each live table one stable unique key. + +Keyless registration is skipped; duplicate keys replace targets. + +Source: TanStack/table:packages/table-devtools/src/tableTarget.ts + +### MEDIUM Production no-op mistaken for failure + +Wrong: expect the default entrypoint to stay active in a production bundle. + +Correct: keep Devtools development-only unless an explicit production import is required. + +The default index deliberately selects no-op implementations outside development. + +Source: TanStack/table:packages/preact-table-devtools/src/index.ts + +## API Discovery + +Inspect `node_modules/@tanstack/preact-table-devtools/src/index.ts` and `useTanStackTableDevtools.ts`; do not copy React adapter imports. diff --git a/packages/preact-table-devtools/skills/preact/compose-with-tanstack-devtools/SKILL.md b/packages/preact-table-devtools/skills/preact/compose-with-tanstack-devtools/SKILL.md deleted file mode 100644 index 20a4eca48c..0000000000 --- a/packages/preact-table-devtools/skills/preact/compose-with-tanstack-devtools/SKILL.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -name: preact/compose-with-tanstack-devtools -description: > - Wire up TanStack Devtools for TanStack Table in Preact. Mount `TanStackDevtools` - with `tableDevtoolsPlugin()` once at the app root and call - `useTanStackTableDevtools(table)` after each `useTable` so the table is - registered as a devtools target. Live devtools are tree-shaken to no-ops in - production unless you import from `@tanstack/preact-table-devtools/production`. -type: composition -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - state-management - - preact/table-state -sources: - - TanStack/table:docs/devtools.md - - TanStack/table:packages/preact-table-devtools/src/index.ts - - TanStack/table:packages/preact-table-devtools/src/plugin.tsx - - TanStack/table:packages/preact-table-devtools/src/useTanStackTableDevtools.ts - - TanStack/table:packages/preact-table-devtools/src/production.ts ---- - -This skill builds on `tanstack-table/preact/table-state`. Read that first — the devtools panel inspects whatever table instance you register, so you need a working `useTable` before this skill is useful. - -## Setup - -Install the TanStack Devtools host and the Preact Table adapter: - -```sh -pnpm add @tanstack/preact-devtools @tanstack/preact-table-devtools -``` - -The recommended pattern has two parts: - -1. Mount `` once at the app root with `tableDevtoolsPlugin()`. -2. Call `useTanStackTableDevtools(table)` right after every `useTable()`. - -```tsx -import { render } from 'preact' -import { useTable } from '@tanstack/preact-table' -import { TanStackDevtools } from '@tanstack/preact-devtools' -import { - tableDevtoolsPlugin, - useTanStackTableDevtools, -} from '@tanstack/preact-table-devtools' - -function UsersScreen() { - const table = useTable({ - features, - key: 'users-table', - columns, - data, - }) - - // Register this table with the devtools panel. - useTanStackTableDevtools(table) - - return -} - -render( - <> - - {/* Mount once, anywhere in the tree. */} - - , - document.getElementById('root')!, -) -``` - -`tableDevtoolsPlugin()` returns a plugin descriptor for the multi-panel TanStack Devtools UI. `useTanStackTableDevtools` is a `preact/hooks` `useEffect` wrapper that upserts/removes the registration target on mount/unmount and re-runs when `table` or `table.options.key` changes. - -## Patterns - -### Keying Tables - -Add a stable `key` option to the table options. Devtools use this key as both the registration id and the panel selector label. - -```tsx -useTanStackTableDevtools(table) -``` - -### Multiple Tables - -Register as many tables as you like. The Table panel renders a selector. Name each one. - -```tsx -function Dashboard() { - const ordersTable = useTable({ ...ordersOptions, key: 'orders-table' }) - const usersTable = useTable({ ...usersOptions, key: 'users-table' }) - - useTanStackTableDevtools(ordersTable) - useTanStackTableDevtools(usersTable) - - return -} -``` - -### Disabling Per Table - -`useTanStackTableDevtools` accepts an `enabled` option. When `false`, the registration is removed (the table disappears from the panel) but the hook still runs cleanly. - -```tsx -useTanStackTableDevtools(table, { - enabled: import.meta.env.DEV && showTableDevtools, -}) -``` - -### Production Builds - -The default `@tanstack/preact-table-devtools` entrypoint swaps to no-op implementations when `process.env.NODE_ENV !== 'development'`. To ship the real devtools to production, switch BOTH imports to the `/production` entrypoint: - -```tsx -import { TanStackDevtools } from '@tanstack/preact-devtools' -import { - tableDevtoolsPlugin, - useTanStackTableDevtools, -} from '@tanstack/preact-table-devtools/production' -``` - -If you mix entrypoints (one from `/production`, one from the default), one side is a no-op in production and the panel will appear empty. - -### Conditional Devtools by Env - -For a code-split production-only devtools bundle, dynamically import the `/production` entrypoint behind a flag: - -```tsx -import { lazy, Suspense } from 'preact/compat' - -const TableDevtoolsRoot = lazy(async () => { - const { tableDevtoolsPlugin } = - await import('@tanstack/preact-table-devtools/production') - const { TanStackDevtools } = await import('@tanstack/preact-devtools') - return { - default: () => , - } -}) -``` - -## Common Mistakes - -### Forgetting to mount `` at the app root - -Calling `useTanStackTableDevtools(table)` alone does nothing visible — it only registers the table with the devtools target store. Without a `` somewhere in the tree, there is no panel to render the registration. Symptom: hook runs without errors, no devtools button appears. - -### Importing devtools from the default path in a prod-only bundle - -If you only deploy production builds, `@tanstack/preact-table-devtools` resolves to no-op implementations. The plugin will mount, but the panel will be empty. Use `@tanstack/preact-table-devtools/production` if you want the real devtools available there. - -### Accidentally shipping devtools to end users via `/production` - -The flip side: importing from `/production` in your default app bundle means every visitor downloads and runs the devtools UI. That is usually not what you want. Restrict `/production` imports to dev/preview entrypoints or code-split them behind a flag. - -### Calling `useTanStackTableDevtools` outside the component that owns the table - -The hook needs a `Table` instance to register. If you call it in a parent before `useTable` runs, or in a sibling that does not have access to the table, you pass `undefined` and nothing is registered. Always call it in the same component as `useTable`, immediately after. - -### Multiple tables without keys - -A table registered without `options.key` is skipped and devtools log an error. Add a unique `key` option to every table that should appear in devtools. diff --git a/packages/preact-table/skills/create-table-hook/SKILL.md b/packages/preact-table/skills/create-table-hook/SKILL.md new file mode 100644 index 0000000000..a30ee5d421 --- /dev/null +++ b/packages/preact-table/skills/create-table-hook/SKILL.md @@ -0,0 +1,176 @@ +--- +name: create-table-hook +description: > + Create reusable Preact table infrastructure with createTableHook, useAppTable, createAppColumnHelper, shared options/features, optional component registries, App wrappers, and typed context hooks. Load for recurring conventions, scoped contexts, or prop drilling. +metadata: + { + type: framework, + library: '@tanstack/preact-table', + library_version: '9.0.0-beta.38', + framework: preact, + } +requires: ['@tanstack/table-core#core', getting-started, table-state] +sources: + - 'TanStack/table:docs/framework/preact/guide/composable-tables.md' + - 'TanStack/table:docs/framework/preact/guide/table-context.md' + - 'TanStack/table:examples/preact/composable-tables' + - 'TanStack/table:packages/preact-table/src/createTableHook.tsx' + - 'TanStack/table:packages/preact-table/src/createTableHookContexts.tsx' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Use the app hook for repeated conventions; one-off tables should stay with `useTable`. + +## Setup + +```tsx +import { + createTableHook, + rowSelectionFeature, + tableFeatures, +} from '@tanstack/preact-table' + +export const { createAppColumnHelper, useAppTable, useTableContext } = + createTableHook({ + features: tableFeatures({ rowSelectionFeature }), + getRowId: (row: { id: string }) => row.id, + }) +``` + +## Core Patterns + +### Use the factory-bound helper + +```tsx +type Person = { id: string; name: string } +const helper = createAppColumnHelper() +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) +``` + +### Consume typed context under its wrapper + +```tsx +function RowCount() { + const table = useTableContext() + return {table.getRowModel().rows.length} +} +const table = useAppTable({ data, columns }) +return ( + + + +) +``` + +`AppTable` takes ordinary JSX children without a selector. Use function children only when a selector supplies their value: + +```tsx + state.rowSelection}> + {(rowSelection) => {Object.keys(rowSelection).length}} + +``` + +### Isolate genuinely nested table setups + +Default contexts are module-scoped and HMR-stable; sibling tables are already isolated by their providers. Separate `createTableHook` calls still share those defaults, so nested providers can silently resolve a consumer to the inner table. Use fresh contexts only when different table setups are nested: + +```tsx +import { + createTableHook, + createTableHookContexts, + rowSelectionFeature, + tableFeatures, +} from '@tanstack/preact-table' + +const features = tableFeatures({ rowSelectionFeature }) +const { tableContext, cellContext, headerContext } = + createTableHookContexts() + +export const app = createTableHook({ + features, + tableContext, + cellContext, + headerContext, +}) +``` + +Prefer hooks returned by `createTableHook` because their types include registered component maps. Hooks returned directly by `createTableHookContexts` know only `TFeatures` and support modules that cannot import the completed factory. + +## Common Mistakes + +### MEDIUM Abstracting a single table + +Wrong: + +```tsx +const app = createTableHook({ features: tableFeatures({}) }) +``` + +Correct: + +```tsx +const table = useTable({ features, columns, data }) +``` + +The factory is valuable when it centralizes repeated policy, not merely another constructor name. + +Source: `docs/framework/preact/guide/composable-tables.md` + +### HIGH Reading outside the matching provider + +Wrong: + +```tsx +return +``` + +Correct: + +```tsx +return ( + + + +) +``` + +Factory context hooks require the corresponding `AppTable`, `AppCell`, or `AppHeader` wrapper. + +Source: `packages/preact-table/src/createTableHook.tsx` + +### HIGH Recreating contexts per render + +Wrong: + +```tsx +function Grid() { + const app = createTableHook({ features }) + const table = app.useAppTable({ data, columns }) + return ( + +
+ + ) +} +``` + +Correct: + +```tsx +const app = createTableHook({ features }) +function Grid() { + const table = app.useAppTable({ data, columns }) + return ( + +
+ + ) +} +``` + +Creating a new factory closure during render makes hook configuration and component registries unstable. Keep the factory and its bound hook identity at module scope. + +Source: `packages/preact-table/src/createTableHook.tsx` + +## API Discovery + +Inspect `node_modules/@tanstack/preact-table/src/createTableHook.tsx` and `createTableHookContexts.tsx` for exact return names, provider props, registries, and scoped context types. diff --git a/packages/preact-table/skills/getting-started/SKILL.md b/packages/preact-table/skills/getting-started/SKILL.md new file mode 100644 index 0000000000..2d1bd24977 --- /dev/null +++ b/packages/preact-table/skills/getting-started/SKILL.md @@ -0,0 +1,157 @@ +--- +name: getting-started +description: > + Create a native @tanstack/preact-table v9 table with useTable, tableFeatures, stable inputs, row/header models, and Preact FlexRender helpers. Load when starting a Preact table or replacing @tanstack/react-table through preact/compat. +metadata: + { + type: framework, + library: '@tanstack/preact-table', + library_version: '9.0.0-beta.38', + framework: preact, + } +requires: ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] +sources: + - 'TanStack/table:docs/framework/preact/guide/migrating.md' + - 'TanStack/table:examples/preact/basic-use-table' + - 'TanStack/table:packages/preact-table/src/index.ts' +--- + +This skill builds on `@tanstack/table-core#core` and `@tanstack/table-core#table-features`. Use the native Preact adapter, not React through compat. + +## Setup + +```tsx +import { useState } from 'preact/hooks' +import { + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/preact-table' + +type Person = { name: string } +const features = tableFeatures({}) +const helper = createColumnHelper() +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) + +export function PeopleTable() { + const [data] = useState([{ name: 'Ada' }]) + const table = useTable({ features, columns, data }) + return ( + + + {table.getHeaderGroups().map((group) => ( + + {group.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((cell) => ( + + ))} + + ))} + +
+ +
+ +
+ ) +} +``` + +## Core Patterns + +### Register only required plugins + +```tsx +import { + createSortedRowModel, + rowSortingFeature, + tableFeatures, +} from '@tanstack/preact-table' +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +``` + +### Keep features and columns module-stable + +```tsx +const features = tableFeatures({}) +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) +``` + +## Common Mistakes + +### HIGH Importing the React adapter through compat + +Wrong: + +```tsx +import { useTable } from '@tanstack/react-table' +``` + +Correct: + +```tsx +import { useTable } from '@tanstack/preact-table' +``` + +The native adapter uses Preact hooks, stores, JSX types, and subscriptions directly. + +Source: `docs/framework/preact/guide/migrating.md` + +### HIGH Copying the v8 constructor + +Wrong: + +```tsx +const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), +}) +``` + +Correct: + +```tsx +const table = useTable({ data, columns, features }) +``` + +V9 uses explicit feature slots rather than v8 row-model table options. + +Source: `docs/framework/preact/guide/migrating.md` + +### MEDIUM Recreating static inputs in render + +Wrong: + +```tsx +const table = useTable({ + features: tableFeatures({}), + columns: [{ accessorKey: 'name' }], + data, +}) +``` + +Correct: + +```tsx +const table = useTable({ features, columns, data }) +``` + +New feature and column identities cause needless option and model work. + +Source: `examples/preact/basic-use-table` + +## API Discovery + +Inspect `node_modules/@tanstack/preact-table/src/index.ts`, then `useTable.ts`, `Subscribe.tsx`, or `FlexRender.tsx`; follow core exports into installed `@tanstack/table-core/src/`. diff --git a/packages/preact-table/skills/migrate-v8-to-v9/SKILL.md b/packages/preact-table/skills/migrate-v8-to-v9/SKILL.md new file mode 100644 index 0000000000..7395dd75ae --- /dev/null +++ b/packages/preact-table/skills/migrate-v8-to-v9/SKILL.md @@ -0,0 +1,191 @@ +--- +name: migrate-v8-to-v9 +description: > + Perform a complete Preact TanStack Table v8-to-v9 migration: remove React-through-preact/compat usage, adopt the native adapter, explicit features and row-model slots, Preact state and atoms, rendering, composition, types, and every shared API rename and semantic change. Use for migration plans, implementation, or audits. +metadata: + type: lifecycle + library: '@tanstack/preact-table' + library_version: '9.0.0-beta.38' + framework: preact +requires: + - '@tanstack/table-core#migrate-v8-to-v9' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/preact/guide/migrating.md' + - 'TanStack/table:packages/preact-table/src/index.ts' + - 'TanStack/table:examples/preact/basic-use-table' +--- + +Read `@tanstack/table-core#migrate-v8-to-v9`, `getting-started`, and `table-state`. Use this as the exhaustive Preact migration checklist. Verify exact APIs in the installed source before assuming another v9 beta has the same surface. + +Framework prerequisite: Preact 10 or newer (`preact >=10`). + +## Target architecture + +```tsx +import { + createSortedRowModel, + rowSortingFeature, + tableFeatures, + useTable, +} from '@tanstack/preact-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +const table = useTable({ features, columns, data }) +``` + +V8 did not have a first-party Preact adapter; many Preact apps used `@tanstack/react-table` through `preact/compat`. V9 uses native `@tanstack/preact-table`. Remove compatibility aliases that existed only for Table after replacing the imports. Prefer explicit features as the end state; `stockFeatures` is only a kitchen-sink migration shortcut. + +## Complete breaking-change map + +### Adapter, construction, and features + +| v8 | v9 | +| --------------------------------------------------------- | ------------------------------------------------------------------- | +| `@tanstack/react-table` through `preact/compat` | Native `@tanstack/preact-table` | +| `useReactTable(options)` | `useTable({ ...options, features })` | +| Every feature bundled | Register used `*Feature` objects with `tableFeatures()` | +| `getCoreRowModel()` option | Remove it; core is automatic | +| `getFilteredRowModel()` | `filteredRowModel: createFilteredRowModel()` | +| `getSortedRowModel()` | `sortedRowModel: createSortedRowModel()` | +| `getPaginationRowModel()` | `paginatedRowModel: createPaginatedRowModel()` | +| `getExpandedRowModel()` | `expandedRowModel: createExpandedRowModel()` | +| `getGroupedRowModel()` | `groupedRowModel: createGroupedRowModel()` | +| `getFacetedRowModel()` | `facetedRowModel: createFacetedRowModel()` | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues: createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues: createFacetedUniqueValues()` | +| Table/factory `sortingFns`, `filterFns`, `aggregationFns` | `sortFns`, `filterFns`, `aggregationFns` slots in `tableFeatures()` | +| Early-beta `rowModels: { ... }` | Direct named slots in `tableFeatures()` | + +Register each prerequisite feature before its row-model slot. Stock features are `columnFilteringFeature`, `globalFilteringFeature`, `rowSortingFeature`, `rowPaginationFeature`, `rowSelectionFeature`, `rowExpandingFeature`, `rowPinningFeature`, `columnPinningFeature`, `columnVisibilityFeature`, `columnOrderingFeature`, `columnSizingFeature`, `columnResizingFeature`, `columnGroupingFeature`, and `columnFacetingFeature`. + +### Preact state and subscriptions + +| v8 | v9 | +| --------------------------- | ------------------------------------------------------------------------------------ | +| `table.getState()` | Reactive `table.state`, snapshot `table.store.state`, or `table.atoms..get()` | +| Top-level `onStateChange` | Per-slice callbacks or `table.store.subscribe()` | +| Whole-component state reads | Custom second-argument selector, `table.Subscribe`, or atom source | + +The default `useTable` selector subscribes the component to all registered state. Pass a selector for `table.state`, or `() => null` and subscribe lower: + +```tsx +const table = useTable(options, () => null) + + + {selection => {Object.keys(selection).length} selected} + +``` + +Controlled `state` plus per-slice `on[State]Change` remains supported. For app-owned atoms, use `useCreateAtom`/`useSelector` from `@tanstack/preact-store` and pass them through `options.atoms`. An external atom wins over `state` for the same slice; do not mirror both ownership models. + +### Rendering and composition + +- Replace React-adapter `flexRender(def, context)` with `` or standalone ``; the function remains for advanced cases. +- Use `tableOptions()` for typed reusable option fragments. +- Use `createTableHook({ features, ...defaults })` for repeated app table conventions; it returns native app helpers such as `useAppTable` and `createAppColumnHelper`. +- Call row/cell/column/header methods on their instance. Prototype methods lose `this` when destructured and do not appear in object spread, `Object.keys`, or JSON. Table-instance methods are not affected. + +### TypeScript and helpers + +| v8 | v9 | +| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| `createColumnHelper()` | `createColumnHelper()` | +| Plain arrays that widen nested values | `columnHelper.columns([...])` | +| `ColumnDef` | `ColumnDef` | +| `Column`, `Row`, `Table` | Add `TFeatures` first | +| `Cell` | `Cell` | +| React module augmentation | Target `@tanstack/preact-table` | +| `TableMeta` / `ColumnMeta` | Add `TFeatures`, or use per-table `tableMeta` / `columnMeta` slots with `metaHelper()` | +| Global `FilterFns`, `SortFns`, `AggregationFns`, and `FilterMeta` augmentation | `filterFns`, `sortFns`, `aggregationFns`, and `filterMeta` feature slots | +| `RowData = unknown` | Record or array row data | + +Infer with `typeof features`; use `StockFeatures` only for deliberate `stockFeatures` typing. + +### Shared API and semantic changes + +| v8 / pre-beta.38 pinning | v9 beta.38+ | +| -------------------------------------------------------------- | ------------------------------------------------------------- | +| `columnPinning.left` / `.right` | `.start` / `.end` | +| `column.pin('left' \| 'right')` | `column.pin('start' \| 'end')` | +| `getIsPinned() === 'left' \| 'right'` | `'start' \| 'end'` | +| `row.getLeftVisibleCells()` / `getRightVisibleCells()` | `getStartVisibleCells()` / `getEndVisibleCells()` | +| `getLeftHeaderGroups()` / `getRightHeaderGroups()` | `getStartHeaderGroups()` / `getEndHeaderGroups()` | +| `getLeftFooterGroups()` / `getRightFooterGroups()` | `getStartFooterGroups()` / `getEndFooterGroups()` | +| `getLeftFlatHeaders()` / `getRightFlatHeaders()` | `getStartFlatHeaders()` / `getEndFlatHeaders()` | +| `getLeftLeafHeaders()` / `getRightLeafHeaders()` | `getStartLeafHeaders()` / `getEndLeafHeaders()` | +| `getLeftLeafColumns()` / `getRightLeafColumns()` | `getStartLeafColumns()` / `getEndLeafColumns()` | +| `getLeftVisibleLeafColumns()` / `getRightVisibleLeafColumns()` | `getStartVisibleLeafColumns()` / `getEndVisibleLeafColumns()` | +| `getLeftTotalSize()` / `getRightTotalSize()` | `getStartTotalSize()` / `getEndTotalSize()` | +| `column.getStart('left')` | `column.getStart('start')` | +| `column.getAfter('right')` | `column.getAfter('end')` | +| `column.getIndex('left' \| 'right')` | `column.getIndex('start' \| 'end')` | + +Logical pinning does not style DOM direction automatically; use logical CSS insets. `columnResizeDirection` is unchanged. + +| v8 | v9 | +| ----------------------------------------------- | -------------------------------------------------------------------------- | +| Table `enablePinning` | `enableColumnPinning` plus `enableRowPinning`; column-level option remains | +| Combined sizing/resizing | `columnSizingFeature` plus `columnResizingFeature` for drag resizing | +| `columnSizingInfo` / `onColumnSizingInfoChange` | `columnResizing` / `onColumnResizingChange` | +| `setColumnSizingInfo()` | `setColumnResizing()` | +| `sortingFn` / `sortingFns` | `sortFn` / `sortFns` | +| `getSortingFn()` / `getAutoSortingFn()` | `getSortFn()` / `getAutoSortFn()` | +| `SortingFn` / `SortingFns` | `SortFn` / `SortFns` | +| `row._getAllCellsByColumnId()` | `row.getAllCellsByColumnId()` | +| `table._getPinnedRows()` | `getTopRows()`, `getCenterRows()`, or `getBottomRows()` | +| `table._getFacetedRowModel()` | Public faceting APIs on the relevant column/table | +| `table._getFacetedMinMaxValues()` | `getFacetedMinMaxValues()` | +| `table._getFacetedUniqueValues()` | `getFacetedUniqueValues()` | + +All other underscore-prefixed internals are removed. `getIsSomeRowsSelected()` and `getIsSomePageRowsSelected()` now stay true when all applicable rows are selected. Use `getIsSomeRowsSelected() && !getIsAllRowsSelected()` or `getIsSomePageRowsSelected() && !getIsAllPageRowsSelected()` for indeterminate UI. + +## Migration procedure + +1. Replace React adapter imports and `useReactTable`; remove Table-only `preact/compat` configuration. +2. Inventory used features, row models, registries, state slices, methods, and `_` internals. +3. Build `tableFeatures()` in prerequisite order; remove `getCoreRowModel` and old/early-beta row-model placement. +4. Apply all pinning, sizing, sorting, row, and selection mappings above. +5. Update helpers/types with `typeof features`, retarget augmentation, and prefer feature registry/meta slots. +6. Replace `getState()`/`onStateChange`; explicitly choose internal, per-slice controlled, or external-atom ownership. +7. Replace unbound row/cell/column/header methods and migrate rendering. +8. Add `tableOptions`, subscriptions, or `createTableHook` only where reusable composition or render isolation needs them. +9. Type-check and exercise every enabled client- and server-side flow. +10. Audit away `stockFeatures` if the production table should remain tree-shakable. + +## Final migration checklist + +- [ ] Replace React-adapter imports/useReactTable with native Preact imports/useTable and remove Table-only compat aliases. +- [ ] Register every used stock feature explicitly and put prerequisites before dependent slots. +- [ ] Remove `getCoreRowModel`; move all eight optional row-model factories into `tableFeatures`. +- [ ] Move `filterFns`, `sortFns`, `aggregationFns`, and `filterMeta` into feature slots. +- [ ] Replace `table.getState()` and top-level `onStateChange`; choose Preact selectors, Subscribe, per-slice callbacks, store subscription, or external atoms. +- [ ] Audit controlled/external ownership and atom precedence. +- [ ] Replace unbound or copied row/cell/column/header methods. +- [ ] Apply the entire left/right to start/end pinning map and logical sticky CSS. +- [ ] Split pinning options and sizing/resizing; rename resizing state, setter, and callback. +- [ ] Apply every sorting option/API/type/registry rename. +- [ ] Remove all listed underscore-prefixed internals and use public replacements. +- [ ] Pair each some-selected check with its all-selected predicate for indeterminate UI. +- [ ] Update TFeatures helpers/types, `columns()`, `StockFeatures`, meta slots, registries, and RowData. +- [ ] Migrate Preact FlexRender and adopt tableOptions/createTableHook only where useful. +- [ ] Type-check and test every enabled client/manual feature plus LTR/RTL layout behavior. +- [ ] Remove temporary stockFeatures after the explicit-feature audit when tree-shaking matters. + +## Common migration failures + +- Keeping React imports or compatibility aliases after adopting the native Preact adapter. +- Registering a row model without its feature, or leaving it on table options. +- Assuming `table.atoms` narrows parent renders while the default full selector is still active. +- Supplying controlled state without its per-slice change handler. +- Destructuring prototype-backed methods. +- Renaming pinning state without updating sticky CSS and all region APIs. +- Using the changed “some selected” semantics directly as checkbox indeterminate state. + +## API discovery + +Inspect `node_modules/@tanstack/preact-table/src/index.ts` and `node_modules/@tanstack/table-core/src/index.ts`. Do not copy React adapter APIs merely because the v8 app used `preact/compat`. diff --git a/packages/preact-table/skills/preact/client-to-server/SKILL.md b/packages/preact-table/skills/preact/client-to-server/SKILL.md deleted file mode 100644 index 7850fbf648..0000000000 --- a/packages/preact-table/skills/preact/client-to-server/SKILL.md +++ /dev/null @@ -1,287 +0,0 @@ ---- -name: preact/client-to-server -description: > - Convert a client-side `@tanstack/preact-table` to server-side (a.k.a. manual) - modes. Pass server-paginated data, set `manualSorting` / `manualFiltering` / - `manualPagination` / `manualGrouping` / `manualExpanding` for whatever the - server owns, supply `rowCount`, key external atoms for pagination/sorting/ - filters and trigger a refetch when they change. Routing keywords: server-side - pagination, manual pagination, manualSorting, manualFiltering, rowCount, - remote data preact. -type: lifecycle -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - state-management - - pagination - - filtering - - sorting - - preact/table-state -sources: - - TanStack/table:examples/preact/basic-external-atoms/src/main.tsx - - TanStack/table:examples/preact/with-tanstack-query/src/main.tsx - - TanStack/table:examples/preact/with-tanstack-query/src/fetchData.ts - - TanStack/table:docs/framework/preact/guide/table-state.md ---- - -Client-side tables run sort/filter/paginate through registered row-model factories. Server-side tables let the server own those stages; the table just renders what the server returned and emits state changes that the app uses to refetch. Same `features`, same APIs — different ownership. - -## The Manual Flags - -Set the matching flag(s) to `true` to tell the table that the server (not the registered row-model factory) is doing that stage: - -| Flag | Owned by server | -| ------------------ | ----------------------- | -| `manualPagination` | page slicing | -| `manualSorting` | row ordering | -| `manualFiltering` | column + global filters | -| `manualGrouping` | group-by rows | -| `manualExpanding` | row expansion | - -The matching `*Feature` should still be in `features` so its state slice exists and its APIs work — you are only telling the row-model pipeline to skip the transform. - -For pagination, supply `rowCount` so `table.getPageCount()` is correct. Optional but usually required for a UI. - -Source: `examples/preact/with-tanstack-query/src/main.tsx`. - -## Standard Pattern - -Own the slices that drive the server request with external atoms. Subscribe to them with `useSelector` so the request key is reactive. Pass them through `options.atoms`. Trigger the refetch from the same atoms. - -```tsx -import { useMemo } from 'preact/hooks' -import { useCreateAtom, useSelector } from '@tanstack/preact-store' -import { - rowPaginationFeature, - tableFeatures, - useTable, - type PaginationState, -} from '@tanstack/preact-table' - -const features = tableFeatures({ rowPaginationFeature }) - -function App() { - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - const pagination = useSelector(paginationAtom) - - // Any data fetcher works — fetch / SWR / preact-query / a Suspense source. - const { data: rowsPayload } = useSomeServerFetcher({ - queryKey: ['rows', pagination], - queryFn: () => fetchRows(pagination), - }) - - const defaultData = useMemo(() => [], []) - - const table = useTable( - { - features, // no paginatedRowModel factory — server owns the slicing - columns, - data: rowsPayload?.rows ?? defaultData, - rowCount: rowsPayload?.rowCount, // makes getPageCount() correct - atoms: { pagination: paginationAtom }, - manualPagination: true, - }, - (state) => state, - ) - // ... -} -``` - -Source: `examples/preact/with-tanstack-query/src/main.tsx` (lines 56–86). - -## All Three Slices Server-Owned - -Same shape, more atoms. Compose `pagination + sorting + columnFilters + globalFilter` into the request key. - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - globalFilteringFeature, -}) - -const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, -}) -const sortingAtom = useCreateAtom([]) -const columnFiltersAtom = useCreateAtom([]) -const globalFilterAtom = useCreateAtom('') - -const pagination = useSelector(paginationAtom) -const sorting = useSelector(sortingAtom) -const columnFilters = useSelector(columnFiltersAtom) -const globalFilter = useSelector(globalFilterAtom) - -const { data } = useSomeServerFetcher({ - queryKey: ['rows', pagination, sorting, columnFilters, globalFilter], - queryFn: () => - fetchRows({ pagination, sorting, columnFilters, globalFilter }), -}) - -const table = useTable({ - features, // no row-model factories — server owns every stage - columns, - data: data?.rows ?? EMPTY, - rowCount: data?.rowCount, - atoms: { - pagination: paginationAtom, - sorting: sortingAtom, - columnFilters: columnFiltersAtom, - globalFilter: globalFilterAtom, - }, - manualPagination: true, - manualSorting: true, - manualFiltering: true, -}) -``` - -When `manualFiltering: true`, both `columnFilters` and the global filter are treated as server-owned. - -## Common Mistakes - -### CRITICAL Setting `manualPagination` without `rowCount` - -Wrong: - -```tsx -useTable({ - features, - columns, - data: response?.rows ?? [], - atoms: { pagination: paginationAtom }, - manualPagination: true, - // no rowCount -}) -table.getPageCount() // Infinity / wrong -``` - -Correct: - -```tsx -useTable({ - features, - columns, - data: response?.rows ?? [], - rowCount: response?.rowCount, - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -Without `rowCount` the table cannot know how many pages exist. -Source: `examples/preact/with-tanstack-query/src/main.tsx`. - -### HIGH Keeping a client-side row-model factory when the server owns that stage - -Wrong: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), // still runs; wasted work -}) -useTable({ features, data: server.rows, manualPagination: true }) -``` - -Correct: - -```tsx -const features = tableFeatures({ rowPaginationFeature }) // no paginatedRowModel -useTable({ - features, - data: server.rows, - rowCount: server.rowCount, - manualPagination: true, -}) -``` - -With `manualPagination`, the paginated row model has nothing useful to do; omit it from `tableFeatures`. Same for `sortedRowModel` under `manualSorting`, `filteredRowModel` under `manualFiltering`. -Source: `examples/preact/with-tanstack-query/src/main.tsx`. - -### HIGH Forgetting to key the request on the slices the server owns - -Wrong: - -```tsx -const { data } = useQuery({ - queryKey: ['rows'], // never changes - queryFn: () => fetchRows(pagination), -}) -``` - -Correct: - -```tsx -const { data } = useQuery({ - queryKey: ['rows', pagination, sorting, columnFilters, globalFilter], - queryFn: () => - fetchRows({ pagination, sorting, columnFilters, globalFilter }), -}) -``` - -The request must vary by the slice values; otherwise the fetcher cache returns stale data when the user sorts or pages. -Source: `examples/preact/with-tanstack-query/src/main.tsx`. - -### HIGH Page flashes empty between fetches - -Wrong: the request resolves to `undefined` while loading, so the table shows zero rows between pages. - -Correct: pass a stable `defaultData` and (with @tanstack/preact-query) `placeholderData: keepPreviousData`. The table re-uses the last page's rows during the fetch. - -```tsx -import { keepPreviousData } from '@tanstack/preact-query' - -const defaultData = useMemo(() => [], []) - -const { data } = useQuery({ - queryKey: ['rows', pagination], - queryFn: () => fetchRows(pagination), - placeholderData: keepPreviousData, -}) - -const table = useTable({ - features, - columns, - data: data?.rows ?? defaultData, - rowCount: data?.rowCount, - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -Source: `examples/preact/with-tanstack-query/src/main.tsx`. - -### MEDIUM Removing the matching feature from `features` - -Wrong: - -```tsx -const features = tableFeatures({}) // dropped rowPaginationFeature -useTable({ - features, - data: server.rows, - rowCount: server.rowCount, - manualPagination: true, -}) -table.setPageIndex(0) // type error / no-op -``` - -Correct: keep the feature registered. `manualPagination: true` only tells the row-model pipeline to skip slicing — you still want the pagination state slice and `setPageIndex` / `nextPage` APIs. - -```tsx -const features = tableFeatures({ rowPaginationFeature }) -``` - -Source: `docs/guide/features.md`. - -## See Also - -- `tanstack-table/preact/compose-with-tanstack-query` — full @tanstack/preact-query recipe with keepPreviousData and refetch ergonomics. -- `tanstack-table/preact/table-state` — atoms vs state, table.Subscribe. -- `tanstack-table/pagination`, `tanstack-table/filtering`, `tanstack-table/sorting` — feature-level state shapes. diff --git a/packages/preact-table/skills/preact/compose-with-tanstack-form/SKILL.md b/packages/preact-table/skills/preact/compose-with-tanstack-form/SKILL.md deleted file mode 100644 index 60a69dfafd..0000000000 --- a/packages/preact-table/skills/preact/compose-with-tanstack-form/SKILL.md +++ /dev/null @@ -1,228 +0,0 @@ ---- -name: preact/compose-with-tanstack-form -description: > - Editable cells with `@tanstack/preact-form`. The table is the layout - primitive; the form owns the state. Use `createFormHook` to register - reusable field components (`TextField`, `NumberField`, `SelectField`), and - in each column's `cell` renderer return the matching field component bound - to that row's accessor. Row identity (via `getRowId`) keeps field state - stable as rows resort / re-filter. Routing keywords: preact-form, editable - cells, inline editing, createFormHook, FieldGroup, getRowId. -type: composition -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - row-selection - - column-definitions -sources: - - TanStack/table:examples/react/with-tanstack-form/ - - TanStack/table:docs/framework/preact/preact-table.md ---- - -This skill is the Preact recipe for editable cells via @tanstack/preact-form. The Preact-Form API mirrors the React-Form API closely; the table half of the recipe is what you'd write in vanilla Preact + Table v9. - -> **No dedicated examples/preact/with-tanstack-form yet** — the reference implementation lives under `examples/react/with-tanstack-form/` and ports line-for-line to Preact. The patterns below are the supported integration shape. - -## Install - -```bash -npm install @tanstack/preact-form @tanstack/preact-table -``` - -Peer dependency: `preact >=10`. - -## The Division of Labor - -| Concern | Owner | -| ------------------------------------------------ | ------------------ | -| Layout (rows, columns, headers) | Table | -| Cell rendering API | Table | -| Sorting / filtering / pagination | Table | -| Row identity | Table (`getRowId`) | -| Field state (value, errors, touched, validation) | Form | -| Form-level submit handler | Form | - -The table never owns cell values for the purposes of editing — it renders fields, the form holds the values, and on submit you read from the form snapshot. - -## Pattern — `createFormHook` + field-component cells - -Define reusable field components once. Compose them with `createFormHook` to get a typed `useAppForm`. In each editable column's `cell` renderer, plug the field component into `form.AppField` keyed by the row id. - -```tsx -import { createFormHook, createFormHookContexts } from '@tanstack/preact-form' - -// Field components (one-off — text input, number input, etc.). -function TextField({ field }) { - return ( - field.handleChange((e.target as HTMLInputElement).value)} - onBlur={field.handleBlur} - /> - ) -} - -function NumberField({ field }) { - return ( - - field.handleChange(Number((e.target as HTMLInputElement).value)) - } - onBlur={field.handleBlur} - /> - ) -} - -export const { fieldContext, formContext } = createFormHookContexts() - -export const { useAppForm } = createFormHook({ - fieldContext, - formContext, - fieldComponents: { TextField, NumberField }, - formComponents: {}, -}) -``` - -Use the form per-row, keyed by `row.id`. Tables built with `getRowId` keep the same row id across re-sorts and refilters, so the form state stays attached to the same logical row. - -```tsx -import { - createColumnHelper, - tableFeatures, - useTable, -} from '@tanstack/preact-table' -import { useAppForm } from './form-hook' - -type Person = { id: string; firstName: string; age: number } - -const features = tableFeatures({}) -const columnHelper = createColumnHelper() - -function EditableTable({ data }: { data: Person[] }) { - const table = useTable({ - features, - columns: columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First Name', - cell: ({ row, getValue }) => ( - - ), - }), - columnHelper.accessor('age', { - header: 'Age', - cell: ({ row, getValue }) => ( - - ), - }), - ]), - data, - getRowId: (row) => row.id, // CRITICAL — keeps form state attached to a row identity - }) - - return ( - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((cell) => ( - - ))} - - ))} - -
- -
- ) -} - -// One small form per row. -function RowFieldCell({ - rowId, - field, - defaultValue, - kind, -}: { - rowId: string - field: string - defaultValue: unknown - kind: 'text' | 'number' -}) { - const form = useAppForm({ - defaultValues: { [field]: defaultValue }, - onSubmit: async ({ value }) => { - await saveRow(rowId, value) - }, - }) - - return ( - - {(f) => (kind === 'text' ? : )} - - ) -} -``` - -The bulk-edit alternative is a single top-level form with `` per cell. Either pattern works; pick whichever matches your save shape. - -## Common Mistakes - -### CRITICAL Forgetting `getRowId` - -Wrong: - -```tsx -useTable({ features, columns, data /* no getRowId */ }) -``` - -Correct: - -```tsx -useTable({ - features, - columns, - data, - getRowId: (row) => row.id, -}) -``` - -Without `getRowId`, the table assigns positional ids. Sorting or filtering changes row ids, and per-row form state ends up bound to a different logical row. -Source: `docs/framework/preact/guide/table-state.md`. - -### HIGH Storing editable values in the table state instead of the form - -Wrong: putting per-cell drafts in `table.atoms` slices, or in `table.options.data`. - -Correct: leave the table data immutable; let the form hold the in-flight values. Only update the table data on save (refresh from server or splice in the new row). - -### HIGH Re-rendering the entire table on every keystroke - -Wrong: the top-level `useTable` selects every form draft state slice. - -Correct: form field state is held in the form, not the table. The table re-renders only when actual table state changes. Field re-renders happen inside `` automatically. - -### MEDIUM Reimplementing form validation by hand - -Wrong: ad hoc `onChange` per field with validation logic in each cell. - -Correct: use `@tanstack/preact-form`'s validators (`validators: { onChange: schema }`). The form handles touched / dirty / error state; the table just renders the field. - -## See Also - -- `tanstack-table/preact/table-state` — Subscribe / atoms / FlexRender. -- `tanstack-table/row-selection` — combining row selection with bulk edit. -- `tanstack-table/column-definitions` — column helper with TFeatures. diff --git a/packages/preact-table/skills/preact/compose-with-tanstack-pacer/SKILL.md b/packages/preact-table/skills/preact/compose-with-tanstack-pacer/SKILL.md deleted file mode 100644 index cf94393a09..0000000000 --- a/packages/preact-table/skills/preact/compose-with-tanstack-pacer/SKILL.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -name: preact/compose-with-tanstack-pacer -description: > - Use `@tanstack/preact-pacer` to debounce / throttle the high-frequency writes - that drive an interactive table: column filter inputs and column resize - state. Pattern: import `useDebouncedCallback` from - `@tanstack/preact-pacer/debouncer` (or `useThrottledCallback` for resize), - wrap your `column.setFilterValue` / `table.setColumnSizing` calls, and let - the table's expensive row-model recompute happen on the trailing edge. - Routing keywords: preact-pacer, debounce filter, throttle resize, useDebouncedCallback, - performant filtering. -type: composition -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - filtering - - column-layout -sources: - - TanStack/table:examples/preact/filters/ - - TanStack/table:examples/preact/column-resizing-performant/ - - TanStack/table:examples/react/with-tanstack-form/ ---- - -Filtering and column resizing fire a lot of events. Each `column.setFilterValue(...)` invalidates `filteredRowModel`, then `paginatedRowModel`, then re-renders subscribed components. For large tables that is the difference between a snappy UI and a janky one. Debounce or throttle the writes with @tanstack/preact-pacer. - -## Install - -```bash -npm install @tanstack/preact-pacer -``` - -## Pattern 1 — Debounced column filter input - -Render the input value from local state (immediate visual feedback) and push the value into the table on a debounce. - -```tsx -import { useState } from 'preact/hooks' -import { useDebouncedCallback } from '@tanstack/preact-pacer/debouncer' -import type { Column } from '@tanstack/preact-table' - -function FilterInput({ - column, -}: { - column: Column -}) { - const initial = (column.getFilterValue() as string | undefined) ?? '' - const [value, setValue] = useState(initial) - - const pushToTable = useDebouncedCallback( - (next: string) => column.setFilterValue(next), - { wait: 250 }, - ) - - return ( - { - const next = (e.target as HTMLInputElement).value - setValue(next) - pushToTable(next) - }} - placeholder="Search…" - /> - ) -} -``` - -The local `value` keeps the cursor and typing responsive; `pushToTable` only runs on the trailing edge, so the table only recomputes the filtered row model once per pause. - -Same pattern works for the global filter via `table.setGlobalFilter`. - -Source: `examples/preact/filters/`. - -## Pattern 2 — Throttled column resize - -Column resize fires per-mousemove. Throttling keeps the resize visually smooth without re-running the full layout on every pixel. - -```tsx -import { useThrottledCallback } from '@tanstack/preact-pacer/throttler' - -function ResizeHandle({ header, table }) { - const pushSize = useThrottledCallback( - (size: number) => { - table.setColumnSizing((prev) => ({ ...prev, [header.column.id]: size })) - }, - { wait: 16 }, // ~60fps - ) - - // Hook this into your existing pointermove handler. - return null -} -``` - -In v9, column sizing is driven by `columnSizingFeature` + `table.setColumnSizing`. The throttle wraps the write side; the read side stays direct. - -Source: `examples/preact/column-resizing-performant/`. - -## When NOT to debounce - -- Sorting (`table.setSorting`) — fires once per click. -- Pagination (`table.setPageIndex`, `table.setPageSize`) — fires once per page. -- Row selection (`row.toggleSelected`) — fires once per toggle. - -Debouncing these adds latency for no win. Reach for pacer only on input/resize/scroll-driven writes. - -## With External Atoms - -If you've moved a slice to an external atom, debounce the atom write instead of the table API. - -```tsx -const globalFilterAtom = useCreateAtom('') - -const pushFilter = useDebouncedCallback( - (next: string) => globalFilterAtom.set(next), - { wait: 250 }, -) -``` - -The table reads from the atom; the atom now changes less often; the row model recomputes less often. - -## Common Mistakes - -### CRITICAL Wrapping `column.setFilterValue` AND reading filter via `column.getFilterValue()` in the same input - -Wrong: - -```tsx -const debouncedSet = useDebouncedCallback( - (v: string) => column.setFilterValue(v), - { wait: 250 }, -) -return ( - debouncedSet((e.target as HTMLInputElement).value)} - /> -) -``` - -Correct: hold local state for the visual `value`, debounce the write. - -```tsx -const [v, setV] = useState((column.getFilterValue() ?? '') as string) -const debouncedSet = useDebouncedCallback( - (next: string) => column.setFilterValue(next), - { wait: 250 }, -) - -return ( - { - const next = (e.target as HTMLInputElement).value - setV(next) - debouncedSet(next) - }} - /> -) -``` - -Otherwise the input lags by the debounce wait — the user sees stale characters. - -### HIGH Debouncing the wrong direction - -Wrong: debouncing the read (`column.getFilterValue`), which is just a memoized derivation. - -Correct: debounce the **write** (`column.setFilterValue`) — that's what triggers the row-model recompute. - -### HIGH Treating pacer as a substitute for stable references - -Wrong: debouncing every `useTable` call. - -Correct: pacer doesn't fix unstable `features` / `columns` / `data` references. Stabilize those first; reach for pacer for input/scroll/resize hotspots. -Source: `docs/framework/preact/guide/table-state.md` (FAQ #1). - -### MEDIUM Forgetting that debounce delays the trailing edge - -The first keystroke still hits the table at the trailing edge of the debounce window. If you want a leading-edge call (e.g. fire immediately, then debounce subsequent calls), use the `{ leading: true, trailing: true }` shape. - -## See Also - -- `tanstack-table/filtering` — column / global filter state shape. -- `tanstack-table/column-layout` — column sizing / pinning / visibility. -- `tanstack-table/preact/production-readiness` — narrow selectors, stable refs. diff --git a/packages/preact-table/skills/preact/compose-with-tanstack-query/SKILL.md b/packages/preact-table/skills/preact/compose-with-tanstack-query/SKILL.md deleted file mode 100644 index 946cd648a1..0000000000 --- a/packages/preact-table/skills/preact/compose-with-tanstack-query/SKILL.md +++ /dev/null @@ -1,285 +0,0 @@ ---- -name: preact/compose-with-tanstack-query -description: > - Server-side / async data flow with `@tanstack/preact-query`. Key the query on - the table state that drives the request (pagination + sort + filters), pass - `placeholderData: keepPreviousData` to avoid a "0 rows flash" between pages, - set `manualPagination` / `manualSorting` / `manualFiltering` for the slices - the server owns, supply `rowCount`, and let `table.set*` writes to external - atoms re-key the query. Routing keywords: preact-query, server pagination, - keepPreviousData, useQuery, manualPagination, rowCount, fetchData. -type: composition -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - preact/client-to-server - - pagination - - state-management -sources: - - TanStack/table:examples/preact/with-tanstack-query/src/main.tsx - - TanStack/table:examples/preact/with-tanstack-query/src/fetchData.ts - - TanStack/table:docs/framework/preact/guide/table-state.md ---- - -This skill is the @tanstack/preact-query recipe for server-side tables. Read `tanstack-table/preact/client-to-server` first for the manual-mode mechanics; this skill is the Preact Query-specific wiring on top. - -## Install - -```bash -npm install @tanstack/preact-query @tanstack/preact-table @tanstack/preact-store -``` - -## The Standard Recipe - -Own the slices that drive the request with external atoms. Read them with `useSelector` so the `queryKey` is reactive. Pass them through `options.atoms`. Set `manual*` for the slices the server owns. Use `placeholderData: keepPreviousData` so pagination doesn't flash empty. - -```tsx -import { useMemo, useReducer } from 'preact/hooks' -import { render } from 'preact' -import { - QueryClient, - QueryClientProvider, - keepPreviousData, - useQuery, -} from '@tanstack/preact-query' -import { useCreateAtom, useSelector } from '@tanstack/preact-store' -import { - createColumnHelper, - rowPaginationFeature, - tableFeatures, - useTable, - type PaginationState, -} from '@tanstack/preact-table' -import { fetchData } from './fetchData' -import type { Person } from './fetchData' - -const queryClient = new QueryClient() -const features = tableFeatures({ rowPaginationFeature }) -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First Name', - cell: (info) => info.getValue(), - }), - columnHelper.accessor('lastName', { header: 'Last Name' }), - columnHelper.accessor('age', { header: 'Age' }), -]) - -function App() { - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - const pagination = useSelector(paginationAtom) - - const dataQuery = useQuery({ - queryKey: ['data', pagination], - queryFn: () => fetchData(pagination), - placeholderData: keepPreviousData, // no "0 rows" flash between pages - }) - - const defaultData = useMemo(() => [], []) - - const table = useTable( - { - features, - columns, - data: dataQuery.data?.rows ?? defaultData, - rowCount: dataQuery.data?.rowCount, - atoms: { pagination: paginationAtom }, - manualPagination: true, // server owns slicing; no row-model factory needed - }, - (state) => state, - ) - - // table.nextPage() writes to paginationAtom → queryKey changes → refetch. - return null -} - -render( - - - , - document.getElementById('root')!, -) -``` - -Source: `examples/preact/with-tanstack-query/src/main.tsx`. - -## Server `fetchData` Shape - -The fetcher returns the page of rows plus the total row count so `table.getPageCount()` is correct. - -```ts -// fetchData.ts -import type { PaginationState } from '@tanstack/preact-table' - -export type Person = { - firstName: string - lastName: string - age: number /* … */ -} - -export async function fetchData(pagination: PaginationState): Promise<{ - rows: Person[] - rowCount: number -}> { - // server returns the current page and the total count - const res = await fetch( - `/api/people?page=${pagination.pageIndex}&size=${pagination.pageSize}`, - ) - return res.json() -} -``` - -Source: `examples/preact/with-tanstack-query/src/fetchData.ts`. - -## Adding Sorting and Filters - -Add more external atoms; include them in the `queryKey`; set the matching `manual*` flag. The server's fetcher accepts whatever shape you forward. - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - globalFilteringFeature, -}) - -const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, -}) -const sortingAtom = useCreateAtom([]) -const columnFiltersAtom = useCreateAtom([]) -const globalFilterAtom = useCreateAtom('') - -const pagination = useSelector(paginationAtom) -const sorting = useSelector(sortingAtom) -const columnFilters = useSelector(columnFiltersAtom) -const globalFilter = useSelector(globalFilterAtom) - -const dataQuery = useQuery({ - queryKey: ['data', pagination, sorting, columnFilters, globalFilter], - queryFn: () => - fetchData({ pagination, sorting, columnFilters, globalFilter }), - placeholderData: keepPreviousData, -}) - -const table = useTable({ - features, - columns, - data: dataQuery.data?.rows ?? defaultData, - rowCount: dataQuery.data?.rowCount, - atoms: { - pagination: paginationAtom, - sorting: sortingAtom, - columnFilters: columnFiltersAtom, - globalFilter: globalFilterAtom, - }, - manualPagination: true, - manualSorting: true, - manualFiltering: true, -}) -``` - -## Common Mistakes - -### CRITICAL `manualPagination` without `rowCount` - -Wrong: - -```tsx -useTable({ - /* … */, - data: dataQuery.data?.rows ?? defaultData, - manualPagination: true, - atoms: { pagination: paginationAtom }, - // no rowCount -}) -table.getPageCount() // Infinity -``` - -Correct: always pass `rowCount: dataQuery.data?.rowCount`. -Source: `examples/preact/with-tanstack-query/src/main.tsx`. - -### CRITICAL `queryKey` that doesn't include reactive table state - -Wrong: - -```tsx -useQuery({ - queryKey: ['data'], - queryFn: () => fetchData(pagination), -}) -``` - -Correct: - -```tsx -useQuery({ - queryKey: ['data', pagination /* + sorting, filters, etc. */], - queryFn: () => fetchData(pagination), -}) -``` - -The query cache must vary by the slice values, or you'll fetch once and never refresh on user interaction. -Source: `examples/preact/with-tanstack-query/src/main.tsx`. - -### HIGH Missing `placeholderData: keepPreviousData` - -Wrong: data goes `undefined` between pages; the table flashes empty. - -Correct: include `placeholderData: keepPreviousData` so the table keeps the last page rendered until the new page resolves. -Source: `examples/preact/with-tanstack-query/src/main.tsx`. - -### HIGH Inline `data: dataQuery.data?.rows ?? []` - -Wrong: - -```tsx -useTable({ /* … */, data: dataQuery.data?.rows ?? [] }) // new [] every render -``` - -Correct: - -```tsx -const defaultData = useMemo(() => [], []) -useTable({ /* … */, data: dataQuery.data?.rows ?? defaultData }) -``` - -A new empty array each render busts row-model memos. - -### HIGH Keeping a client-side row-model factory when the server owns that stage - -Wrong: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), // wasted work when manual -}) -useTable({ features, /* … */, manualPagination: true }) -``` - -Correct: omit the row-model factory for stages the server owns. With `manualPagination: true`, the server returns the page slice already. - -```tsx -const features = tableFeatures({ rowPaginationFeature }) // no paginatedRowModel -useTable({ features, /* … */, manualPagination: true }) -``` - -### MEDIUM Creating a new `paginationAtom` per render - -Wrong: `createAtom(...)` inside the component body. - -Correct: `useCreateAtom(...)` (or atom at module scope). -Source: `examples/preact/basic-external-atoms/src/main.tsx`. - -## See Also - -- `tanstack-table/preact/client-to-server` — manual-mode mechanics independent of any specific async lib. -- `tanstack-table/preact/compose-with-tanstack-store` — slice atoms and sharing state. -- `tanstack-table/preact/production-readiness` — narrow selectors, stable refs. diff --git a/packages/preact-table/skills/preact/compose-with-tanstack-store/SKILL.md b/packages/preact-table/skills/preact/compose-with-tanstack-store/SKILL.md deleted file mode 100644 index a8995b306d..0000000000 --- a/packages/preact-table/skills/preact/compose-with-tanstack-store/SKILL.md +++ /dev/null @@ -1,259 +0,0 @@ ---- -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.` (per-slice readonly), `table.store` (flat readonly - view), and `table.baseAtoms.` (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.` | `ReadonlyAtom` per slice | Read or subscribe to one slice (preferred) | -| `table.store` | `ReadonlyStore>` (flat derived view) | Reading full table state, selecting projections | -| `table.baseAtoms.` | `Atom` (writable internal) | Low-level writes when the slice is internally owned | - -External atoms passed via `options.atoms.` take precedence over `options.state[]` and over `table.baseAtoms.`. 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 - -```tsx -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 }) { - // Stable atoms owned by this component. - const sortingAtom = useCreateAtom([]) - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - - // Independent reactive reads — only re-renders for the slice that changed. - const sorting = useSelector(sortingAtom) - const pagination = useSelector(paginationAtom) - - const table = useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom, pagination: paginationAtom }, - }) - - // table.setSorting / table.setPageIndex write to your atoms. - 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. - -```tsx -// shared/atoms.ts -import { createAtom } from '@tanstack/preact-store' -import type { RowSelectionState } from '@tanstack/preact-table' - -export const selectionAtom = createAtom({}) -``` - -```tsx -// TableScreen.tsx -import { selectionAtom } from '../shared/atoms' - -const table = useTable({ - features, - columns, - data, - atoms: { rowSelection: selectionAtom }, -}) -``` - -```tsx -// SelectionSummary.tsx -import { useSelector } from '@tanstack/preact-store' -import { selectionAtom } from '../shared/atoms' - -function SelectionSummary() { - const sel = useSelector(selectionAtom) - return {Object.keys(sel).length} selected -} -``` - -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. - -```tsx -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.` 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 `` - -Inside a cell or header render context, `table` is the core `Table`, not `PreactTable`. `table.Subscribe` is undefined — import the standalone component. - -```tsx -import { Subscribe } from '@tanstack/preact-table' - -columnHelper.display({ - id: 'select', - cell: ({ row, table }) => ( - rs[row.id]}> - {(isSelected) => ( - - )} - - ), -}) -``` - -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: - -```tsx -function MyTable() { - const sortingAtom = createAtom([]) // new atom every render - useTable({ /* … */, atoms: { sorting: sortingAtom } }) -} -``` - -Correct: - -```tsx -function MyTable() { - const sortingAtom = useCreateAtom([]) // stable - 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: - -```tsx -useTable({ /* … */, atoms: { pagination: paginationAtom } }) -table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 })) // updates the wrong atom -``` - -Correct: - -```tsx -// Use the feature API (writes to whichever atom owns the slice). -table.setPageIndex(0) -// Or write to your external atom directly. -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: - -```tsx -function Pager() { - const { pageIndex } = table.atoms.pagination.get() // current-value read - return Page {pageIndex + 1} -} -``` - -Correct: - -```tsx -import { useSelector } from '@tanstack/preact-store' - -function Pager() { - const pageIndex = useSelector(table.atoms.pagination, (p) => p.pageIndex) - return Page {pageIndex + 1} -} -// or -; p.pageIndex}> - {(pageIndex) => Page {pageIndex + 1}} - -``` - -`.get()` returns the current value without subscribing. - -### MEDIUM Passing the same slice via `atoms` AND `state` - -Wrong: - -```tsx -useTable({ - /* … */, - atoms: { pagination: paginationAtom }, - state: { pagination }, // silently ignored - onPaginationChange: setPagination, // silently ignored -}) -``` - -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. diff --git a/packages/preact-table/skills/preact/compose-with-tanstack-virtual/SKILL.md b/packages/preact-table/skills/preact/compose-with-tanstack-virtual/SKILL.md deleted file mode 100644 index d7eea38f53..0000000000 --- a/packages/preact-table/skills/preact/compose-with-tanstack-virtual/SKILL.md +++ /dev/null @@ -1,278 +0,0 @@ ---- -name: preact/compose-with-tanstack-virtual -description: > - TanStack Table does NOT include virtualization — pair with TanStack Virtual. - Preact has no dedicated `@tanstack/preact-virtual` adapter yet; use - `@tanstack/virtual-core`'s `Virtualizer` class behind a small hook, or use - the React adapter via `preact/compat`. Pattern: get `rows = table.getRowModel().rows`, - feed `rows.length` to the virtualizer, render only virtual items, and use - CSS transforms for row positioning. Routing keywords: preact virtualization, - large table, virtual rows, virtual-core, getVirtualItems, table-core. -type: composition -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - preact/table-state - - row-expanding -sources: - - TanStack/table:docs/guide/virtualization.md - - TanStack/table:examples/lit/virtualized-rows/src/main.ts - - TanStack/table:examples/react/virtualized-rows/ - - TanStack/table:examples/react/virtualized-columns/ ---- - -TanStack Table is headless — it does not virtualize rows or columns. For long lists, pair the table with TanStack Virtual. - -> **Adapter status:** There is no published `@tanstack/preact-virtual` adapter as of `@tanstack/table` v9.0.0-alpha.48. The two supported paths are: -> -> 1. **Use `@tanstack/virtual-core` directly.** The framework-agnostic `Virtualizer` class wrapped in a small Preact hook is the recommended path. -> 2. **Use `@tanstack/react-virtual` via `preact/compat`.** Works if your project already aliases `react` → `preact/compat`. -> -> The patterns below use path 1. Both paths feed the same `rows = table.getRowModel().rows` array to the virtualizer. - -## Install - -```bash -npm install @tanstack/virtual-core -``` - -## The Pattern (Row Virtualization) - -1. Build the table with `useTable` as usual. -2. Get `const { rows } = table.getRowModel()` — the table is the source of truth for which rows to render (already sorted, filtered, paginated, etc.). -3. Pass `rows.length` to the virtualizer. -4. Render only `virtualizer.getVirtualItems()`. -5. Each virtual row is absolutely positioned via `transform: translateY(${item.start}px)`. - -```tsx -import { useEffect, useMemo, useRef, useState } from 'preact/hooks' -import { - Virtualizer, - observeElementOffset, - observeElementRect, - elementScroll, -} from '@tanstack/virtual-core' -import { - useTable, - createSortedRowModel, - rowSortingFeature, - sortFns, - tableFeatures, -} from '@tanstack/preact-table' -import type { JSX } from 'preact' - -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -// Minimal Preact hook around the framework-agnostic Virtualizer. -function useVirtualizer( - options: Omit< - ConstructorParameters>[0], - 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' - > & { onChange?: (instance: Virtualizer) => void }, -) { - const [, force] = useState(0) - const virtualizer = useMemo( - () => - new Virtualizer({ - ...options, - observeElementRect, - observeElementOffset, - scrollToFn: elementScroll, - onChange: (inst) => { - options.onChange?.(inst) - force((n) => n + 1) - }, - }), - [], - ) - - // Sync count / estimateSize / overscan when they change. - virtualizer.setOptions({ - ...virtualizer.options, - count: options.count, - estimateSize: options.estimateSize, - overscan: options.overscan, - }) - - useEffect(() => { - return virtualizer._didMount() - }, [virtualizer]) - - useEffect(() => { - virtualizer._willUpdate() - }) - - return virtualizer -} - -function BigTable({ data }) { - const table = useTable( - { - features, - columns, - data, - }, - () => null, // huge table — opt out at the top, subscribe lower down - ) - - const { rows } = table.getRowModel() - - const scrollRef = useRef(null) - - const rowVirtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => scrollRef.current!, - estimateSize: () => 33, - overscan: 5, - }) - - return ( -
- - - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((h) => ( - - ))} - - ))} - - - - {rowVirtualizer.getVirtualItems().map((virtualItem) => { - const row = rows[virtualItem.index] - return ( - rowVirtualizer.measureElement(node!)} - data-index={virtualItem.index} - style={{ - display: 'flex', - position: 'absolute', - transform: `translateY(${virtualItem.start}px)`, - width: '100%', - }} - > - {row.getAllCells().map((cell) => ( - - ))} - - ) - })} - -
- -
- -
-
- ) -} -``` - -The structure matches the Lit virtualized-rows example one-for-one; only the host framework changes. -Source: `examples/lit/virtualized-rows/src/main.ts`; `docs/guide/virtualization.md`. - -## Column Virtualization - -Same idea, but the virtualizer's `count` is `columns.length` and you index the visible columns inside each row. Useful for wide kitchen-sink tables. - -## With Pagination / Filtering - -Use the row model that's already been transformed by registered features. The virtualizer count is `rows.length` — the table handles sorting, filtering, and pagination upstream. - -## Combining with `` - -On large tables, pass `() => null` to `useTable` (or use the standalone ``) and wrap the `` in a subscription that re-renders only when the row model can actually change. - -```tsx - ({ - columnFilters: s.columnFilters, - globalFilter: s.globalFilter, - sorting: s.sorting, - })} -> - {() => {/* virtualized rows */}} - -``` - -Source: `examples/preact/basic-subscribe/src/main.tsx`. - -## Common Mistakes - -### CRITICAL Reimplementing virtualization by hand - -Wrong: - -```tsx -// Manual slicing + intersection observer + per-row offset calculation -``` - -Correct: use TanStack Virtual's `Virtualizer` — it handles measurement, overscan, scroll alignment, and dynamic sizing. -Source: `docs/guide/virtualization.md`. - -### HIGH Using the wrong row source - -Wrong: - -```tsx -const virtualizer = useVirtualizer({ count: data.length /* … */ }) // bypasses sort/filter/paginate -``` - -Correct: - -```tsx -const { rows } = table.getRowModel() -const virtualizer = useVirtualizer({ count: rows.length /* … */ }) -``` - -Always feed `table.getRowModel().rows.length` — that's the post-feature row array. - -### HIGH Forgetting `position: relative` on the scroll parent / `position: absolute` on rows - -Wrong: - -```tsx -
- {/* rows */} -
-``` - -Correct: the absolute rows need a `position: relative` ancestor with the total height set. Without it, rows stack at the top. - -### HIGH Forgetting to set up `measureElement` for dynamic sizing - -Wrong: rows render but `estimateSize` is wrong and rows overlap or leave gaps. - -Correct: attach `ref={(node) => rowVirtualizer.measureElement(node!)}` to each row element so the virtualizer can measure actual size. - -### MEDIUM Mixing virtualization with `manualPagination` - -You usually don't need both — server pagination already limits the row count. Virtualize when the client holds the full dataset. - -## See Also - -- `tanstack-table/preact/table-state` — Subscribe for fine-grained re-renders. -- `tanstack-table/preact/production-readiness` — narrow selectors, stable refs. -- `tanstack-table/row-expanding` — virtualizing rows with sub-component rows requires variable height + measureElement. diff --git a/packages/preact-table/skills/preact/getting-started/SKILL.md b/packages/preact-table/skills/preact/getting-started/SKILL.md deleted file mode 100644 index ee01e83f04..0000000000 --- a/packages/preact-table/skills/preact/getting-started/SKILL.md +++ /dev/null @@ -1,372 +0,0 @@ ---- -name: preact/getting-started -description: > - End-to-end first-table journey for `@tanstack/preact-table` v9: install the - adapter, declare `features` via `tableFeatures()` with row model factories - and *Fns slots on the features object, build a typed column helper, call - `useTable` with stable references, and render with `table.FlexRender`. - Routing keywords: install preact-table, first table, getting started, - tableFeatures, features, useTable, FlexRender, basic-use-table. -type: lifecycle -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - setup - - column-definitions - - state-management - - preact/table-state -sources: - - TanStack/table:docs/installation.md - - TanStack/table:docs/framework/preact/preact-table.md - - TanStack/table:examples/preact/basic-use-table/src/main.tsx - - TanStack/table:packages/preact-table/src/useTable.ts ---- - -This skill walks through a first working Preact v9 table end-to-end. It assumes you have read `tanstack-table/setup` and `tanstack-table/state-management` for core concepts and `tanstack-table/preact/table-state` for adapter wiring. - -## Install - -`@tanstack/preact-table` is the Preact adapter. It pulls in `@tanstack/table-core` and `@tanstack/preact-store` (used internally for atom-backed state). - -```bash -npm install @tanstack/preact-table -``` - -Peer dependency: `preact >=10`. - -Source: `packages/preact-table/package.json`. - -## Step 1 — Declare `features` - -v9 is explicit about what a table uses. `features` is a registry of every feature the table needs. Use `tableFeatures({...})` to get an object whose TypeScript shape drives state inference, API surface, and tree-shaking. - -```tsx -import { - tableFeatures, - rowPaginationFeature, - rowSortingFeature, -} from '@tanstack/preact-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - // row model factories and *Fns slots are added here in Step 2 -}) -``` - -If `features` does not include `rowSelectionFeature`, then `table.atoms.rowSelection`, `table.setRowSelection`, `table.getIsAllRowsSelected()`, etc. all become TypeScript errors — and the runtime won't ship that logic. Pass `tableFeatures({})` for a minimum-overhead table with just the core row model. - -Source: `docs/framework/preact/preact-table.md`; `docs/guide/features.md`. - -## Step 2 — Register row model factories on `features` - -Each registered feature that needs a row-model stage maps to a factory added directly to the `tableFeatures({...})` call. Pass \*Fns maps (predicates, comparators, etc.) as slots on the same object. - -```tsx -import { - tableFeatures, - rowPaginationFeature, - rowSortingFeature, - createPaginatedRowModel, - createSortedRowModel, - sortFns, -} from '@tanstack/preact-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - sortFns, -}) -``` - -The core row model is always included. A `tableFeatures({})` call with no row-model factories is valid for a feature-free table. - -Source: `docs/framework/preact/preact-table.md`. - -## Step 3 — Type your data and build columns - -Declare your row shape once and feed it to `createColumnHelper()`. This is the type-safe path; `ColumnDef[]` also works. - -```tsx -import { createColumnHelper, type ColumnDef } from '@tanstack/preact-table' - -type Person = { - firstName: string - lastName: string - age: number - visits: number - status: string - progress: number -} - -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First Name', - cell: (info) => info.getValue(), - }), - columnHelper.accessor('lastName', { header: () => Last Name }), - columnHelper.accessor('age', { header: 'Age' }), - columnHelper.accessor('visits', { header: 'Visits' }), - columnHelper.accessor('status', { header: 'Status' }), - columnHelper.accessor('progress', { header: 'Profile Progress' }), -]) -``` - -Source: `examples/preact/basic-use-table/src/main.tsx`. - -## Step 4 — Call `useTable` and render - -`useTable` takes options and an optional selector. Render headers, cells, and footers with `table.FlexRender` so column defs can be strings or Preact components. - -```tsx -import { render } from 'preact' -import { useState } from 'preact/hooks' -import { useTable } from '@tanstack/preact-table' - -const defaultData: Person[] = [ - { - firstName: 'tanner', - lastName: 'linsley', - age: 24, - visits: 100, - status: 'In Relationship', - progress: 50, - }, - { - firstName: 'kevin', - lastName: 'vandy', - age: 12, - visits: 100, - status: 'Single', - progress: 70, - }, -] - -function App() { - const [data] = useState(() => [...defaultData]) - - const table = useTable( - { - features, - columns, - data, - debugTable: true, - }, - (state) => state, // default selector - ) - - return ( - - - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((h) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((cell) => ( - - ))} - - ))} - - - {table.getFooterGroups().map((fg) => ( - - {fg.headers.map((h) => ( - - ))} - - ))} - -
- {h.isPlaceholder ? null : } -
- -
- {h.isPlaceholder ? null : } -
- ) -} - -render(, document.getElementById('root')!) -``` - -Source: `examples/preact/basic-use-table/src/main.tsx`. - -## Step 5 — Drive features with feature APIs - -Reach for `table.setSorting(...)`, `table.setPageIndex(...)`, `table.nextPage()`, `column.toggleVisibility()`, `row.toggleSelected()`, etc. — never edit `table.state` directly. - -```tsx - - -``` - -For starting values, use `initialState`. For controlled slices, use `atoms` (preferred) or `state` + `on*Change` — see `tanstack-table/preact/table-state`. - -## Common Mistakes - -### CRITICAL Calling a feature API when the feature is not in `features` - -Wrong: - -```tsx -const features = tableFeatures({}) // no rowPaginationFeature -const table = useTable({ features, columns, data }) - -table.setPageIndex(0) // TypeScript error AND runtime no-op -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), -}) -const table = useTable({ features, columns, data }) - -table.setPageIndex(0) -``` - -v9 generates feature APIs and state slices only for registered features. The missing-feature failure mode (calling `setSorting`, accessing `state.pagination`, etc. before registering the feature) is the #1 v9 trap. -Source: `docs/guide/features.md`; `docs/framework/preact/guide/table-state.md`. - -### HIGH Forgetting the matching row-model factory - -Wrong: - -```tsx -const features = tableFeatures({ rowSortingFeature }) // no sortedRowModel -const table = useTable({ features, columns, data }) -table.setSorting([{ id: 'age', desc: true }]) -// table.getRowModel().rows is still unsorted — no sortedRowModel registered -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -``` - -Each row-model feature (sorting, filtering, pagination, grouping, expanding, faceting) requires its row-model factory registered in `tableFeatures` to actually transform the rows. -Source: `docs/framework/preact/preact-table.md`. - -### HIGH Unstable `features` / `columns` / `data` references - -Wrong: - -```tsx -function MyTable({ rows }) { - const features = tableFeatures({ rowSortingFeature }) // new every render - const columns = [ - /* … */ - ] // new every render - const data = rows ?? [] // new [] every render - const table = useTable({ features, columns, data }) -} -``` - -Correct: - -```tsx -const features = tableFeatures({ rowSortingFeature }) -const columns: ColumnDef[] = [ - /* … */ -] -const EMPTY: Person[] = [] - -function MyTable({ rows }) { - const data = rows ?? EMPTY - const table = useTable({ features, columns, data }) -} -``` - -Internal memos key off identity. Fresh references bust everything every render. -Source: `docs/framework/preact/guide/table-state.md` (FAQ #1). - -### HIGH Reimplementing built-in feature logic - -Wrong: - -```tsx -const sorted = useMemo(() => [...data].sort(/* … */), [data, sorting]) // duplicates rowSortingFeature -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -const rows = table.getRowModel().rows // already sorted -``` - -v9 ships built-ins for sorting, filtering, pagination, grouping, expanding, faceting, row selection, column visibility/order/pinning/sizing, and row pinning. Use them. -Source: `docs/guide/features.md`. - -### MEDIUM Using v8 hook names (`getCoreRowModel`, `useReactTable`, etc.) - -Wrong: - -```tsx -import { - useReactTable, - getCoreRowModel, - getSortedRowModel, -} from '@tanstack/preact-table' -const table = useReactTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), -}) -``` - -Correct: - -```tsx -import { - useTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, -} from '@tanstack/preact-table' - -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -const table = useTable({ features, columns, data }) -``` - -v8 used `useReactTable` and `get*RowModel` options. v9 uses `useTable` with `features` (which now also carries row model factories and \*Fns slots). See `tanstack-table/preact/migrate-v8-to-v9` for the full mapping. -Source: `docs/framework/preact/preact-table.md`. - -## See Also - -- `tanstack-table/preact/table-state` — atoms, Subscribe, createTableHook. -- `tanstack-table/preact/migrate-v8-to-v9` — moving an existing v8 codebase over. -- `tanstack-table/preact/production-readiness` — perf, tree-shaking, stable refs. diff --git a/packages/preact-table/skills/preact/migrate-v8-to-v9/SKILL.md b/packages/preact-table/skills/preact/migrate-v8-to-v9/SKILL.md deleted file mode 100644 index d9e7238624..0000000000 --- a/packages/preact-table/skills/preact/migrate-v8-to-v9/SKILL.md +++ /dev/null @@ -1,309 +0,0 @@ ---- -name: preact/migrate-v8-to-v9 -description: > - Mechanical breaking-change migration from TanStack Table v8 to v9 for - `@tanstack/preact-table`. Maps every old-shaped option, helper, type, and - method an agent will reproduce from v8 muscle memory to its v9 equivalent: - `useReactTable` → `useTable`, per-row-model `get*RowModel` options → - row-model factories registered in `tableFeatures({...})`, plain column - helpers → typed column helpers, `state` + `on*Change` → `atoms`, - `flexRender` → `table.FlexRender`, and core type renames. - Routing keywords: v8 to v9, migration, useReactTable, table v8 preact, - get*RowModel, features. -type: lifecycle -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - column-definitions -sources: - - TanStack/table:docs/framework/preact/preact-table.md - - TanStack/table:docs/framework/preact/guide/table-state.md - - TanStack/table:docs/framework/react/guide/use-legacy-table.md - - TanStack/table:packages/preact-table/src/useTable.ts ---- - -The Preact adapter mirrors the React v9 surface, so any v8 → v9 migration guide for React applies almost line-for-line. There is no `useLegacyTable` shim in `@tanstack/preact-table` — migrate directly. - -## The Core Mapping - -| v8 (Preact / React-compatible) | v9 (`@tanstack/preact-table`) | -| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `useReactTable(opts)` | `useTable(opts, selector?)` | -| `getCoreRowModel: getCoreRowModel()` | included by default; no factory needed | -| `getSortedRowModel: getSortedRowModel()` | `tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns })` | -| `getFilteredRowModel: getFilteredRowModel()` | `tableFeatures({ columnFilteringFeature, globalFilteringFeature, filteredRowModel: createFilteredRowModel(), filterFns })` | -| `getPaginationRowModel: getPaginationRowModel()` | `tableFeatures({ rowPaginationFeature, paginatedRowModel: createPaginatedRowModel() })` | -| `getGroupedRowModel: getGroupedRowModel()` | `tableFeatures({ columnGroupingFeature, groupedRowModel: createGroupedRowModel(), aggregationFns })` | -| `getExpandedRowModel: getExpandedRowModel()` | `tableFeatures({ rowExpandingFeature, expandedRowModel: createExpandedRowModel() })` | -| `getFacetedRowModel`, `getFacetedUniqueValues`, `getFacetedMinMaxValues` | `tableFeatures({ columnFacetingFeature, globalFacetingFeature, facetedRowModel: createFacetedRowModel(), facetedUniqueValues: createFacetedUniqueValues(), facetedMinMaxValues: createFacetedMinMaxValues() })` | -| `flexRender(def, ctx)` | `` / `header={...}` / `footer={...}` | -| `state`, `on*Change` (only) | still supported, plus `atoms` (preferred per slice) | -| `createColumnHelper()` | `createColumnHelper()` — both generics required | -| `ColumnDef` | `ColumnDef` — `TFeatures` is now the first generic | -| `Table` | `Table` | - -Source: `docs/framework/preact/preact-table.md`; `docs/framework/preact/guide/table-state.md`. - -## Migration Steps - -### 1. Update the package import - -```tsx -// v8 -import { - useReactTable, - getCoreRowModel, - getSortedRowModel, - flexRender, - type ColumnDef, -} from '@tanstack/preact-table' - -// v9 -import { - useTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, - type ColumnDef, -} from '@tanstack/preact-table' -``` - -### 2. Declare `features` with row model factories - -Replace each `get*RowModel: get*RowModel()` option with a feature import and its row-model factory registered directly in `tableFeatures({...})`. - -```tsx -// v8 -const table = useReactTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), -}) - -// v9 -const features = tableFeatures({ - rowSortingFeature, - columnFilteringFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, - filterFns, -}) - -const table = useTable({ features, columns, data }) -``` - -Move `features` to module scope. Reference stability matters — see `tanstack-table/preact/production-readiness`. - -### 3. Update column types and helpers - -`TFeatures` is now the first generic on `ColumnDef`, `Table`, and `createColumnHelper`. - -```tsx -// v8 -const columnHelper = createColumnHelper() -const columns: ColumnDef[] = columnHelper.columns([ - /* … */ -]) - -// v9 -const columnHelper = createColumnHelper() -const columns: Array> = columnHelper.columns( - [ - /* … */ - ], -) -``` - -### 4. Replace `flexRender` calls with `table.FlexRender` - -```tsx -// v8 -{flexRender(header.column.columnDef.header, header.getContext())} -{flexRender(cell.column.columnDef.cell, cell.getContext())} - -// v9 -{header.isPlaceholder ? null : } - -``` - -`flexRender` is still exported for advanced cases, but `table.FlexRender` (or the standalone `FlexRender` component) handles grouping placeholder/aggregated branches for you. - -Source: `packages/preact-table/src/FlexRender.tsx`. - -### 5. Move external state to atoms (recommended) - -`state` + `on*Change` still works, but v9 prefers slice atoms for fine-grained reactivity. - -```tsx -// v8 / v9 fallback -const [sorting, setSorting] = useState([]) -const table = useTable({ - features, - columns, - data, - state: { sorting }, - onSortingChange: setSorting, -}) - -// v9 preferred — external atom -import { useCreateAtom } from '@tanstack/preact-store' -const sortingAtom = useCreateAtom([]) -const table = useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, - // no onSortingChange needed -}) -``` - -Source: `examples/preact/basic-external-atoms/src/main.tsx`. - -### 6. Drop `onStateChange` - -The v8-style global `onStateChange` is gone. Subscribe per-slice with `on*Change`, an external atom, or `table.store.subscribe(...)` if you really need every change. - -Source: `docs/framework/preact/guide/table-state.md`. - -## Common Mistakes - -### CRITICAL Keeping `get*RowModel` options after upgrading - -Wrong: - -```tsx -const table = useTable({ - features, - columns, - data, - getSortedRowModel: getSortedRowModel(), // v8 leftover — silently ignored -}) -table.setSorting([{ id: 'age', desc: true }]) // rows are NOT sorted -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -``` - -v9 doesn't read the `get*RowModel` options. The row model only runs for stages registered in `tableFeatures`, and the feature only mounts if it is also in `tableFeatures`. -Source: `docs/framework/preact/preact-table.md`. - -### CRITICAL Forgetting to register a feature whose API you are calling - -Wrong: - -```tsx -const features = tableFeatures({}) // no rowSelectionFeature -const table = useTable({ features, columns, data }) -table.getIsAllRowsSelected() // TypeScript error / runtime no-op -row.toggleSelected(true) // same -``` - -Correct: - -```tsx -const features = tableFeatures({ rowSelectionFeature }) -const table = useTable({ features, columns, data, enableRowSelection: true }) -table.getIsAllRowsSelected() -``` - -v9 generates feature APIs and state slices only for registered features. This is the #1 v9 trap. -Source: `docs/guide/features.md`. - -### HIGH Re-using `getCoreRowModel: getCoreRowModel()` from v8 - -Wrong: - -```tsx -const table = useTable({ - features, - columns, - data, - getCoreRowModel: getCoreRowModel(), // no-op in v9 -}) -``` - -Correct: - -```tsx -const table = useTable({ features, columns, data }) -``` - -The core row model is always included in v9. There is no `getCoreRowModel` option. -Source: `docs/framework/preact/preact-table.md`. - -### HIGH Single-generic column helper / `ColumnDef` - -Wrong: - -```tsx -const columnHelper = createColumnHelper() // v8 shape -const columns: ColumnDef[] = [ - /* … */ -] // v8 shape -``` - -Correct: - -```tsx -const columnHelper = createColumnHelper() -const columns: Array> = [ - /* … */ -] -``` - -`TFeatures` is the first generic for nearly every public type in v9. Without it, types degrade to `any` for feature methods. -Source: `docs/framework/preact/preact-table.md`. - -### HIGH Reimplementing built-ins (the #1 AI tell) - -Wrong: - -```tsx -// Manual sorting, manual filtering, manual pagination, manual row-selection objects -``` - -Correct: register the matching feature and its row-model factory in `tableFeatures({...})`, then use the feature API. v9 ships built-ins for sorting, filtering, pagination, grouping, expanding, faceting, row selection, column visibility/order/pinning/sizing, and row pinning. -Source: `docs/guide/features.md`. - -### MEDIUM Calling `flexRender` directly when grouping is on - -Wrong: - -```tsx -{flexRender(cell.column.columnDef.cell, cell.getContext())} -``` - -Correct: - -```tsx - - - -``` - -`` handles aggregated / placeholder cells when `columnGroupingFeature` is registered. Raw `flexRender` does not. -Source: `packages/preact-table/src/FlexRender.tsx`. - -## See Also - -- `tanstack-table/preact/getting-started` — green-field v9 setup. -- `tanstack-table/preact/table-state` — atom model and Subscribe patterns. -- `tanstack-table/preact/production-readiness` — perf, tree-shaking, stable refs. diff --git a/packages/preact-table/skills/preact/production-readiness/SKILL.md b/packages/preact-table/skills/preact/production-readiness/SKILL.md deleted file mode 100644 index ab4abf4574..0000000000 --- a/packages/preact-table/skills/preact/production-readiness/SKILL.md +++ /dev/null @@ -1,274 +0,0 @@ ---- -name: preact/production-readiness -description: > - Ship-ready optimizations for `@tanstack/preact-table` v9: tree-shake the - bundle by registering ONLY the `features` you actually use; memoize - `features`, `data`, and `columns` for stable identity; replace - `(state) => state` with narrow selectors or per-slice `useSelector` - subscriptions; wrap hot subtrees in ``; and prefer slice - atoms over `state` + `on*Change` for fine-grained updates. Routing keywords: - preact-table performance, optimization, tree-shaking, stable refs, Subscribe, - narrow selector. -type: lifecycle -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - preact/table-state -sources: - - TanStack/table:docs/guide/features.md - - TanStack/table:docs/framework/preact/guide/table-state.md - - TanStack/table:examples/preact/basic-subscribe/src/main.tsx - - TanStack/table:examples/preact/basic-external-atoms/src/main.tsx - - TanStack/table:packages/preact-table/src/useTable.ts ---- - -This skill collects the production-readiness levers for a Preact v9 table. Each one is independent — apply only the ones whose problem you actually have. - -## 1. Tree-Shake `features` - -Only register features the table actually uses. v9's bundle savings come from `features` controlling which feature code (and which state slices and APIs) get included. - -```tsx -// Bad — pulls every feature into the bundle even if the UI never uses them. -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - rowSelectionFeature, - columnFilteringFeature, - globalFilteringFeature, - columnFacetingFeature, - globalFacetingFeature, - columnGroupingFeature, - rowExpandingFeature, - columnSizingFeature, - columnVisibilityFeature, - columnOrderingFeature, - columnPinningFeature, - rowPinningFeature, -}) - -// Good — feature list matches what the UI exposes. -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - rowSelectionFeature, -}) -``` - -The same idea applies to row-model factories: only register factories for features that need one and that you have registered, by including them in the same `tableFeatures({...})` call. - -Source: `docs/guide/features.md`; `docs/framework/preact/preact-table.md`. - -## 2. Stable References for `features`, `columns`, and `data` - -Identity drives every internal memo. Declare these at module scope when possible; otherwise wrap with `useMemo`. - -```tsx -// Best — module scope. Single allocation. -const features = tableFeatures({ rowSortingFeature }) -const columns: Array> = [ - /* … */ -] -const EMPTY: Person[] = [] - -function MyTable({ rows }: { rows: Person[] | undefined }) { - const data = rows ?? EMPTY - const table = useTable({ features, columns, data }) -} - -// Okay — useMemo for dynamic columns. -function MyTable({ visibleKeys }: { visibleKeys: string[] }) { - const columns = useMemo( - () => visibleKeys.map((k) => columnHelper.accessor(k as any, {})), - [visibleKeys.join(',')], - ) -} -``` - -Source: `docs/framework/preact/guide/table-state.md` (FAQ #1). - -## 3. Narrow `useTable` Selector - -The default selector `(state) => state` re-renders the component on any registered slice change. Narrow it to just the slices the component reads. The Preact adapter uses `shallow` compare from `@tanstack/preact-store` — projected objects only trigger a render when a member changes. - -```tsx -// All slices — fine for a small table. -const table = useTable(opts, (state) => state) - -// Narrow — re-render only on sorting/pagination changes. -const table = useTable(opts, (state) => ({ - sorting: state.sorting, - pagination: state.pagination, -})) -table.state.pagination - -// Opt-out at the parent; do subscriptions lower in the tree. -const table = useTable(opts, () => null) -``` - -Source: `examples/preact/basic-subscribe/src/main.tsx`. - -## 4. Wrap Hot Subtrees in `` - -Once the parent uses `() => null`, push subscriptions next to the UI that actually reads them. Subscribe to single atoms (`source={table.atoms.X}`) to avoid re-deriving the flat store on unrelated changes. - -```tsx -const table = useTable(opts, () => null) - -// Row body — re-render only when filters/pagination cause the row model to change. - ({ - columnFilters: s.columnFilters, - globalFilter: s.globalFilter, - pagination: s.pagination, - })} -> - {() => ( - - {table.getRowModel().rows.map((row) => ( - {/* … */} - ))} - - )} - - -// Per-row selection checkbox — narrow to that row's selection bit. - rs[row.id]} -> - {(isSelected) => ( - - )} - -``` - -Source: `examples/preact/basic-subscribe/src/main.tsx`. - -## 5. Prefer Slice Atoms over `state` + `on*Change` - -External `state` + `on*Change` re-renders the whole component that owns the `useState`. Slice atoms let `useSelector` / `` subscribe individually. - -```tsx -// Less granular — every slice change re-renders this component. -const [sorting, setSorting] = useState([]) -const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }) -useTable({ - /* … */, - state: { sorting, pagination }, - onSortingChange: setSorting, - onPaginationChange: setPagination, -}) - -// More granular — independent atom subscriptions. -const sortingAtom = useCreateAtom([]) -const paginationAtom = useCreateAtom({ pageIndex: 0, pageSize: 10 }) -useTable({ /* … */, atoms: { sorting: sortingAtom, pagination: paginationAtom } }) -``` - -Source: `examples/preact/basic-external-atoms/src/main.tsx`. - -## 6. Set Sensible `initialState` Once - -Use `initialState` for starting values. Setting state in an effect after mount triggers an extra render. - -```tsx -const table = useTable({ - features, - columns, - data, - initialState: { - pagination: { pageIndex: 0, pageSize: 25 }, - sorting: [{ id: 'createdAt', desc: true }], - }, -}) -``` - -Source: `docs/framework/preact/guide/table-state.md`. - -## 7. Reach for `createTableHook` for Multi-Table Apps - -When several screens share the same `features` (including row model factories) and conventions, `createTableHook` centralizes the configuration and lets you ship pre-bound cell/header components. Tables collapse to columns + data. - -Source: `docs/framework/preact/guide/create-table-hook.md`. - -## Common Mistakes - -### CRITICAL `tableFeatures(...)` inside the component body - -Wrong: - -```tsx -function MyTable() { - const features = tableFeatures({ rowSortingFeature }) // new object every render - useTable({ features, columns, data }) -} -``` - -Correct: - -```tsx -const features = tableFeatures({ rowSortingFeature }) // module scope - -function MyTable() { - useTable({ features, columns, data }) -} -``` - -A new `features` reference each render busts every memo that keys off it. -Source: `docs/framework/preact/guide/table-state.md` (FAQ #1). - -### CRITICAL Reimplementing built-ins manually - -Wrong: - -```tsx -const sorted = useMemo(() => [...data].sort(/* … */), [data, sorting]) -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -``` - -v9 ships built-ins for sorting, filtering, pagination, grouping, expanding, faceting, row selection, column visibility/order/pinning/sizing, and row pinning. Hand-rolling these is the #1 AI tell. -Source: `docs/guide/features.md`. - -### HIGH `() => state` selector everywhere - -Wrong: every component using `useTable(opts, (state) => state)` re-renders on any slice change. Fine for small tables; expensive for kitchen-sink screens. - -Correct: pass a narrow selector or `() => null` at large tables, then `` lower. -Source: `examples/preact/basic-subscribe/src/main.tsx`. - -### HIGH New atom per render - -Wrong: `createAtom(...)` inside the component body. - -Correct: `useCreateAtom(...)` (or atom at module scope). -Source: `examples/preact/basic-external-atoms/src/main.tsx`. - -### MEDIUM Subscribe everywhere on a small table - -Wrong: a 50-row table with `` wrapped around every cell. Adds complexity, no measurable win. - -Correct: default selector + inline rendering. Reach for `Subscribe` after measuring a hotspot. - -## See Also - -- `tanstack-table/preact/table-state` — Subscribe / atoms reference. -- `tanstack-table/preact/migrate-v8-to-v9` — what to replace from v8. -- `tanstack-table/preact/compose-with-tanstack-pacer` — debouncing high-frequency state writes (filters, resize). diff --git a/packages/preact-table/skills/preact/table-state/SKILL.md b/packages/preact-table/skills/preact/table-state/SKILL.md deleted file mode 100644 index a2038dce0d..0000000000 --- a/packages/preact-table/skills/preact/table-state/SKILL.md +++ /dev/null @@ -1,422 +0,0 @@ ---- -name: preact/table-state -description: > - Wiring reactivity for `@tanstack/preact-table` v9. Covers `useTable` (and its - second-argument selector), reading state via `table.state` / `table.store` / - `table.atoms.`, rendering with `table.FlexRender`, opting subtrees into - fine-grained reactivity with `` and the standalone - ``, owning slices with external atoms via `useCreateAtom` + - `options.atoms`, and packaging shared config into a reusable hook with - `createTableHook` (`useAppTable`, `createAppColumnHelper`, `table.AppTable` / - `table.AppHeader` / `table.AppCell` / `table.AppFooter`). Routing keywords: - useTable, useSelector, useCreateAtom, atoms, preact-store, table.Subscribe, - FlexRender. -type: framework -library: tanstack-table -framework: preact -library_version: '9.0.0-alpha.48' -requires: - - state-management - - setup -sources: - - TanStack/table:docs/framework/preact/guide/table-state.md - - TanStack/table:docs/framework/preact/guide/create-table-hook.md - - TanStack/table:packages/preact-table/src/useTable.ts - - TanStack/table:packages/preact-table/src/Subscribe.ts - - TanStack/table:packages/preact-table/src/FlexRender.tsx - - TanStack/table:packages/preact-table/src/createTableHook.tsx - - TanStack/table:examples/preact/basic-use-table/src/main.tsx - - TanStack/table:examples/preact/basic-subscribe/src/main.tsx - - TanStack/table:examples/preact/basic-external-atoms/src/main.tsx - - TanStack/table:examples/preact/basic-external-state/src/main.tsx - - TanStack/table:examples/preact/basic-use-app-table/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/setup`. Read those first: `state-management` explains the v9 atom model (per-slice readonly `table.atoms`, internal writable `table.baseAtoms`, flat `table.store`). The Preact adapter closely mirrors the React adapter: `useTable` returns a `PreactTable` whose state is backed by TanStack Store atoms, and `` lets components subscribe to slices fine-grained. - -## Setup - -Every Preact v9 table follows the same shape. Define `features` (including row model factories and \*Fns slots) and `columns` at module scope so their references are stable, then call `useTable` and render with ``. - -```tsx -import { render } from 'preact' -import { useState } from 'preact/hooks' -import { - createColumnHelper, - createSortedRowModel, - rowSortingFeature, - sortFns, - tableFeatures, - useTable, -} from '@tanstack/preact-table' - -type Person = { firstName: string; lastName: string; age: number } - -// Module-scope = stable identity. Critical for re-render perf. -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('lastName', { header: 'Last' }), - columnHelper.accessor('age', { header: 'Age' }), -]) - -function PeopleTable({ data }: { data: Person[] }) { - const table = useTable( - { - features, - columns, - data, - }, - (state) => state, // default selector - ) - - return ( - - - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((h) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((cell) => ( - - ))} - - ))} - -
- {h.isPlaceholder ? null : } -
- -
- ) -} -``` - -Source: `examples/preact/basic-use-table/src/main.tsx`. - -## Core Patterns - -### 1. `useTable` selector (second argument) - -The default selector returns the full `TableState` — the component re-renders on any registered state slice change. Pass a narrower selector once you have a measurable perf problem, or pass `() => null` to opt the parent out at the top level and use `` walls instead. - -The Preact adapter uses `useSelector` from `@tanstack/preact-store` with `shallow` compare under the hood. - -```tsx -// Narrow selector — re-render only on sorting/pagination changes. -const table = useTable( - { - features, - columns, - data, - }, - (state) => ({ sorting: state.sorting, pagination: state.pagination }), -) - -table.state.sorting // typed to the projected shape - -// Or: subscribe to nothing at the top level; read inside . -const table = useTable(opts, () => null) -``` - -Source: `docs/framework/preact/guide/table-state.md`; `examples/preact/basic-subscribe/src/main.tsx` (uses `() => null`). - -### 2. `` and standalone `` - -Use `` at the component level. Inside cell/header render contexts, `table` is the core `Table` (not `PreactTable`), so `table.Subscribe` is **not on the object** — import the standalone `` and pass `source={table.store}` or `source={table.atoms.X}`. - -```tsx -import { Subscribe } from '@tanstack/preact-table' - -// Component-level: selector against table.store. - s.pagination}> - {(pagination) => Page {pagination.pageIndex + 1}} - - -// Single-atom source — narrower than table.store. - - {(rowSelection) => {Object.keys(rowSelection).length} selected} - - -// Per-row identity projection — re-renders only that row's checkbox. - rs[row.id]} -> - {(isSelected) => ( - - )} - - -// Inside a cell — table here is the CORE Table, no .Subscribe. Use the import. -columnHelper.display({ - id: 'select', - cell: ({ row, table }) => ( - s[row.id]} - > - {(isSelected) => ( - - )} - - ), -}) -``` - -Source: `packages/preact-table/src/Subscribe.ts`; `examples/preact/basic-subscribe/src/main.tsx`. - -### 3. External atoms with `useCreateAtom` + `options.atoms` - -Move ownership of any slice to an atom you create with `useCreateAtom` from `@tanstack/preact-store`. Pass it via `options.atoms.`. The table writes to your atom when you call `table.setSorting(...)`, `table.setPageIndex(...)`, etc. — **no `on*Change` handler is needed**. - -Precedence: `options.atoms[key]` > `options.state[key]` > internal `baseAtoms[key]`. Don't pass both `state.foo` and `atoms.foo` for the same slice; `atoms` wins silently. - -```tsx -import { useCreateAtom, useSelector } from '@tanstack/preact-store' -import type { PaginationState, SortingState } from '@tanstack/preact-table' - -function MyTable({ data }) { - const sortingAtom = useCreateAtom([]) - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - - // Fine-grained subscriptions independent of the table. - const sorting = useSelector(sortingAtom) - const pagination = useSelector(paginationAtom) - - const table = useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom, pagination: paginationAtom }, - // NOTE: no onSortingChange / onPaginationChange — table writes directly to atoms. - }) -} -``` - -Source: `examples/preact/basic-external-atoms/src/main.tsx`. - -### 4. External state with `state` + `on*Change` and `createTableHook` - -Classic `useState` + `on*Change` integration (v8 migration paths) and the `createTableHook` factory for packaging shared `features` (with row model factories) and cell components into `useAppTable` + `createAppColumnHelper` + `table.AppTable` / `AppHeader` / `AppCell` / `AppFooter` boundaries — see [advanced-state-patterns.md](references/advanced-state-patterns.md). - -## Common Mistakes - -### CRITICAL Reading `table.atoms.X.get()` during render and expecting re-renders - -Wrong: - -```tsx -function Pager({ table }) { - const pagination = table.atoms.pagination.get() // current-value read, NOT a subscription - return Page {pagination.pageIndex + 1} -} -``` - -Correct: - -```tsx -function Pager({ table }) { - return ( - p.pageIndex} - > - {(pageIndex) => Page {pageIndex + 1}} - - ) -} -``` - -`.get()` and `table.state` are current-value reads, not subscriptions. The component never re-renders when the atom changes. -Source: `docs/framework/preact/guide/table-state.md`. - -### HIGH Passing both `atoms.X` and `state.X` for the same slice - -Wrong: - -```tsx -const table = useTable({ - features, - columns, - data, - atoms: { pagination: paginationAtom }, - state: { pagination }, // silently ignored - onPaginationChange: setPagination, // silently ignored -}) -``` - -Correct: - -```tsx -const table = useTable({ - features, - columns, - data, - atoms: { pagination: paginationAtom }, -}) -``` - -Precedence is `options.atoms[key]` > `options.state[key]` > internal. `state` is dropped without a warning when `atoms` is provided for the same slice. -Source: `docs/framework/preact/guide/table-state.md`. - -### HIGH Using `table.Subscribe` inside a column cell or header render - -Wrong: - -```tsx -cell: ({ row, table }) => ( - s[row.id]} - > - {(isSelected) => } - -) -``` - -Correct: - -```tsx -import { Subscribe } from '@tanstack/preact-table' - -cell: ({ row, table }) => ( - s[row.id]}> - {(isSelected) => ( - - )} - -) -``` - -In cell and header render contexts, `table` is the core `Table`, not `PreactTable` — `table.Subscribe` is undefined. Use the standalone import. -Source: `packages/preact-table/src/Subscribe.ts`. - -### CRITICAL Creating an atom inside the render body without `useCreateAtom` - -Wrong: - -```tsx -function MyTable() { - const sortingAtom = createAtom([]) // new atom every render - useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, - }) -} -``` - -Correct: - -```tsx -function MyTable() { - const sortingAtom = useCreateAtom([]) // stable across renders - useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, - }) -} -``` - -A fresh atom each render unbinds the table from the slice and resets the state to the initial value on every render. -Source: `examples/preact/basic-external-atoms/src/main.tsx`. - -### HIGH Unstable `data` / `columns` / `features` references - -Wrong: - -```tsx -function MyTable({ rows }) { - const features = tableFeatures({ rowSortingFeature }) // new every render - const columns = [ - /* … */ - ] // new every render - const table = useTable({ - features, - columns, - data: rows ?? [], - }) -} -``` - -Correct: - -```tsx -// Module scope — declared once. -const features = tableFeatures({ rowSortingFeature }) -const columns: ColumnDef[] = [ - /* … */ -] -const EMPTY: Person[] = [] - -function MyTable({ rows }) { - const data = rows ?? EMPTY - const table = useTable({ features, columns, data }) -} -``` - -Internal memoization keys off identity. A new reference each render busts memos and forces full recomputation. -Source: `docs/framework/preact/guide/table-state.md` (FAQ #1). - -### HIGH Reimplementing built-in feature logic by hand - -Wrong: - -```tsx -// Re-sorting rows manually outside the table — duplicates rowSortingFeature work. -const sorted = useMemo(() => [...data].sort(/* … */), [data, sorting]) -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -const rows = table.getRowModel().rows // already sorted -``` - -TanStack Table v9 ships built-ins for sorting, filtering, pagination, grouping, expanding, faceting, row selection, column visibility/order/pinning/sizing, and row pinning. Register the matching `*Feature` and its row-model factory in `tableFeatures`, then call the feature APIs (`setSorting`, `setColumnFilters`, etc.). Re-implementing these by hand is the #1 AI tell. -Source: `docs/guide/features.md`. - -## See Also - -- `tanstack-table/preact/getting-started` — first-table walkthrough. -- `tanstack-table/preact/migrate-v8-to-v9` — mechanical v8 → v9 breaking changes. -- `tanstack-table/preact/production-readiness` — narrowing selectors, tree-shaking, reference stability. -- `tanstack-table/preact/compose-with-tanstack-store` — sharing slice atoms across components, persistence. - -## References - -- [advanced-state-patterns.md](references/advanced-state-patterns.md) — `state` + `on*Change` external state and `createTableHook` for reusable shared config diff --git a/packages/preact-table/skills/preact/table-state/references/advanced-state-patterns.md b/packages/preact-table/skills/preact/table-state/references/advanced-state-patterns.md deleted file mode 100644 index 6c5eca581a..0000000000 --- a/packages/preact-table/skills/preact/table-state/references/advanced-state-patterns.md +++ /dev/null @@ -1,92 +0,0 @@ -# Advanced state patterns — Preact - -Extended state patterns extracted from `SKILL.md`. The three essential patterns (`useTable` selector, `` walls, and external atoms via `useCreateAtom` + `options.atoms`) remain inline in the SKILL; this file covers the additional patterns. - -## External state with `state` + `on*Change` - -Classic Preact `useState` integration is still supported via `state` and matching `on[State]Change`. Useful for v8 migration paths or simple cases. Less fine-grained than external atoms. - -```tsx -const [sorting, setSorting] = useState([]) -const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10, -}) - -const table = useTable({ - features, - columns, - data, - state: { sorting, pagination }, - onSortingChange: setSorting, - onPaginationChange: setPagination, -}) -``` - -Source: `docs/framework/preact/guide/table-state.md`. - -## `createTableHook` for reusable shared config - -When you ship the same `features` (including row model factories) and cell components across many tables, package them with `createTableHook`. You get `useAppTable`, `createAppColumnHelper`, `useTableContext` / `useCellContext` / `useHeaderContext`, and `table.AppTable` / `AppHeader` / `AppCell` / `AppFooter` boundaries. - -```tsx -import { - createTableHook, - rowPaginationFeature, - rowSortingFeature, - createPaginatedRowModel, - createSortedRowModel, - sortFns, - tableFeatures, -} from '@tanstack/preact-table' - -export const { - useAppTable, - createAppColumnHelper, - useTableContext, - useCellContext, - useHeaderContext, -} = createTableHook({ - features: tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - sortFns, - }), - tableComponents: { PaginationControls, RowCount }, - cellComponents: { TextCell, NumberCell }, - headerComponents: { SortIndicator }, -}) - -const columnHelper = createAppColumnHelper() - -function UsersTable({ data }: { data: Person[] }) { - const table = useAppTable({ columns, data }) - - return ( - - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((c) => ( - - {(cell) => ( - - )} - - ))} - - ))} - -
- -
- -
- ) -} -``` - -Source: `docs/framework/preact/guide/create-table-hook.md`; `examples/preact/basic-use-app-table/src/main.tsx`; `packages/preact-table/src/createTableHook.tsx`. diff --git a/packages/preact-table/skills/table-state/SKILL.md b/packages/preact-table/skills/table-state/SKILL.md new file mode 100644 index 0000000000..a96dbbdb83 --- /dev/null +++ b/packages/preact-table/skills/table-state/SKILL.md @@ -0,0 +1,155 @@ +--- +name: table-state +description: > + Read and own Preact Table v9 state with useTable selectors, selected table.state, table.Subscribe, table.atoms/store, controlled slices, and external Preact Store atoms. Load for snapshot-versus-subscription bugs or state render tuning. +metadata: + { + type: framework, + library: '@tanstack/preact-table', + library_version: '9.0.0-beta.38', + framework: preact, + } +requires: ['@tanstack/table-core#core', getting-started] +sources: + - 'TanStack/table:docs/framework/preact/guide/table-state.md' + - 'TanStack/table:examples/preact/basic-subscribe' + - 'TanStack/table:packages/preact-table/src/useTable.ts' +--- + +This skill builds on `@tanstack/table-core#core` and `getting-started`. Preact selection resembles React, but imports and store bindings must remain Preact-native. + +## State Mental Model + +TanStack Table is primarily a state coordinator. Keep state internal unless another subsystem needs to read, persist, or drive it. With no `initialState`, `atoms`, `state`, or `on[State]Change`, the table owns every registered state slice. + +- `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. +- `table.state` contains only the result of the second `useTable` selector. + +Only registered features contribute state and types. If pagination is absent from `table.atoms`, `table.store`, `table.state`, or table options, add `rowPaginationFeature`; do not cast around the missing API. Keep `features`, `data`, and `columns` stable across renders. + +## Setup + +```tsx +const table = useTable({ features, columns, data }, (state) => ({ + pagination: state.pagination, +})) +return {table.state.pagination.pageIndex + 1} +``` + +The selector determines rerenders and the shape exposed on `table.state`; omission selects all registered slices. + +## Core Patterns + +### Fine-grained Preact subscription + +```tsx +return ( + + {(selection) => {Object.keys(selection).length}} + +) +``` + +### External atom ownership + +```tsx +import { useCreateAtom } from '@tanstack/preact-store' +const pagination = useCreateAtom({ pageIndex: 0, pageSize: 20 }) +const table = useTable({ features, columns, data, atoms: { pagination } }) +``` + +## Choose State Ownership + +Use one owner per slice: + +- Prefer internal state and feature APIs when state is local to the table. +- Use `initialState` for starting/reset values; changing it later does not reset current state. +- Prefer a stable `@tanstack/preact-store` atom in `atoms` when state must be shared. Do not pair it with `on[State]Change`. +- Use `state.` and the matching callback for simple controlled state. Feed the next value back and handle value-or-updater semantics. + +External atoms win over external `state`, which syncs into the internal base atom. Do not use two owners intentionally. The v8 global `onStateChange` option is gone; control slices individually or subscribe to `table.store`. + +## Initialize, Update, and Reset + +Use feature methods (`setSorting`, `nextPage`, `toggleVisibility`, `toggleSelected`) for writes. Write `baseAtoms` only as a rare low-level escape hatch for internal state; write the external atom when it owns the slice. + +```tsx +table.resetSorting() // reset to table.initialState.sorting +table.resetPagination() +table.resetPagination(true) // reset to the feature default +``` + +Slice resets can update an external owner. Core `table.reset()` only resets internal base atoms. Import types such as `PaginationState` for one slice and use `TableState` when a full feature-inferred state type is actually needed. + +## Common Mistakes + +### HIGH Reading snapshots as subscriptions + +Wrong: + +```tsx +const page = table.store.state.pagination.pageIndex +``` + +Correct: + +```tsx +const page = table.state.pagination.pageIndex +``` + +Store and atom `.get()` reads are snapshots; selected `table.state` or `Subscribe` connects a Preact render. + +Source: `packages/preact-table/src/useTable.ts` + +### HIGH Controlling only the callback + +Wrong: + +```tsx +useTable({ features, columns, data, onPaginationChange: setPagination }) +``` + +Correct: + +```tsx +useTable({ + features, + columns, + data, + state: { pagination }, + onPaginationChange: setPagination, +}) +``` + +The callback must write into the value supplied for that controlled slice. + +Source: `docs/framework/preact/guide/table-state.md` + +### MEDIUM Narrowing away rendered dependencies + +Wrong: + +```tsx +const table = useTable(options, (state) => ({ pagination: state.pagination })) +return table.getSelectedRowModel().rows.length +``` + +Correct: + +```tsx +const table = useTable(options, (state) => ({ + pagination: state.pagination, + rowSelection: state.rowSelection, +})) +return table.getSelectedRowModel().rows.length +``` + +If render output depends on selection, the owning render boundary must subscribe to it directly or via `Subscribe`. + +Source: `examples/preact/basic-subscribe` + +## API Discovery + +Inspect `node_modules/@tanstack/preact-table/src/useTable.ts` and `Subscribe.tsx`; use `@tanstack/preact-store` rather than React Store hooks. diff --git a/packages/preact-table/skills/with-tanstack-query/SKILL.md b/packages/preact-table/skills/with-tanstack-query/SKILL.md new file mode 100644 index 0000000000..eb9a8e268b --- /dev/null +++ b/packages/preact-table/skills/with-tanstack-query/SKILL.md @@ -0,0 +1,135 @@ +--- +name: with-tanstack-query +description: > + Compose native Preact Table v9 with @tanstack/preact-query using table state in query keys, manual server filtering/sorting/pagination, server counts, and stable query-result data. Load when React Query examples or manual* expectations are wrong for Preact. +metadata: + { + type: composition, + library: '@tanstack/preact-table', + library_version: '9.0.0-beta.38', + framework: preact, + } +requires: + ['@tanstack/table-core#client-vs-server', getting-started, table-state] +sources: + - 'TanStack/table:examples/preact/with-tanstack-query' + - 'TanStack/table:docs/framework/preact/guide/pagination.md' +--- + +This skill builds on `@tanstack/table-core#client-vs-server`, `getting-started`, and `table-state`. Query fetches already processed rows; Table's manual flags only declare that boundary. + +## Setup + +```tsx +import { keepPreviousData, useQuery } from '@tanstack/preact-query' +import { useCreateAtom, useSelector } from '@tanstack/preact-store' +import { + rowPaginationFeature, + tableFeatures, + useTable, +} from '@tanstack/preact-table' + +const features = tableFeatures({ rowPaginationFeature }) +const emptyRows: Array<{ id: string }> = [] +const paginationAtom = useCreateAtom({ pageIndex: 0, pageSize: 20 }) +const pagination = useSelector(paginationAtom, (value) => value) +const result = useQuery({ + queryKey: ['people', pagination], + queryFn: () => fetchPeople(pagination), + placeholderData: keepPreviousData, +}) +const table = useTable({ + features, + columns, + data: result.data?.rows ?? emptyRows, + rowCount: result.data?.rowCount, + atoms: { pagination: paginationAtom }, + manualPagination: true, +}) +``` + +## Core Patterns + +### Key requests by every server-owned slice + +```tsx +useQuery({ + queryKey: ['people', pagination, sorting], + queryFn: () => fetchPeople({ pagination, sorting }), +}) +``` + +### Pass cached rows directly + +```tsx +useTable({ features, columns, data: result.data?.rows ?? emptyRows }) +``` + +## Common Mistakes + +### HIGH Importing React Query APIs + +Wrong: + +```tsx +import { useQuery } from '@tanstack/react-query' +``` + +Correct: + +```tsx +import { useQuery } from '@tanstack/preact-query' +``` + +Use native Preact Query and Store integrations throughout the composition. + +Source: `examples/preact/with-tanstack-query` + +### HIGH Reusing one cache key + +Wrong: + +```tsx +useQuery({ queryKey: ['people'], queryFn: () => fetchPeople(pagination) }) +``` + +Correct: + +```tsx +useQuery({ + queryKey: ['people', pagination], + queryFn: () => fetchPeople(pagination), +}) +``` + +Each processed server result needs a key that includes its request state. + +Source: `examples/preact/with-tanstack-query` + +### HIGH Expecting manual flags to fetch + +Wrong: + +```tsx +useTable({ features, columns, data, manualPagination: true }) +``` + +Correct: + +```tsx +useTable({ + features, + columns, + data: result.data?.rows ?? emptyRows, + rowCount: result.data?.rowCount, + manualPagination: true, +}) +``` + +Manual pagination bypasses local processing; it neither calls the backend nor invents totals. + +Source: `docs/framework/preact/guide/pagination.md` + +## API Discovery + +Inspect installed `node_modules/@tanstack/preact-table/src/index.ts` and the relevant core feature source; inspect `@tanstack/preact-query` source for exact query APIs. diff --git a/packages/preact-table/skills/with-tanstack-virtual/SKILL.md b/packages/preact-table/skills/with-tanstack-virtual/SKILL.md new file mode 100644 index 0000000000..cb7d0a7d57 --- /dev/null +++ b/packages/preact-table/skills/with-tanstack-virtual/SKILL.md @@ -0,0 +1,118 @@ +--- +name: with-tanstack-virtual +description: > + Apply TanStack Virtual rendering to the final Preact Table row or column model through React compatibility or virtual-core. Load for adapter selection, reactive counts, scroll targets, stable keys, measurements, spacer geometry, sticky layout, sizing CSS, or overscan. +metadata: + { + type: composition, + library: '@tanstack/preact-table', + library_version: '9.0.0-beta.38', + framework: preact, + } +requires: ['@tanstack/table-core#core', getting-started, table-state] +sources: + - 'TanStack/table:docs/framework/preact/guide/virtualization.md' + - 'TanStack/table:packages/preact-table/src/index.ts' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Virtual is renderer composition over the final Table model, never a `tableFeatures` slot. + +## Setup + +```tsx +import { useRef } from 'preact/hooks' +import { useVirtualizer } from '@tanstack/react-virtual' // via preact/compat + +const scrollRef = useRef(null) +const rows = table.getRowModel().rows +const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 36, + getItemKey: (index) => rows[index].id, + overscan: 5, +}) +``` + +Render a spacer of `getTotalSize()` and position each row from its virtual item's `start`. + +## Core Patterns + +### Apply Table's column sizes + +```tsx + + + +``` + +### Measure dynamic rows by index + +```tsx +
+ {row.id} +
+``` + +## Common Mistakes + +### HIGH Looking for a Preact-specific adapter + +There is no `@tanstack/preact-virtual` package. In the standard Preact setup that aliases React to `preact/compat`, use the maintained React adapter: + +```tsx +import { useVirtualizer } from '@tanstack/react-virtual' +``` + +If the application cannot use the compat alias, integrate the `Virtualizer` class from `@tanstack/virtual-core` directly. That is a lower-level integration with application-owned lifecycle and subscriptions; do not invent Preact hooks around it. + +Source: `docs/framework/preact/guide/virtualization.md` + +### HIGH Virtualizing raw data + +Wrong: + +```tsx +useVirtualizer({ count: data.length, getScrollElement, estimateSize }) +``` + +Correct: + +```tsx +const rows = table.getRowModel().rows +useVirtualizer({ count: rows.length, getScrollElement, estimateSize }) +``` + +Raw data omits Table's current filtering, sorting, expansion, grouping, and pagination. + +Source: `docs/framework/preact/guide/virtualization.md` + +### HIGH Assuming layout is automatic + +Wrong: + +```tsx +virtualizer.getVirtualItems().map((item) =>
{rows[item.index].id}
) +``` + +Correct: + +```tsx +virtualizer + .getVirtualItems() + .map((item) => ( +
+ {rows[item.index].id} +
+ )) +``` + +Virtual computes geometry; application markup and CSS must apply it and supply the scroll container/spacer. + +Source: `docs/framework/preact/guide/virtualization.md` + +## API Discovery + +Inspect `node_modules/@tanstack/preact-table/src/index.ts`. For rendering APIs, inspect installed `node_modules/@tanstack/react-virtual/src/` when using the standard `preact/compat` alias, or `node_modules/@tanstack/virtual-core/src/` for a direct integration. There is no maintained Preact-specific Virtual package or Table example; start from the Preact guide and translate the maintained React examples only through the compat setup it describes. diff --git a/packages/react-table-devtools/skills/devtools/SKILL.md b/packages/react-table-devtools/skills/devtools/SKILL.md new file mode 100644 index 0000000000..b8f1d42076 --- /dev/null +++ b/packages/react-table-devtools/skills/devtools/SKILL.md @@ -0,0 +1,85 @@ +--- +name: devtools +description: > + Connect React Table v9 instances to Devtools with tableDevtoolsPlugin and useTanStackTableDevtools. Load for missing Table panels, registration lifecycle, required options.key, enabled state, or development versus explicit production exports. +metadata: + type: framework + library: '@tanstack/react-table-devtools' + framework: react + library_version: '9.0.0-beta.40' +requires: + - '@tanstack/table-core#core' + - '@tanstack/table-devtools#devtools' +sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/react-table-devtools/src/index.ts' + - 'TanStack/table:packages/react-table-devtools/src/useTanStackTableDevtools.ts' +--- + +This skill builds on @tanstack/table-core#core and @tanstack/table-devtools#devtools. + +## Setup + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools' +import { useTable, tableFeatures } from '@tanstack/react-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/react-table-devtools' + +const features = tableFeatures({}) +const columns = [{ accessorKey: 'id' }] +const data: Array<{ id: string }> = [] + +export function App() { + const table = useTable({ + key: 'users-table', + features, + columns, + data, + }) + useTanStackTableDevtools(table) + return +} +``` + +## Hooks and Components + +Use `useTanStackTableDevtools(table, { enabled })` immediately after creating the table. Mount one TanStackDevtools host with `tableDevtoolsPlugin()` near the application root. + +## Common Mistakes + +### HIGH Hook receives a keyless table + +Wrong: create the table without `key` and expect the hook to infer a name. + +Correct: set a unique `key` in table options before calling the hook. + +Core target registration skips keyless tables. + +Source: TanStack/table:docs/devtools.md + +### HIGH Hook called conditionally + +Wrong: call `useTanStackTableDevtools` only inside an `if` branch. + +Correct: call it every render and pass `{ enabled: condition }`. + +The hook owns React effect registration and cleanup. + +Source: TanStack/table:packages/react-table-devtools/src/useTanStackTableDevtools.ts + +### MEDIUM Default import expected in production + +Wrong: expect the normal hook/plugin/panel to inspect production tables. + +Correct: keep normal guidance development-only; use the `/production` entrypoint only when explicitly required. + +The default index selects no-op implementations outside development. + +Source: TanStack/table:packages/react-table-devtools/src/index.ts + +## API Discovery + +Inspect `node_modules/@tanstack/react-table-devtools/src/index.ts` and `useTanStackTableDevtools.ts` for the installed lifecycle API. diff --git a/packages/react-table-devtools/skills/react/compose-with-tanstack-devtools/SKILL.md b/packages/react-table-devtools/skills/react/compose-with-tanstack-devtools/SKILL.md deleted file mode 100644 index f4eb9eac95..0000000000 --- a/packages/react-table-devtools/skills/react/compose-with-tanstack-devtools/SKILL.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -name: react/compose-with-tanstack-devtools -description: > - Wire up TanStack Devtools for TanStack Table in React. Mount `TanStackDevtools` - with `tableDevtoolsPlugin()` once at the app root and call - `useTanStackTableDevtools(table)` after each `useTable` so the table is - registered as a devtools target. Live devtools are tree-shaken to no-ops in - production unless you import from `@tanstack/react-table-devtools/production`. -type: composition -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - state-management - - react/table-state -sources: - - TanStack/table:docs/devtools.md - - TanStack/table:packages/react-table-devtools/src/index.ts - - TanStack/table:packages/react-table-devtools/src/plugin.tsx - - TanStack/table:packages/react-table-devtools/src/useTanStackTableDevtools.ts - - TanStack/table:packages/react-table-devtools/src/production.ts ---- - -This skill builds on `tanstack-table/react/table-state`. Read that first — the devtools panel inspects whatever table instance you hand it, so you need a working `useTable` before this skill is useful. - -## Setup - -Install the TanStack Devtools host and the React Table adapter: - -```sh -pnpm add @tanstack/react-devtools @tanstack/react-table-devtools -``` - -The recommended pattern has two parts: - -1. Mount `` once at the app root with `tableDevtoolsPlugin()`. -2. Call `useTanStackTableDevtools(table)` right after every `useTable()`. - -```tsx -import React from 'react' -import ReactDOM from 'react-dom/client' -import { useTable } from '@tanstack/react-table' -import { TanStackDevtools } from '@tanstack/react-devtools' -import { - tableDevtoolsPlugin, - useTanStackTableDevtools, -} from '@tanstack/react-table-devtools' - -function UsersScreen() { - const table = useTable({ - features, - key: 'users-table', - columns, - data, - }) - - // Register this table with the devtools panel. - useTanStackTableDevtools(table) - - return -} - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - {/* Mount once, anywhere in the tree. */} - - , -) -``` - -`tableDevtoolsPlugin()` returns a plugin descriptor for the multi-panel TanStack Devtools UI. `useTanStackTableDevtools` is the hook that upserts/removes the registration target on mount/unmount and re-runs when `table` or `table.options.key` changes. - -## Patterns - -### Keying Tables - -Add a stable `key` option to the table options. Devtools use this key as both the registration id and the panel selector label. - -```tsx -useTanStackTableDevtools(table) -``` - -### Multiple Tables - -You can register as many tables as you like. The Table panel renders a selector so you can switch between them. Always give each table a unique `key`; duplicate keys update the existing devtools target. - -```tsx -function Dashboard() { - const ordersTable = useTable({ ...ordersOptions, key: 'orders-table' }) - const usersTable = useTable({ ...usersOptions, key: 'users-table' }) - - useTanStackTableDevtools(ordersTable) - useTanStackTableDevtools(usersTable) - - return -} -``` - -### Disabling Per Table - -`useTanStackTableDevtools` accepts an `enabled` option. When `false`, the registration is removed (so the table disappears from the panel) but the hook still runs cleanly. Useful for feature flags or per-route opt-out. - -```tsx -useTanStackTableDevtools(table, { - enabled: import.meta.env.DEV && showTableDevtools, -}) -``` - -### Production Builds - -The default `@tanstack/react-table-devtools` entrypoint swaps to no-op implementations when `process.env.NODE_ENV !== 'development'`. To ship the real devtools to production, switch BOTH imports to the `/production` entrypoint: - -```tsx -import { TanStackDevtools } from '@tanstack/react-devtools' -import { - tableDevtoolsPlugin, - useTanStackTableDevtools, -} from '@tanstack/react-table-devtools/production' -``` - -If you mix entrypoints (one from `/production`, one from the default), one side is a no-op in production and the panel will appear empty. - -### Conditional Devtools by Env - -A common pattern is to lazy-load the production entrypoint behind a feature flag so the devtools bundle does not ship to all users: - -```tsx -const TableDevtools = React.lazy(async () => { - const { tableDevtoolsPlugin } = - await import('@tanstack/react-table-devtools/production') - const { TanStackDevtools } = await import('@tanstack/react-devtools') - return { - default: () => , - } -}) - -function Root() { - return ( - <> - - {showDevtools && ( - - - - )} - - ) -} -``` - -## Common Mistakes - -### Forgetting to mount `` at the app root - -Calling `useTanStackTableDevtools(table)` alone does nothing visible — it only registers the table with the devtools target store. Without a `` somewhere in the tree, there is no panel to render the registration. Symptom: hook runs without errors, no devtools button appears. - -### Importing devtools from the default path in a prod-only bundle - -If you only deploy production builds (e.g. an E2E preview, a staging environment that builds with `NODE_ENV=production`), `@tanstack/react-table-devtools` resolves to no-op implementations. The plugin will mount, but the panel will be empty. Use `@tanstack/react-table-devtools/production` instead if you want the real devtools available there. - -### Accidentally shipping devtools to end users via `/production` - -The flip side: importing from `/production` in your default app bundle means every visitor downloads and runs the devtools UI. That is usually not what you want. Restrict `/production` imports to dev/preview entrypoints, code-split them behind a flag, or keep them out of the default app entirely. - -### Calling `useTanStackTableDevtools` outside the component that owns the table - -The hook needs a `Table` instance to register. If you call it in a parent before `useTable` runs, or inside a sibling that does not have access to the table, you pass `undefined` and nothing is registered. Always call it in the same component as `useTable`, immediately after. - -### Multiple tables without keys - -A table registered without `options.key` is skipped and devtools log an error. Add a unique `key` option to every table that should appear in devtools. diff --git a/packages/react-table/skills/create-table-hook/SKILL.md b/packages/react-table/skills/create-table-hook/SKILL.md new file mode 100644 index 0000000000..be3cf92428 --- /dev/null +++ b/packages/react-table/skills/create-table-hook/SKILL.md @@ -0,0 +1,182 @@ +--- +name: create-table-hook +description: > + Build reusable React table infrastructure with createTableHook, useAppTable, createAppColumnHelper, shared features/defaults, component registries, AppTable/AppCell/AppHeader wrappers, and typed context hooks. Load for recurring application table conventions, scoped contexts, HMR cycles, or table prop drilling. +metadata: + type: framework + library: '@tanstack/react-table' + library_version: '9.0.0-beta.38' + framework: react +requires: + - '@tanstack/table-core#core' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/react/guide/composable-tables.md' + - 'TanStack/table:docs/framework/react/guide/table-context.md' + - 'TanStack/table:examples/react/composable-tables' + - 'TanStack/table:packages/react-table/src/createTableHook.tsx' + - 'TanStack/table:packages/react-table/src/createTableHookContexts.tsx' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Use a factory when multiple tables share real conventions; use standalone `useTable` for a one-off. + +## Setup + +```tsx +import { + createTableHook, + rowSelectionFeature, + tableFeatures, +} from '@tanstack/react-table' + +export const { createAppColumnHelper, useAppTable, useTableContext } = + createTableHook({ + features: tableFeatures({ rowSelectionFeature }), + getRowId: (row: { id: string }) => row.id, + }) +``` + +Keep this factory in an infrastructure module. It binds feature types and defaults once while each `useAppTable` call still supplies its own data, columns, state, and initial state. + +## Core Patterns + +### Infer columns through the bound helper + +```tsx +type Person = { id: string; name: string } +const helper = createAppColumnHelper() +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) + +function People({ data }: { data: Person[] }) { + const table = useAppTable({ data, columns }) + return ( + +
{table.getRowModel().rows.length}
+
+ ) +} +``` + +`AppTable` takes ordinary JSX children when it has no selector. Function children are only valid with a selector: + +```tsx + state.rowSelection}> + {(rowSelection) => {Object.keys(rowSelection).length}} + +``` + +### Read registered context instead of drilling props + +```tsx +function RowCount() { + const table = useTableContext() + return {table.getRowModel().rows.length} +} +``` + +Register reusable table/cell/header components in the factory and consume them under their matching `App*` provider. + +### Isolate genuinely nested table setups + +The default module-scoped contexts are HMR-stable, and normal provider scoping already isolates sibling tables. Separate `createTableHook` calls still use those shared default contexts, so nested providers can silently resolve a consumer to the inner table. Create scoped contexts only when different table setups are nested: + +```tsx +import { + createTableHook, + createTableHookContexts, + rowSelectionFeature, + tableFeatures, +} from '@tanstack/react-table' + +const features = tableFeatures({ rowSelectionFeature }) +const { tableContext, cellContext, headerContext } = + createTableHookContexts() + +export const app = createTableHook({ + features, + tableContext, + cellContext, + headerContext, +}) +``` + +Prefer context hooks returned by `createTableHook`; they include the registered component maps in their types. Hooks returned directly by `createTableHookContexts` know only `TFeatures` and are useful from modules that cannot import the completed factory. + +## Common Mistakes + +### MEDIUM Factory for a one-off table + +Wrong: + +```tsx +const { useAppTable } = createTableHook({ features: tableFeatures({}) }) +``` + +Correct: + +```tsx +const table = useTable({ features: tableFeatures({}), columns, data }) +``` + +A factory adds an app-wide abstraction; standalone construction is clearer without shared conventions. + +Source: `docs/framework/react/guide/composable-tables.md` + +### HIGH Creating the factory during render + +Wrong: + +```tsx +function People() { + const app = createTableHook({ features }) + const table = app.useAppTable({ data, columns }) + return ( + +
+ + ) +} +``` + +Correct: + +```tsx +const app = createTableHook({ features }) +function People() { + const table = app.useAppTable({ data, columns }) + return ( + +
+ + ) +} +``` + +Creating a new factory closure during render makes hook configuration and component registries unstable. Keep the factory and its bound hook identity at module scope. + +Source: `packages/react-table/src/createTableHook.tsx` + +### HIGH Closing a circular HMR import + +Wrong: + +```tsx +// table.ts imports RowCount; RowCount.tsx imports useTableContext from table.ts +export const app = createTableHook({ features, tableComponents: { RowCount } }) +``` + +Correct: + +```tsx +// components.tsx receives the exported hook through a cycle-free module boundary +export const app = createTableHook({ features }) +``` + +Keep the factory dependency graph acyclic or inject components from a separate composition root; circular registries can break Vite HMR. + +Source: `https://github.com/TanStack/table/issues/6348` + +## API Discovery + +Inspect `node_modules/@tanstack/react-table/src/createTableHook.tsx` and `createTableHookContexts.tsx` for the exact returned helpers, component registries, wrapper props, and scoped context types. diff --git a/packages/react-table/skills/getting-started/SKILL.md b/packages/react-table/skills/getting-started/SKILL.md new file mode 100644 index 0000000000..99cd5a5bbb --- /dev/null +++ b/packages/react-table/skills/getting-started/SKILL.md @@ -0,0 +1,171 @@ +--- +name: getting-started +description: > + Create and render a TanStack React Table v9 table with useTable, tableFeatures, stable data and columns, row/header models, and table.FlexRender. Load for a first React table, headless rendering, or when v8 useReactTable examples are producing the wrong setup. +metadata: + type: framework + library: '@tanstack/react-table' + library_version: '9.0.0-beta.38' + framework: react +requires: + - '@tanstack/table-core#core' + - '@tanstack/table-core#table-features' +sources: + - 'TanStack/table:docs/framework/react/guide/migrating.md' + - 'TanStack/table:examples/react/basic-use-table' + - 'TanStack/table:packages/react-table/src/index.ts' +--- + +This skill builds on `@tanstack/table-core#core` and `@tanstack/table-core#table-features`. Read them first for the headless model and explicit feature registration. + +## Setup + +```tsx +import { useMemo, useState } from 'react' +import { + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/react-table' + +type Person = { name: string; age: number } +const features = tableFeatures({}) +const helper = createColumnHelper() +const columns = helper.columns([ + helper.accessor('name', { header: 'Name' }), + helper.accessor('age', { header: 'Age' }), +]) + +export function PeopleTable() { + const [data] = useState([{ name: 'Ada', age: 36 }]) + const table = useTable({ features, columns, data }) + + return ( + + + {table.getHeaderGroups().map((group) => ( + + {group.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + + )} +
+ +
+ ) +} +``` + +Table produces models and state; React owns the semantic markup, styles, event affordances, and accessibility. + +## Core Patterns + +### Add only the feature the table uses + +```tsx +import { + createSortedRowModel, + rowSortingFeature, + tableFeatures, +} from '@tanstack/react-table' + +const sortableFeatures = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +``` + +Row-model slots belong inside `tableFeatures`, after their prerequisite feature. + +### Keep static inputs outside render + +```tsx +const features = tableFeatures({}) +const data: Person[] = [{ name: 'Ada', age: 36 }] +``` + +Use state, memoization, or query results for changing data; avoid a new fallback array every render. + +## Common Mistakes + +### HIGH Copying the v8 table constructor + +Wrong: + +```tsx +const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), +}) +``` + +Correct: + +```tsx +const table = useTable({ data, columns, features }) +``` + +V9 uses `useTable`; optional row models are registered as feature slots rather than table options. + +Source: `docs/framework/react/guide/migrating.md` + +### HIGH Assuming feature APIs are global + +Wrong: + +```tsx +const features = tableFeatures({}) +``` + +Correct: + +```tsx +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +``` + +Sorting state and methods do not exist until the sorting feature is registered. + +Source: `packages/table-core/src/TableFeatures.ts` + +### MEDIUM Recreating fallback data each render + +Wrong: + +```tsx +const table = useTable({ features, columns, data: response.data ?? [] }) +``` + +Correct: + +```tsx +// module scope +const EMPTY_DATA: Person[] = [] +const table = useTable({ features, columns, data: response.data ?? EMPTY_DATA }) +``` + +A fresh fallback invalidates data-dependent models on every render. + +Source: `docs/framework/react/guide/data.md` + +## API Discovery + +Inspect `node_modules/@tanstack/react-table/src/index.ts` first, then the exported `useTable.ts`, `FlexRender.tsx`, or core feature source. Use installed source so names match the consumer's exact v9 version. diff --git a/packages/react-table/skills/migrate-v8-to-v9/SKILL.md b/packages/react-table/skills/migrate-v8-to-v9/SKILL.md new file mode 100644 index 0000000000..32cf9f2ca3 --- /dev/null +++ b/packages/react-table/skills/migrate-v8-to-v9/SKILL.md @@ -0,0 +1,224 @@ +--- +name: migrate-v8-to-v9 +description: > + Perform a complete @tanstack/react-table v8-to-v9 migration: hook and feature architecture, row-model slots, React state and subscriptions, rendering, composable tables, type helpers, and every shared API rename and semantic change. Use for migration plans, implementation, or audits. Treat useLegacyTable only as a deprecated temporary bridge. +metadata: + type: lifecycle + library: '@tanstack/react-table' + library_version: '9.0.0-beta.38' + framework: react +requires: + - '@tanstack/table-core#migrate-v8-to-v9' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/react/guide/migrating.md' + - 'TanStack/table:packages/react-table/src/index.ts' + - 'TanStack/table:packages/react-table/src/legacy.ts' + - 'TanStack/table:examples/react/basic-use-table' +--- + +Read `@tanstack/table-core#migrate-v8-to-v9`, `getting-started`, and `table-state`. Use this skill as the exhaustive migration checklist, not as general API documentation. Inspect the installed `src` files before writing APIs for a different v9 beta. + +Framework prerequisite: React 18 or newer (`react >=18`). + +## Target architecture + +```tsx +import { + columnFilteringFeature, + createFilteredRowModel, + createSortedRowModel, + filterFns, + rowSortingFeature, + sortFns, + tableFeatures, + useTable, +} from '@tanstack/react-table' + +const features = tableFeatures({ + columnFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns, + sortFns, +}) + +const table = useTable({ features, columns, data }) +``` + +Prefer explicit features as the end state. `stockFeatures` is a useful kitchen-sink migration shortcut, but bundles every stock feature. Do not target `useLegacyTable`: it is deprecated, React-only, exported from `@tanstack/react-table/legacy`, and intended only to keep an existing migration moving temporarily. + +## Complete breaking-change map + +### Construction and feature registration + +| v8 | v9 | +| ---------------------------------------------- | --------------------------------------------------------------- | +| `useReactTable(options)` | `useTable({ ...options, features })` | +| Every feature bundled automatically | Register used `*Feature` objects with `tableFeatures()` | +| `getCoreRowModel()` option | Remove it; the core row model is automatic | +| `getFilteredRowModel()` option | `filteredRowModel: createFilteredRowModel()` feature slot | +| `getSortedRowModel()` option | `sortedRowModel: createSortedRowModel()` feature slot | +| `getPaginationRowModel()` option | `paginatedRowModel: createPaginatedRowModel()` feature slot | +| `getExpandedRowModel()` option | `expandedRowModel: createExpandedRowModel()` feature slot | +| `getGroupedRowModel()` option | `groupedRowModel: createGroupedRowModel()` feature slot | +| `getFacetedRowModel()` option | `facetedRowModel: createFacetedRowModel()` feature slot | +| `getFacetedMinMaxValues()` option | `facetedMinMaxValues: createFacetedMinMaxValues()` feature slot | +| `getFacetedUniqueValues()` option | `facetedUniqueValues: createFacetedUniqueValues()` feature slot | +| `sortingFns` table option | `sortFns` slot in `tableFeatures()` | +| `filterFns` table option/factory argument | `filterFns` slot in `tableFeatures()` | +| `aggregationFns` table option/factory argument | `aggregationFns` slot in `tableFeatures()` | +| Early-v9-beta `rowModels: { ... }` | Named row-model slots directly in `tableFeatures()` | + +Declare each prerequisite feature before its row-model slot in the same `tableFeatures()` call. Available stock features are `columnFilteringFeature`, `globalFilteringFeature`, `rowSortingFeature`, `rowPaginationFeature`, `rowSelectionFeature`, `rowExpandingFeature`, `rowPinningFeature`, `columnPinningFeature`, `columnVisibilityFeature`, `columnOrderingFeature`, `columnSizingFeature`, `columnResizingFeature`, `columnGroupingFeature`, and `columnFacetingFeature`. + +### State and React subscriptions + +| v8 | v9 | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `table.getState()` | `table.state`, `table.store.state`, or `table.atoms..get()` | +| Top-level `onStateChange` | Per-slice `onSortingChange`, `onPaginationChange`, etc., or `table.store.subscribe()` for all changes | +| Broad component updates | Default `useTable` selector still subscribes to all registered state; narrow with a selector, `table.Subscribe`, or `useSelector(table.atoms.)` | +| Framework state only | Optional writable atoms through `options.atoms` | + +Controlled `state` plus per-slice callbacks remains valid: + +```tsx +const [sorting, setSorting] = useState([]) +const table = useTable({ + features, + columns, + data, + state: { sorting }, + onSortingChange: setSorting, +}) +``` + +For fine-grained rendering, pass a selector as the second `useTable` argument or select closer to the consumer: + +```tsx +const table = useTable(options, () => null) + + state.pagination}> + {pagination => Page {pagination.pageIndex + 1}} + +``` + +External atoms override the same slice in `state`; table setters write directly to them, and `table.reset()` does not reset them. Do not supply an atom, controlled value, and callback for the same slice without intentionally applying that precedence. + +### Rendering and composition + +- `flexRender(def, context)` still works. Prefer ``, ``, or the standalone `` for the v9 component form. +- Use `tableOptions()` to type reusable partial option objects. +- Use `createTableHook()` only when several tables share features, row models, defaults, and registered components. It returns app-specific helpers such as `useAppTable`, `createAppColumnHelper`, and table/cell/header context hooks; it is not required for one-off tables. +- Invoke row, cell, column, header, and related methods through their instance. Their methods now live on prototypes, so destructuring, object spread, `Object.keys`, and `JSON.stringify` do not preserve/expose them. Table-instance methods are not affected. + +### TypeScript and helper changes + +| v8 | v9 | +| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `createColumnHelper()` | `createColumnHelper()` | +| Plain column array | Prefer `columnHelper.columns([...])` to preserve each nested column's `TValue` | +| `ColumnDef` | `ColumnDef` | +| `Column` | `Column` | +| `Table` / `Row` | `Table` / `Row` | +| `Cell` | `Cell` | +| Global `TableMeta` / `ColumnMeta` | Add `TFeatures` first, or register per-table `tableMeta` / `columnMeta` with `metaHelper()` | +| Augment `FilterFns`, `SortFns`, `AggregationFns`, `FilterMeta` | Register `filterFns`, `sortFns`, `aggregationFns`, and `filterMeta` slots | +| `RowData = unknown` | Row data must be a record or array | + +Infer `TFeatures` with `typeof features`. If deliberately using `stockFeatures`, use `StockFeatures`. Do not manually propagate generics when a helper can infer them. + +### Shared API and behavior changes + +Column pinning now uses logical regions, with no deprecated aliases: + +| v8 / pre-beta.38 | v9 beta.38+ | +| -------------------------------------------------------------- | ------------------------------------------------------------- | +| `columnPinning.left` / `.right` | `.start` / `.end` | +| `column.pin('left' \| 'right')` | `column.pin('start' \| 'end')` | +| `getIsPinned() === 'left' \| 'right'` | `'start' \| 'end'` | +| `row.getLeftVisibleCells()` / `getRightVisibleCells()` | `getStartVisibleCells()` / `getEndVisibleCells()` | +| `getLeftHeaderGroups()` / `getRightHeaderGroups()` | `getStartHeaderGroups()` / `getEndHeaderGroups()` | +| `getLeftFooterGroups()` / `getRightFooterGroups()` | `getStartFooterGroups()` / `getEndFooterGroups()` | +| `getLeftFlatHeaders()` / `getRightFlatHeaders()` | `getStartFlatHeaders()` / `getEndFlatHeaders()` | +| `getLeftLeafHeaders()` / `getRightLeafHeaders()` | `getStartLeafHeaders()` / `getEndLeafHeaders()` | +| `getLeftLeafColumns()` / `getRightLeafColumns()` | `getStartLeafColumns()` / `getEndLeafColumns()` | +| `getLeftVisibleLeafColumns()` / `getRightVisibleLeafColumns()` | `getStartVisibleLeafColumns()` / `getEndVisibleLeafColumns()` | +| `getLeftTotalSize()` / `getRightTotalSize()` | `getStartTotalSize()` / `getEndTotalSize()` | +| `column.getStart('left')` | `column.getStart('start')` | +| `column.getAfter('right')` | `column.getAfter('end')` | +| `column.getIndex('left' \| 'right')` | `column.getIndex('start' \| 'end')` | + +This is logical table positioning, not automatic DOM-direction styling. Use CSS logical inset properties for sticky layouts. `columnResizeDirection` is unchanged. + +Other exact changes: + +| v8 | v9 | +| ----------------------------------------- | --------------------------------------------------------------------------------- | +| Table option `enablePinning` | `enableColumnPinning` plus `enableRowPinning`; per-column `enablePinning` remains | +| Combined `ColumnSizing` | `columnSizingFeature`; add `columnResizingFeature` for interaction | +| `columnSizingInfo` | `columnResizing` | +| `setColumnSizingInfo()` | `setColumnResizing()` | +| `onColumnSizingInfoChange` | `onColumnResizingChange` | +| `sortingFn` | `sortFn` | +| `column.getSortingFn()` | `column.getSortFn()` | +| `column.getAutoSortingFn()` | `column.getAutoSortFn()` | +| `SortingFn` / `SortingFns` / `sortingFns` | `SortFn` / `SortFns` / `sortFns` | +| `row._getAllCellsByColumnId()` | `row.getAllCellsByColumnId()` | +| `table._getPinnedRows()` | `getTopRows()`, `getCenterRows()`, or `getBottomRows()` | +| `table._getFacetedRowModel()` | Public faceting APIs on the relevant column/table | +| `table._getFacetedMinMaxValues()` | `getFacetedMinMaxValues()` | +| `table._getFacetedUniqueValues()` | `getFacetedUniqueValues()` | + +All other underscore-prefixed internals are removed. `getIsSomeRowsSelected()` and `getIsSomePageRowsSelected()` now mean **at least one**, including when all are selected. Compute indeterminate state with `getIsSomeRowsSelected() && !getIsAllRowsSelected()` or `getIsSomePageRowsSelected() && !getIsAllPageRowsSelected()`. + +## Migration procedure + +1. Upgrade imports and replace `useReactTable` with `useTable`. +2. Inventory every used state slice, table/column/row method, row model, function registry, and internal `_` API. +3. Build `tableFeatures()` with the corresponding features first, followed by row-model and registry slots; remove `getCoreRowModel`. +4. Apply every mapping above, including physical-to-logical pinning and the sizing/resizing split. +5. Add `typeof features` to helpers and explicit public types; migrate meta and function registry augmentation. +6. Replace `getState()` and `onStateChange`; choose internal, controlled per-slice, or external-atom ownership deliberately. +7. Audit destructured object methods and shallow clones of rows/cells/columns/headers. +8. Migrate rendering and optionally introduce `tableOptions`, `table.Subscribe`, or `createTableHook` where they solve an actual composition/render boundary. +9. Type-check, then exercise sorting, filtering, grouping, pagination, expansion, pinning, resizing, selection, and controlled/server-side flows that the table uses. +10. Remove `stockFeatures` after the feature audit if bundle specificity matters; remove `useLegacyTable` rather than treating it as the destination. + +## Final migration checklist + +- [ ] Replace `useReactTable` with `useTable`; remove any temporary `useLegacyTable` endpoint. +- [ ] Register every used stock feature explicitly and put each prerequisite before its dependent slot. +- [ ] Remove `getCoreRowModel`; move all eight optional row-model factories into `tableFeatures`. +- [ ] Move `filterFns`, `sortFns`, `aggregationFns`, and `filterMeta` into feature slots. +- [ ] Replace `table.getState()` and top-level `onStateChange`; choose selectors, per-slice callbacks, store subscription, or external atoms deliberately. +- [ ] Audit external-atom precedence/reset ownership and every controlled slice update path. +- [ ] Replace destructured, spread, serialized, or bare-callback row/cell/column/header methods. +- [ ] Replace every pinning state key, argument, comparison, method family, and sticky CSS use of left/right with start/end. +- [ ] Split `enablePinning`; split sizing/resizing and rename its state, setter, and callback. +- [ ] Apply every sorting option, method, type, interface, and built-in registry rename. +- [ ] Remove each listed underscore-prefixed internal API and use the public replacement. +- [ ] Rebuild indeterminate selection checks with the matching all-selected predicate. +- [ ] Update helpers and explicit public types for `TFeatures`; use `columns()` and `StockFeatures` where applicable. +- [ ] Update meta generics or per-table meta slots; replace function/meta augmentation with registry slots. +- [ ] Ensure `RowData` is a record or array. +- [ ] Migrate FlexRender usage; adopt `tableOptions` or `createTableHook` only where repeated composition warrants it. +- [ ] Type-check and test every enabled client/manual feature flow, including LTR/RTL pinning and resizing. +- [ ] Audit away temporary `stockFeatures` usage when explicit tree-shaking is the intended end state. + +## Common migration failures + +- An API is missing because its feature was not registered, not because v9 removed it. +- A row model is placed in table options or an obsolete `rowModels` object instead of its feature slot. +- A controlled value is supplied without its matching per-slice callback, freezing that slice. +- A React parent still re-renders for every table update because the default selector was retained while assuming atom reads alone narrowed it. +- An extracted `row.getValue`, `cell.getContext`, or column/header method loses `this`. +- Sticky pinning is renamed in state but not in CSS or every header/row sizing call. +- An indeterminate selection checkbox stays indeterminate when all rows are selected. + +## API discovery + +Inspect `node_modules/@tanstack/react-table/src/index.ts` and `node_modules/@tanstack/table-core/src/index.ts` for the installed v9 exports and types. Inspect `src/legacy.ts` only to identify temporary bridge code that remains to be removed. diff --git a/packages/react-table/skills/react/client-to-server/SKILL.md b/packages/react-table/skills/react/client-to-server/SKILL.md deleted file mode 100644 index 70bef62c2a..0000000000 --- a/packages/react-table/skills/react/client-to-server/SKILL.md +++ /dev/null @@ -1,380 +0,0 @@ ---- -name: react/client-to-server -description: > - Convert a client-side `@tanstack/react-table` v9 table to server-side - (manual modes). Pass server-paginated/sorted/filtered rows as `data`, set - `manualPagination` / `manualSorting` / `manualFiltering` / `manualGrouping` / - `manualExpanding` for whatever the server now owns, supply `rowCount` so - `getPageCount()` works, and DROP the matching factory from `tableFeatures()` (no - `paginatedRowModel` slot if the server paginates). Own the relevant state slices - via external atoms (`useCreateAtom` + `options.atoms`) so a query can key on - the slice and refetch automatically — OR via classic `state` + `on*Change` - controlled state. -type: lifecycle -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - state-management - - pagination - - filtering - - sorting - - react/table-state -sources: - - TanStack/table:examples/react/basic-external-atoms/src/main.tsx - - TanStack/table:examples/react/with-tanstack-query/src/main.tsx - - TanStack/table:examples/react/with-tanstack-query/src/fetchData.ts ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — the atom model is what makes the cleanest server-side wiring possible. - -## Why "client-to-server" - -A client-side table sees every row, sorts/filters/paginates them locally, and renders a slice. A server-side table sees only the slice the server returned for the current request; the table must be told "don't try to slice this again — and here's the total row count so you can render a pager". - -Four moves convert any client table to a server table: - -1. **`manualX: true`** for whichever operations the server owns. -2. **Drop the matching factory** from `tableFeatures()` so it doesn't ship in your bundle. -3. **Provide `rowCount`** so `table.getPageCount()` / `getCanNextPage()` work. -4. **Own the slice state externally** so your data fetcher can key on it. - -## Setup - -Two state-ownership patterns work; pick one per slice. - -### Pattern A — external atom (cleanest with Query/SWR) - -```tsx -import * as React from 'react' -import { useCreateAtom, useSelector } from '@tanstack/react-store' -import { - useTable, - tableFeatures, - rowPaginationFeature, - createColumnHelper, -} from '@tanstack/react-table' -import type { PaginationState } from '@tanstack/react-table' - -const features = tableFeatures({ rowPaginationFeature }) -const columnHelper = createColumnHelper() -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('age', { header: 'Age' }), -]) - -const EMPTY: Person[] = [] - -function ServerTable() { - // 1) Own pagination in an external atom. - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - const pagination = useSelector(paginationAtom) - - // 2) Fetch keyed on the atom value. - const [serverPage, setServerPage] = React.useState<{ - rows: Person[] - rowCount: number - } | null>(null) - React.useEffect(() => { - let cancelled = false - fetchPeople(pagination).then((page) => { - if (!cancelled) setServerPage(page) - }) - return () => { - cancelled = true - } - }, [pagination]) - - // 3) Manual pagination + rowCount. No paginatedRowModel in features. - const table = useTable({ - features, - columns, - data: serverPage?.rows ?? EMPTY, // EMPTY at module scope - rowCount: serverPage?.rowCount, - atoms: { pagination: paginationAtom }, // table writes here directly - manualPagination: true, - }) - // No onPaginationChange — table.setPageIndex(...) writes through the atom. -} -``` - -Source: `examples/react/basic-external-atoms/src/main.tsx` (atoms wiring); `examples/react/with-tanstack-query/src/main.tsx` (rowCount + manualPagination). - -### Pattern B — classic `state` + `on*Change` - -```tsx -const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: 10, -}) - -const table = useTable({ - features, - columns, - data: serverPage?.rows ?? EMPTY, - rowCount: serverPage?.rowCount, - state: { pagination }, - onPaginationChange: setPagination, // REQUIRED with state.pagination - manualPagination: true, -}) -``` - -Both work. `state` + `on*Change` is familiar from v8; atoms compose more cleanly with Query (the table writes to the atom, the query key includes the atom value, the query refetches automatically). - -## Core Patterns - -### Combining server-side sort + filter + pagination - -Add the matching `manual*` flags for each operation the server now owns. Local features (column visibility, ordering, pinning) still work because they don't depend on the row model. - -```tsx -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - columnFilteringFeature, - columnVisibilityFeature, // still local - columnPinningFeature, // still local -}) - -const sortingAtom = useCreateAtom([]) -const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, -}) -const columnFiltersAtom = useCreateAtom([]) - -const sorting = useSelector(sortingAtom) -const pagination = useSelector(paginationAtom) -const columnFilters = useSelector(columnFiltersAtom) - -const serverArgs = { sorting, pagination, columnFilters } -// ... fetch keyed on serverArgs - -const table = useTable({ - features, // no sorted/filtered/paginated factories — server owns them - columns, - data: serverPage?.rows ?? EMPTY, - rowCount: serverPage?.rowCount, - atoms: { - sorting: sortingAtom, - pagination: paginationAtom, - columnFilters: columnFiltersAtom, - }, - manualSorting: true, - manualFiltering: true, - manualPagination: true, -}) -``` - -Source: `examples/react/basic-external-atoms/src/main.tsx`. - -### When NOT to manual-mode a slice - -If the server returns the **entire** dataset, leave the table client-side. Manual mode is for slices the server has already trimmed. - -## Common Mistakes - -### CRITICAL Forgetting `manualPagination` / `manualSorting` / `manualFiltering` - -Wrong: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), // factory registered — but server already paginated -}) -const table = useTable({ - features, - columns, - data: serverPage.rows, - // missing manualPagination -}) -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - // no paginatedRowModel — server paginates -}) -const table = useTable({ - features, - columns, - data: serverPage.rows, - rowCount: serverPage.rowCount, - manualPagination: true, -}) -``` - -Without `manualPagination: true`, the table tries to slice the already-server-sliced page a second time, producing rows that don't exist (or visibly wrong pagination). -Source: `examples/react/with-tanstack-query/src/main.tsx`. - -### CRITICAL Missing `rowCount` - -Wrong: - -```tsx -const table = useTable({ - features, - columns, - data: serverPage.rows, - manualPagination: true, - // missing rowCount: serverPage.totalRowCount -}) -// table.getPageCount() → 1, pager locks at "Page 1 of 1" -``` - -Correct: - -```tsx -const table = useTable({ - features, - columns, - data: serverPage.rows, - rowCount: serverPage.rowCount, // ← required for accurate pager - manualPagination: true, -}) -``` - -Without `rowCount`, `getPageCount()` falls back to `Math.ceil(data.length / pageSize)` — which is 1 if the server returned a single page. -Source: `examples/react/with-tanstack-query/src/main.tsx`. - -### HIGH `state.pagination` without `onPaginationChange` - -Wrong: - -```tsx -const [pagination] = React.useState({ - pageIndex: 0, - pageSize: 10, -}) -useTable({ - features, - columns, - data, - state: { pagination }, - // missing onPaginationChange — table.setPageIndex is a no-op - manualPagination: true, -}) -``` - -Correct: - -```tsx -const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: 10, -}) -useTable({ - features, - columns, - data, - state: { pagination }, - onPaginationChange: setPagination, // ← required - manualPagination: true, -}) -// OR — use atoms.pagination instead, which doesn't need a writeback handler. -``` - -The library treats `state` as controlled; without a writeback handler, `table.setPageIndex(...)` writes nowhere. -Source: `docs/framework/react/guide/table-state.md`. - -### HIGH Leaving `paginatedRowModel` registered for a server-paginated table - -Wrong: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), // ships for nothing -}) -useTable({ - features, - columns, - data: serverPage.rows, - manualPagination: true, -}) -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - // drop paginatedRowModel — server owns pagination -}) -useTable({ - features, - columns, - data: serverPage.rows, - rowCount: serverPage.rowCount, - manualPagination: true, -}) -``` - -The factory ships in your bundle for no reason. Manual mode + the factory will also let the factory re-slice your already-sliced server page if `manualPagination` is ever flipped off. -Source: maintainer guidance. - -### HIGH Mixing `state.X` and `atoms.X` for the same slice - -Wrong: - -```tsx -useTable({ - features, - columns, - data, - state: { pagination }, // silently ignored - onPaginationChange: setPagination, // silently ignored - atoms: { pagination: paginationAtom }, // wins - manualPagination: true, -}) -``` - -Correct: - -```tsx -// Pick one ownership mechanism per slice. -useTable({ - features, - columns, - data, - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -Precedence is `options.atoms[key]` > `options.state[key]` > internal. The `state` plumbing is dead code in this configuration. -Source: `examples/react/basic-external-atoms/src/main.tsx`. - -### MEDIUM Recreating `data` array identity in JSX - -Wrong: - -```tsx - -// `?? []` produces a new array reference each render → internal memos bust -``` - -Correct: - -```tsx -const EMPTY: Person[] = [] // module scope - - -// or wrap in useMemo -``` - -Internal memoization keys off identity. A fresh `[]` each render bypasses memos and may force re-computation. -Source: maintainer guidance; `examples/react/with-tanstack-query/src/main.tsx`. - -## See Also - -- `tanstack-table/react/compose-with-tanstack-query` — the canonical Query + server-side pattern. -- `tanstack-table/react/compose-with-tanstack-store` — sharing slice atoms across components. -- `tanstack-table/react/table-state` — selectors, ``, external atoms. -- `tanstack-table/react/production-readiness` — when to narrow selectors as the table grows. diff --git a/packages/react-table/skills/react/compose-with-tanstack-form/SKILL.md b/packages/react-table/skills/react/compose-with-tanstack-form/SKILL.md deleted file mode 100644 index c7ab74d1e4..0000000000 --- a/packages/react-table/skills/react/compose-with-tanstack-form/SKILL.md +++ /dev/null @@ -1,353 +0,0 @@ ---- -name: react/compose-with-tanstack-form -description: > - Editable cells for `@tanstack/react-table` v9 via `@tanstack/react-form`. The - table is the layout primitive; the form owns editing state. Use - `createFormHook` to register reusable field components (`TextField`, - `NumberField`, `SelectField`), then in each column's `cell` return - `{(field) => }`. - Critical typing gotcha: if your row has a recursive `subRows`, use - `Omit` for the form row type — TanStack Form's `DeepKeys` - recurses and hits TS2589. Subscribe to `form.state.values.data.length` (not - the whole array) for row add/remove re-renders. -type: composition -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - row-selection - - column-definitions - - react/table-state -sources: - - TanStack/table:examples/react/with-tanstack-form/src/main.tsx - - TanStack/table:examples/react/with-tanstack-form/src/form.tsx ---- - -This skill builds on `tanstack-table/state-management`, `tanstack-table/react/table-state`, and `tanstack-table/column-definitions`. Read those first. - -## Why this exists - -TanStack Table v9 deliberately ships no built-in editing — Kevin (the maintainer) scoped it out in favor of composing with TanStack Form. The form owns row-level state, validation, dirty tracking, submit; the table is the layout/sort/filter/paginate engine. This is the v9-blessed answer to "how do I make editable cells?" - -## Setup - -```bash -pnpm add @tanstack/react-table @tanstack/react-form zod -``` - -Define your field components and a form hook in a `form.tsx` module. Source: `examples/react/with-tanstack-form/src/form.tsx`. - -```tsx -import { createFormHook, createFormHookContexts } from '@tanstack/react-form' - -const { fieldContext, formContext } = createFormHookContexts() - -function TextField() { - /* reads field state from fieldContext */ -} -function NumberField() { - /* … */ -} -function SelectField() { - /* … */ -} -function SubmitButton() { - /* … */ -} -function FormStateIndicator() { - /* … */ -} - -export const { useAppForm } = createFormHook({ - fieldComponents: { TextField, NumberField, SelectField }, - formComponents: { SubmitButton, FormStateIndicator }, - fieldContext, - formContext, -}) -``` - -## Core Pattern — editable people table - -```tsx -import * as React from 'react' -import { - useTable, - tableFeatures, - columnFilteringFeature, - rowPaginationFeature, - createColumnHelper, - createFilteredRowModel, - createPaginatedRowModel, - filterFns, -} from '@tanstack/react-table' -import { useStore } from '@tanstack/react-form' -import { z } from 'zod' -import { useAppForm } from './form' -import type { Person } from './makeData' - -// CRITICAL: flatten recursive subRows before handing rows to the form. -// Without Omit, TanStack Form's DeepKeys walks subRows and hits TS2589. -type FormRow = Omit - -const features = tableFeatures({ - rowPaginationFeature, - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - filterFns, -}) -const columnHelper = createColumnHelper() - -function App() { - const initialData: FormRow[] = makeData(100) - - const form = useAppForm({ - defaultValues: { data: initialData }, - onSubmit: ({ value }) => { - alert(`Submitted ${value.data.length} records`) - }, - validators: { onChange: z.object({ data: z.array(personSchema) }) }, - }) - - // Memo'd columns — field bindings close over `form`, so without memoization - // we'd build new column defs on every keystroke. - const columns = React.useMemo( - () => - columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First Name', - cell: ({ row }) => ( - - {(field) => } - - ), - }), - columnHelper.accessor('age', { - header: 'Age', - cell: ({ row }) => ( - - {(field) => } - - ), - }), - columnHelper.accessor('status', { - header: 'Status', - cell: ({ row }) => ( - - {(field) => } - - ), - }), - ]), - [form], - ) - - // Subscribe ONLY to length — triggers re-renders on add/remove without infinite loops - // (vs subscribing to data, which fires on every keystroke). - const dataLength = useStore(form.store, (state) => state.values.data.length) - void dataLength - - const table = useTable({ - features, - columns, - data: form.state.values.data, // table reads fresh form values each render - }) - - const addRow = () => - form.pushFieldValue('data', { - firstName: '', - lastName: '', - age: 0, - visits: 0, - progress: 0, - status: 'single', - }) - - const refreshData = () => form.reset({ data: makeData(100) }) - - return ( - <> - - - - {/* … */} - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((cell) => ( - - ))} - - ))} - -
- -
- - - ) -} -``` - -Source: `examples/react/with-tanstack-form/src/main.tsx`. - -## Add / remove rows - -`form.pushFieldValue('data', newRow)` adds; `form.removeFieldValue('data', index)` removes; `form.reset({ data })` replaces. The `useStore` subscription on `state.values.data.length` re-renders the holder so the table sees the new array length and renders the new row. - -## Common Mistakes - -### CRITICAL Typing rows as `Person` with recursive `subRows` - -Wrong: - -```tsx -const form = useAppForm({ defaultValues: { data: makeData(100) as Person[] } }) -// TanStack Form's DeepKeys walks Person.subRows recursively → TS2589 -// ("Type instantiation is excessively deep and possibly infinite") -``` - -Correct: - -```tsx -type FormRow = Omit -const initialData: FormRow[] = makeData(100) -const form = useAppForm({ defaultValues: { data: initialData } }) -const columnHelper = createColumnHelper() -``` - -Always strip the recursive child field from the row type you hand to the form. -Source: `examples/react/with-tanstack-form/src/main.tsx`. - -### CRITICAL Subscribing to the whole `state.values.data` array - -Wrong: - -```tsx -// Every keystroke in any cell re-renders App → recreates form → re-binds every cell. -const data = useStore(form.store, (s) => s.values.data) -``` - -Correct: - -```tsx -// Subscribe to length only — triggers re-renders on add/remove, ignores edits. -const dataLength = useStore(form.store, (state) => state.values.data.length) -void dataLength -// Table reads `data: form.state.values.data` directly on render. -``` - -Source: `examples/react/with-tanstack-form/src/main.tsx`. - -### HIGH Forgetting `useMemo` around columns - -Wrong: - -```tsx -function App() { - const form = useAppForm({ - /* … */ - }) - const columns = columnHelper.columns([ - // new column defs every render - columnHelper.accessor('firstName', { - cell: ({ row }) => ( - - {(field) => } - - ), - }), - ]) -} -``` - -Correct: - -```tsx -const columns = React.useMemo( - () => - columnHelper.columns([ - columnHelper.accessor('firstName', { - cell: ({ row }) => ( - - {(field) => } - - ), - }), - ]), - [form], -) -``` - -Cell renderers close over `form`. Without memoization the column defs change every render, busting internal memos and remounting field components. -Source: `examples/react/with-tanstack-form/src/main.tsx`. - -### HIGH Passing the form itself in `useTable`'s `data` - -Wrong: - -```tsx -const table = useTable({ - features, - columns, - data: form, // wrong — table only needs the row array -}) -``` - -Correct: - -```tsx -const table = useTable({ - features, - columns, - data: form.state.values.data, -}) -``` - -The table consumes the rows array. Mix the form's data into the table's `data` prop; don't try to make the table aware of the form instance. -Source: `examples/react/with-tanstack-form/src/main.tsx`. - -### MEDIUM Trying to reuse v8's `tableMeta.updateData` pattern - -Wrong: - -```tsx -// v8 muscle memory: track edits in tableMeta with a per-cell useState. -const table = useReactTable({ - data, - columns, - meta: { - updateData: (rowIndex, columnId, value) => { - /* manual setState dance */ - }, - }, -}) -``` - -Correct: - -```tsx -// v9 idiom: TanStack Form owns the data, table renders it. -const form = useAppForm({ defaultValues: { data } }) -const table = useTable({ - features, - columns, - data: form.state.values.data, -}) -``` - -The v8 `tableMeta.updateData` pattern still works mechanically, but the form composition handles validation, dirty tracking, submit, and add/remove for free. -Source: maintainer guidance. - -## See Also - -- `tanstack-table/react/table-state` — base table reactivity. -- `tanstack-table/react/compose-with-tanstack-pacer` — debounce column filter inputs on the same screen. -- `tanstack-table/column-definitions` — cell renderer API. -- `tanstack-table/row-selection` — row selection works alongside per-cell editing. diff --git a/packages/react-table/skills/react/compose-with-tanstack-pacer/SKILL.md b/packages/react-table/skills/react/compose-with-tanstack-pacer/SKILL.md deleted file mode 100644 index cc6fad27fa..0000000000 --- a/packages/react-table/skills/react/compose-with-tanstack-pacer/SKILL.md +++ /dev/null @@ -1,287 +0,0 @@ ---- -name: react/compose-with-tanstack-pacer -description: > - Use `@tanstack/react-pacer` to debounce/throttle the high-frequency writes - that drive an interactive `@tanstack/react-table` v9 table: column filter - inputs and column resize state. Pattern: import `useDebouncedCallback` - from `@tanstack/react-pacer/debouncer`, wrap your `onChange` writer in - it, and keep local input state so typing feels instant. For column - resizing, throttle `onColumnResizingChange` so a drag doesn't push 60+ - state updates per second. Pacer is the v9 replacement for the hand-rolled - `DebouncedInput` setTimeout component from v8 examples. -type: composition -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - filtering - - column-layout - - react/table-state -sources: - - TanStack/table:examples/react/basic-subscribe/src/main.tsx - - TanStack/table:examples/react/with-tanstack-form/src/main.tsx - - TanStack/table:examples/react/kitchen-sink/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management`, `tanstack-table/react/table-state`, and the `filtering` core skill. Read those first. - -## Why this exists - -A column filter input writes to `state.columnFilters` on every keystroke. Each write recomputes the filtered row model. For 100 rows that's fine; for 10k+ it's visibly janky. Pacer debounces the write so the filter only fires once after typing stops, while the input itself updates instantly via local state. - -The same principle applies to drag-to-resize: a single drag can fire `onColumnResizingChange` 60+ times per second. Throttling at ~16ms (one frame) keeps the perceived smoothness without spamming the store. - -## Setup - -```bash -pnpm add @tanstack/react-pacer -``` - -```tsx -import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer' -import { useThrottledCallback } from '@tanstack/react-pacer/throttler' -``` - -## Core Pattern — `DebouncedInput` for column filters - -The shape comes straight from the v9 examples — keep local input state so the input is instant, debounce the writer so the store only sees the trailing value. - -```tsx -import * as React from 'react' -import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer' - -function DebouncedInput({ - value: initialValue, - onChange, - debounce = 300, - ...props -}: { - value: string | number - onChange: (value: string | number) => void - debounce?: number -} & Omit, 'onChange'>) { - // Local state so the input feels instant. - const [value, setValue] = React.useState(initialValue) - - React.useEffect(() => { - setValue(initialValue) - }, [initialValue]) - - // Debounced writer — fires once after `debounce` ms of inactivity. - const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce }) - - return ( - { - setValue(e.target.value) // instant UI update - debouncedOnChange(e.target.value) // debounced store write - }} - /> - ) -} - -// Usage in a column filter: -; column.setFilterValue(v)} - placeholder="Search..." -/> -``` - -Source: `examples/react/basic-subscribe/src/main.tsx`; `examples/react/with-tanstack-form/src/main.tsx`. - -## Debounce vs throttle — choose by intent - -| Pattern | Use case | Typical wait | -| ------------ | ------------------------------------------------------------------------ | ---------------- | -| **Debounce** | "Wait until they stop typing, then commit" — filter inputs, search boxes | 250–500ms | -| **Throttle** | "Fire at most every N ms" — drag-to-resize, scroll-triggered fetches | 16ms (one frame) | - -```tsx -// Throttled column resize -const throttledResize = useThrottledCallback( - (next: ColumnResizingState) => columnResizingAtom.set(next), - { wait: 16 }, -) -``` - -## Global filter with debounce + instant input - -The exact pattern used in `examples/react/basic-subscribe/src/main.tsx`: - -```tsx - - {(globalFilter) => ( - table.setGlobalFilter(value)} - placeholder="Search all columns..." - /> - )} - -``` - -The `` wrapper keeps the input controlled by the table's atom (so external resets propagate), while `DebouncedInput`'s local state keeps the visual update instant. - -## Common Mistakes - -### CRITICAL Writing filter values directly on every keystroke - -Wrong: - -```tsx - column.setFilterValue(e.target.value)} /> -// On a 10k-row table, this recomputes the filtered row model per character — janky. -``` - -Correct: - -```tsx - column.setFilterValue(v)} -/> -``` - -Debounce the store write; keep the input instant via local state. -Source: `examples/react/basic-subscribe/src/main.tsx`. - -### HIGH Rolling your own `setTimeout` debounce - -Wrong: - -```tsx -function DebouncedInput({ value, onChange, debounce = 300 }) { - const [v, setV] = React.useState(value) - React.useEffect(() => { - const t = setTimeout(() => onChange(v), debounce) - return () => clearTimeout(t) - }, [v]) - // Works, but reinvents what useDebouncedCallback already provides with proper - // ref stability, cleanup, and trailing-edge semantics. -} -``` - -Correct: - -```tsx -function DebouncedInput({ - value: initialValue, - onChange, - debounce = 300, - ...props -}) { - const [value, setValue] = React.useState(initialValue) - React.useEffect(() => { - setValue(initialValue) - }, [initialValue]) - const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce }) - return ( - { - setValue(e.target.value) - debouncedOnChange(e.target.value) - }} - /> - ) -} -``` - -v8 examples shipped the hand-rolled version. v9 explicitly delegates to Pacer. -Source: `examples/react/basic-subscribe/src/main.tsx`. - -### HIGH Debouncing the local input state instead of (or in addition to) the writer - -Wrong: - -```tsx -const debouncedSetLocal = useDebouncedCallback(setValue, { wait: 300 }) - debouncedSetLocal(e.target.value)} /> -// User sees stale characters in the input box. -``` - -Correct: - -```tsx -const debouncedOnChange = useDebouncedCallback(onChange, { wait: 300 }) - { - setValue(e.target.value) // instant local update - debouncedOnChange(e.target.value) // debounced store write only - }} -/> -``` - -Local state should always be instant. Only the expensive store write should debounce. -Source: `examples/react/basic-subscribe/src/main.tsx`. - -### HIGH Throttling column resize at 250ms - -Wrong: - -```tsx -const throttledResize = useThrottledCallback(setResize, { wait: 250 }) -// 4 fps drag → visibly laggy. -``` - -Correct: - -```tsx -const throttledResize = useThrottledCallback(setResize, { wait: 16 }) -// ~60 fps, smooth. -``` - -Use roughly one frame (16ms). 250ms is fine for filter writes; far too long for drag interactions. -Source: maintainer guidance. - -### MEDIUM Wrapping `column.setFilterValue` directly without local input state - -Wrong: - -```tsx -const debouncedSet = useDebouncedCallback(column.setFilterValue, { wait: 300 }) - debouncedSet(e.target.value)} /> -// Input becomes uncontrolled-feeling because the render goes through the slow path. -``` - -Correct: - -```tsx - column.setFilterValue(v)} -/> -``` - -The `DebouncedInput` pattern combines local state (instant) with debounced commit (cheap). Don't skip the local state. -Source: `examples/react/basic-subscribe/src/main.tsx`. - -### MEDIUM Using `wait: 0` and expecting debouncing - -Wrong: - -```tsx -const debouncedOnChange = useDebouncedCallback(onChange, { wait: 0 }) -// Effectively no debounce — fires synchronously. -``` - -Correct: - -```tsx -const debouncedOnChange = useDebouncedCallback(onChange, { wait: 300 }) -``` - -`wait` is meaningful. 250–500ms is the typical sweet spot for filter inputs; 16ms is the typical sweet spot for resize/scroll throttling. -Source: maintainer guidance. - -## See Also - -- `tanstack-table/react/table-state` — Subscribe boundaries for the debounced writers to feed into. -- `tanstack-table/filtering` — column filter feature API surface. -- `tanstack-table/column-layout` — column resize feature. -- `tanstack-table/react/compose-with-tanstack-query` — debounce filter input that feeds a server-side query. diff --git a/packages/react-table/skills/react/compose-with-tanstack-query/SKILL.md b/packages/react-table/skills/react/compose-with-tanstack-query/SKILL.md deleted file mode 100644 index bd7be9e56b..0000000000 --- a/packages/react-table/skills/react/compose-with-tanstack-query/SKILL.md +++ /dev/null @@ -1,469 +0,0 @@ ---- -name: react/compose-with-tanstack-query -description: > - Server-side / async data flow for `@tanstack/react-table` v9 with - `@tanstack/react-query`. Canonical pattern: external pagination atom via - `useCreateAtom` + `options.atoms` (NOT `state + on*Change`), - pagination object as part of `queryKey`, `manualPagination: true`, - `placeholderData: keepPreviousData` to avoid the 0-rows flash, and - `defaultData = useMemo(() => [], [])` to keep `data` reference stable - between fetches. `rowCount` from the API response so `getPageCount()` works. -type: composition -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - react/client-to-server - - pagination - - react/table-state -sources: - - TanStack/table:examples/react/with-tanstack-query/src/main.tsx - - TanStack/table:examples/react/with-tanstack-query/src/fetchData.ts ---- - -This skill builds on `tanstack-table/state-management`, `tanstack-table/react/table-state`, and `tanstack-table/react/client-to-server`. Read those first — Query composition is `client-to-server` with a specific server. - -## Why this pattern - -A v9 React table written against TanStack Query has three load-bearing decisions: - -1. **External pagination atom**, not `state` + `onPaginationChange`. Cleaner because the table writes to the atom directly; the query's `queryKey` watches the atom; refetches happen automatically. -2. **`placeholderData: keepPreviousData`** so the previous page stays visible while the next page fetches. Without it the table collapses to 0 rows on every page change and the scroll position jumps. -3. **Stable `data` fallback** (`defaultData = useMemo(() => [], [])`). `data: dataQuery.data?.rows ?? []` in JSX produces a new array each render and busts internal memos. - -Source: `examples/react/with-tanstack-query/src/main.tsx`. - -## Setup - -```bash -pnpm add @tanstack/react-table @tanstack/react-query @tanstack/react-store -``` - -Mount one `` at the root: - -```tsx -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -const queryClient = new QueryClient() - -ReactDOM.createRoot(rootElement).render( - - - - - , -) -``` - -## Core Pattern — canonical server-paginated table - -```tsx -import * as React from 'react' -import { keepPreviousData, useQuery } from '@tanstack/react-query' -import { useCreateAtom, useSelector } from '@tanstack/react-store' -import { - useTable, - tableFeatures, - rowPaginationFeature, - createColumnHelper, -} from '@tanstack/react-table' -import type { PaginationState } from '@tanstack/react-table' -import { fetchData } from './fetchData' // returns { rows, rowCount } -import type { Person } from './fetchData' - -const features = tableFeatures({ rowPaginationFeature }) - -const columnHelper = createColumnHelper() -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First Name', - cell: (i) => i.getValue(), - }), - columnHelper.accessor('lastName', { - header: 'Last Name', - cell: (i) => i.getValue(), - }), - columnHelper.accessor('age', { header: 'Age' }), - columnHelper.accessor('visits', { header: 'Visits' }), - columnHelper.accessor('status', { header: 'Status' }), - columnHelper.accessor('progress', { header: 'Profile Progress' }), -]) - -function App() { - // 1) Pagination atom — stable identity via useCreateAtom. - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - // 2) Subscribe so the query refetches on pagination changes. - const pagination = useSelector(paginationAtom, (s) => s) - - // 3) Query keyed on the pagination object — refetch on every page/size change. - const dataQuery = useQuery({ - queryKey: ['data', pagination], - queryFn: () => fetchData(pagination), - placeholderData: keepPreviousData, // 4) avoid 0-rows flash - }) - - // 5) Stable fallback — fresh `[]` in JSX would bust internal memos. - const defaultData = React.useMemo(() => [], []) - - // 6) Manual pagination + rowCount; no row model factories needed. - const table = useTable( - { - features, - columns, - data: dataQuery.data?.rows ?? defaultData, - rowCount: dataQuery.data?.rowCount, - atoms: { pagination: paginationAtom }, // table writes here directly - manualPagination: true, - }, - (state) => state, - ) - - return ( - <> - - {/* table.FlexRender header={h} */} - {/* table.FlexRender cell={c} */} -
-
- - - - - - Page{' '} - - {pagination.pageIndex + 1} of {table.getPageCount()} - - - - {dataQuery.isFetching ? 'Loading...' : null} -
- - ) -} -``` - -Source: `examples/react/with-tanstack-query/src/main.tsx` (this is the canonical example, near-verbatim). - -## Adding sort + filter - -The same pattern extends to multiple slices. Key the query on each and set the matching `manual*` flag. - -```tsx -const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, -}) -const sortingAtom = useCreateAtom([]) -const columnFiltersAtom = useCreateAtom([]) - -const pagination = useSelector(paginationAtom) -const sorting = useSelector(sortingAtom) -const columnFilters = useSelector(columnFiltersAtom) - -const dataQuery = useQuery({ - queryKey: ['data', { pagination, sorting, columnFilters }], - queryFn: () => fetchData({ pagination, sorting, columnFilters }), - placeholderData: keepPreviousData, -}) - -const table = useTable({ - features: tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - // no row model factories — server owns sort/filter/paginate - }), - columns, - data: dataQuery.data?.rows ?? defaultData, - rowCount: dataQuery.data?.rowCount, - atoms: { - pagination: paginationAtom, - sorting: sortingAtom, - columnFilters: columnFiltersAtom, - }, - manualSorting: true, - manualFiltering: true, - manualPagination: true, -}) -``` - -## Mutations and invalidation - -TanStack Table is a downstream consumer — it has no way to know the server data changed. Call `queryClient.invalidateQueries` after mutations: - -```tsx -const queryClient = useQueryClient() -const addPerson = useMutation({ - mutationFn: createPerson, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['data'] }), -}) -``` - -## Common Mistakes - -### CRITICAL Forgetting `manualPagination` / `manualSorting` / `manualFiltering` - -Wrong: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), // factory registered but server already paginated -}) -const table = useTable({ - features, - columns, - data: query.data?.rows ?? [], - // missing manualPagination -}) -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - // no paginatedRowModel — server paginates -}) -const table = useTable({ - features, - columns, - data: query.data?.rows ?? defaultData, - rowCount: query.data?.rowCount, - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -Without `manualPagination: true`, the table re-paginates the server-already-paginated 10-row "dataset" — `getPageCount()` returns 1, and the pager locks at "Page 1 of 1". -Source: `examples/react/with-tanstack-query/src/main.tsx`. - -### CRITICAL Missing `rowCount` - -Wrong: - -```tsx -const table = useTable({ - features, - columns, - data: query.data?.rows ?? defaultData, - atoms: { pagination: paginationAtom }, - manualPagination: true, - // missing rowCount -}) -``` - -Correct: - -```tsx -const table = useTable({ - features, - columns, - data: query.data?.rows ?? defaultData, - rowCount: query.data?.rowCount, // ← required for accurate pager - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -`getPageCount()` falls back to `Math.ceil(data.length / pageSize)` — which equals 1 when the server returned one page. -Source: `examples/react/with-tanstack-query/src/main.tsx`. - -### CRITICAL `queryKey` doesn't include the pagination state - -Wrong: - -```tsx -useQuery({ - queryKey: ['data'], // never changes - queryFn: () => fetchData(pagination), -}) -``` - -Correct: - -```tsx -useQuery({ - queryKey: ['data', pagination], // refetch on pagination change - queryFn: () => fetchData(pagination), - placeholderData: keepPreviousData, -}) -``` - -Query has no way to know its inputs changed unless they're in `queryKey`. Pager button clicks update the atom but the query never refetches. -Source: `examples/react/with-tanstack-query/src/main.tsx`. - -### HIGH Skipping `placeholderData: keepPreviousData` - -Wrong: - -```tsx -useQuery({ - queryKey: ['data', pagination], - queryFn: () => fetchData(pagination), -}) -// Between pages: table renders 0 rows, container collapses, scroll position jumps. -``` - -Correct: - -```tsx -useQuery({ - queryKey: ['data', pagination], - queryFn: () => fetchData(pagination), - placeholderData: keepPreviousData, // previous page stays visible while fetching -}) -``` - -The previous page renders during the fetch — no flash, no jump. -Source: `examples/react/with-tanstack-query/src/main.tsx`. - -### HIGH Recreating `data: query.data?.rows ?? []` in JSX - -Wrong: - -```tsx -const table = useTable({ - features, - columns, - data: query.data?.rows ?? [], // new identity every render - // ... -}) -``` - -Correct: - -```tsx -const defaultData = React.useMemo(() => [], []) -// or: const EMPTY: Person[] = [] at module scope - -const table = useTable({ - features, - columns, - data: query.data?.rows ?? defaultData, - // ... -}) -``` - -`?? []` creates a fresh array reference each render, busting internal memos that depend on `data` identity. -Source: `examples/react/with-tanstack-query/src/main.tsx` (uses `useMemo`). - -### HIGH Mixing `state.pagination` + `onPaginationChange` AND `atoms.pagination` - -Wrong: - -```tsx -useTable({ - features, - columns, - data, - state: { pagination }, // silently ignored - onPaginationChange: setPagination, // silently ignored - atoms: { pagination: paginationAtom }, // wins - manualPagination: true, -}) -``` - -Correct: - -```tsx -// Pick one. The atom pattern is canonical for Query. -useTable({ - features, - columns, - data, - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -Precedence is `atoms` > `state` > internal. The `state` plumbing is dead. -Source: `examples/react/basic-external-atoms/src/main.tsx`. - -### HIGH Forgetting `invalidateQueries` after mutations - -Wrong: - -```tsx -const addPerson = useMutation({ - mutationFn: createPerson, - // missing onSuccess invalidation -}) -// Table never sees the new row. -``` - -Correct: - -```tsx -const queryClient = useQueryClient() -const addPerson = useMutation({ - mutationFn: createPerson, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['data'] }), -}) -``` - -The table is downstream of Query. Mutations must invalidate the relevant query keys. -Source: docs/framework/react/react-query. - -### MEDIUM Leaving `paginatedRowModel` registered when the server paginates - -Wrong: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), // ships unused code -}) -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - // no paginatedRowModel — server paginates -}) -``` - -Bundle waste plus a foot-gun if `manualPagination` is ever flipped off. -Source: `examples/react/with-tanstack-query/src/main.tsx`. - -## See Also - -- `tanstack-table/react/client-to-server` — the underlying manual-mode mechanics. -- `tanstack-table/react/compose-with-tanstack-store` — owning state slices via atoms. -- `tanstack-table/react/compose-with-tanstack-virtual` — infinite scroll = Virtual + `useInfiniteQuery`. -- `tanstack-table/react/compose-with-tanstack-pacer` — debounce filter writes that feed the query. diff --git a/packages/react-table/skills/react/compose-with-tanstack-store/SKILL.md b/packages/react-table/skills/react/compose-with-tanstack-store/SKILL.md deleted file mode 100644 index c0ccb8b1a5..0000000000 --- a/packages/react-table/skills/react/compose-with-tanstack-store/SKILL.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -name: react/compose-with-tanstack-store -description: > - `@tanstack/react-table` v9 is built on TanStack Store. Each state slice - (sorting, pagination, rowSelection, columnFilters, …) is a separate atom. - The table exposes three READ surfaces — `table.atoms.` (per-slice - readonly), `table.store` (flat readonly view), `table.state` (selector - output from `useTable`) — and two WRITE paths — internal - `table.baseAtoms.` OR YOUR `options.atoms[slice]` if you opt to own - the slice. Use `useCreateAtom` from `@tanstack/react-store` for stable - identity, `useSelector` for fine-grained reads, and pass the atom in - `options.atoms` so the table writes through it directly — no `on*Change` - handler required. -type: composition -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - react/table-state - - state-management -sources: - - TanStack/table:docs/framework/react/guide/table-state.md - - TanStack/table:examples/react/basic-external-atoms/src/main.tsx - - TanStack/table:examples/react/basic-subscribe/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — `state-management` explains the atom model conceptually; `table-state` covers the basic React adapter. This skill goes deeper into composition with `@tanstack/react-store`. - -## What "compose with TanStack Store" means in practice - -A v9 React table is already a Store consumer — every slice (`sorting`, `pagination`, `rowSelection`, `columnFilters`, `columnVisibility`, …) is an atom managed by Store. Composing with Store means **opting to own one or more of those atoms yourself** so you can: - -- Share a slice across multiple components without prop-drilling the table instance. -- Share a slice across multiple **tables** (e.g. a global filter atom). -- Persist a slice (subscribe to localStorage outside the table). -- Integrate with other atom-based code (atoms in your data layer). -- Skip the `on*Change` callback dance — the table writes through your atom directly. - -## Setup - -Install `@tanstack/react-store` if you don't already have it (it's a peer of `@tanstack/react-table`): - -```bash -pnpm add @tanstack/react-store -``` - -Three APIs do the work: - -```tsx -import { useCreateAtom, useSelector } from '@tanstack/react-store' -``` - -- `useCreateAtom(initial)` — create an atom with stable identity inside a component (React-safe replacement for `useRef(createAtom(...))`). -- `useSelector(atomOrStore, selector?)` — subscribe a component to an atom or store. -- The table's `options.atoms` option — hand ownership of named slices to your atoms. - -## Core Patterns - -### 1. Own a slice externally - -```tsx -import { useCreateAtom, useSelector } from '@tanstack/react-store' -import { - useTable, - tableFeatures, - rowSortingFeature, - rowPaginationFeature, - createSortedRowModel, - createPaginatedRowModel, - sortFns, -} from '@tanstack/react-table' -import type { PaginationState, SortingState } from '@tanstack/react-table' - -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, -}) - -function MyTable({ columns, data }) { - const sortingAtom = useCreateAtom([]) - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - - // Fine-grained reads — each component re-renders independently. - const sorting = useSelector(sortingAtom) - const pagination = useSelector(paginationAtom) - - const table = useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom, pagination: paginationAtom }, - // NOTE: no onSortingChange / onPaginationChange — table writes to atoms directly. - }) - - // You own the atom → you own its reset. - const resetMyState = () => { - sortingAtom.set([]) - paginationAtom.set({ pageIndex: 0, pageSize: 10 }) - } -} -``` - -Source: `examples/react/basic-external-atoms/src/main.tsx`. - -### 2. Read a table-owned atom from a sibling component - -You don't have to own a slice to read it surgically — `table.atoms.` works with `useSelector` too. - -```tsx -function SelectedCount({ table }) { - // Re-renders ONLY when rowSelection changes. - const selection = useSelector(table.atoms.rowSelection) - return {Object.keys(selection).length} selected -} -``` - -### 3. Persist a slice to localStorage - -Because you own the atom, you can do anything you want outside the table render path: - -```tsx -const visibilityAtom = useCreateAtom>(() => - JSON.parse(localStorage.getItem('cv') ?? '{}'), -) - -React.useEffect(() => { - return visibilityAtom.subscribe(() => { - localStorage.setItem('cv', JSON.stringify(visibilityAtom.get())) - }) -}, [visibilityAtom]) - -const table = useTable({ - features, - columns, - data, - atoms: { columnVisibility: visibilityAtom }, -}) -``` - -### 4. Share one slice across multiple tables - -Create the atom outside any component (or in a parent and pass down): - -```tsx -import { createAtom } from '@tanstack/store' - -const globalFilterAtom = createAtom('') - -function UsersTable() { - return -} -function OrdersTable() { - return
-} - -function Table({ data, filter }) { - const table = useTable({ - features, - columns, - data, - atoms: { globalFilter: filter }, - }) -} -``` - -## Read surfaces and write paths — cheat sheet - -| Surface | Reactive | Use case | -| ----------------------------------- | --------------------------- | --------------------------------------------------------------- | -| `table.state` | ✓ (via `useTable` selector) | Default top-level reads in the component that called `useTable` | -| `` / `` | ✓ | Surgical re-render boundaries inside the tree | -| `useSelector(table.atoms.X)` | ✓ | Narrowest possible subscription to one slice | -| `table.atoms.X.get()` | ✗ current-value read | Inside event handlers / effects | -| `table.state` | ✗ current-value read | Debugging / one-shot reads | - -| Write path | Owner | Effect | -| ------------------------------- | ----------------- | ---------------------------------------------------------------------------------- | -| Internal `table.baseAtoms.X` | The table | Used when you provide neither `options.atoms.X` nor `options.state.X` | -| `options.atoms.X` (yours) | You | Table writes through; you can `.set()` from anywhere | -| `options.state.X` + `onXChange` | You (React state) | Classic controlled state. Cannot coexist with `options.atoms.X` for the same slice | - -Precedence: `options.atoms[key]` > `options.state[key]` > internal. - -## Common Mistakes - -### CRITICAL Creating the atom with `createAtom(...)` inside the component body - -Wrong: - -```tsx -function MyTable() { - const sortingAtom = createAtom([]) // new atom every render - useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, - }) -} -``` - -Correct: - -```tsx -function MyTable() { - const sortingAtom = useCreateAtom([]) // stable across renders - useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, - }) -} -``` - -A fresh atom every render rebinds the table to a new atom whose state is the initial value — the slice resets on every render. -Source: `examples/react/basic-external-atoms/src/main.tsx`. - -### HIGH Passing the same slice via `state` AND `atoms` - -Wrong: - -```tsx -useTable({ - features, - columns, - data, - state: { sorting: localSorting }, // silently ignored - onSortingChange: setLocalSorting, // silently ignored - atoms: { sorting: sortingAtom }, // wins -}) -``` - -Correct: - -```tsx -// Pick exactly one ownership mechanism per slice. -useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, -}) -``` - -`options.atoms[key]` > `options.state[key]`. Confusing to debug because the `state` plumbing looks live but does nothing. -Source: `docs/framework/react/guide/table-state.md`. - -### HIGH Pairing external atoms with `on*Change` handlers - -Wrong: - -```tsx -useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, - onSortingChange: (next) => sortingAtom.set(next), // redundant + confusing -}) -``` - -Correct: - -```tsx -useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, -}) -``` - -The table writes directly to the atom you provided via the atom's `set()`. Adding an `on*Change` does nothing useful and confuses readers. -Source: `examples/react/basic-external-atoms/src/main.tsx`. - -### HIGH Expecting `table.reset()` to clear externally-owned atoms - -Wrong: - -```tsx - -// External atoms remain at their current values — table can't reset what it doesn't own. -``` - -Correct: - -```tsx - -``` - -The table only resets slices it manages internally. Your atoms are yours to reset. -Source: `docs/framework/react/guide/table-state.md`. - -### MEDIUM Reading via `table.state.sorting` in deeply-nested components - -Wrong: - -```tsx -function SortIndicator({ table }) { - const { sorting } = table.state // re-renders when ANY state slice changes - return {sorting.length} cols -} -``` - -Correct: - -```tsx -import { useSelector } from '@tanstack/react-store' - -function SortIndicator({ table }) { - const sorting = useSelector(table.atoms.sorting) // re-renders only on sorting changes - return {sorting.length} cols -} -``` - -`useSelector(table.atoms.X)` is the narrowest subscription surface — skips constructing a state snapshot. -Source: `docs/framework/react/guide/table-state.md`. - -## See Also - -- `tanstack-table/react/table-state` — the base API, includes `` and `` shapes. -- `tanstack-table/react/compose-with-tanstack-query` — Query queryKey keyed on a Store atom is the canonical pattern. -- `tanstack-table/react/production-readiness` — narrowing selectors and per-slice subscriptions. -- `tanstack-table/react/client-to-server` — atoms make manual-mode wiring trivial. diff --git a/packages/react-table/skills/react/compose-with-tanstack-virtual/SKILL.md b/packages/react-table/skills/react/compose-with-tanstack-virtual/SKILL.md deleted file mode 100644 index 3128cdad6c..0000000000 --- a/packages/react-table/skills/react/compose-with-tanstack-virtual/SKILL.md +++ /dev/null @@ -1,392 +0,0 @@ ---- -name: react/compose-with-tanstack-virtual -description: > - `@tanstack/react-table` v9 does NOT include virtualization — pair with - `@tanstack/react-virtual`. Standard row-virtualization pattern: get the row - array from `table.getRowModel().rows`, feed `rows.length` to - `useVirtualizer({ count, estimateSize, getScrollElement, ... })` in the - DEEPEST possible component (a `TableBody`, NOT `App`), iterate - `rowVirtualizer.getVirtualItems()` instead of `rows.map`, absolute-position - each row with `transform: translateY(virtualRow.start)px`, and render - `` as a CSS grid with a fixed total height. Column virtualization - uses `horizontal: true` plus padding-left/right placeholder cells. An - experimental ref-mutation variant skips React reconciliation for ~10% - extra perf but the standard pattern is the default. -type: composition -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - react/table-state - - row-expanding -sources: - - TanStack/table:docs/guide/virtualization.md - - TanStack/table:examples/react/virtualized-rows/src/main.tsx - - TanStack/table:examples/react/virtualized-columns/src/main.tsx - - TanStack/table:examples/react/virtualized-infinite-scrolling/src/main.tsx - - TanStack/table:examples/react/virtualized-rows-experimental/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — the table's row model is what feeds the virtualizer. - -## Why this skill exists - -TanStack Table renders every row in its `getRowModel().rows` array. For 50 rows that's fine; for 50k or 500k it crashes the browser. `@tanstack/react-virtual` only renders the rows that fit inside the scroll container, recycling DOM nodes as the user scrolls. - -## Setup - -```bash -pnpm add @tanstack/react-table @tanstack/react-virtual -``` - -The two pieces: - -```tsx -import { useTable } from '@tanstack/react-table' -import { useVirtualizer } from '@tanstack/react-virtual' -``` - -## Core Pattern — row virtualization (standard) - -The single most important rule: **keep `useVirtualizer` in the deepest component possible.** Any state change in the component that owns the virtualizer re-runs it, blowing away scroll position and measurement cache. - -```tsx -import * as React from 'react' -import { - useTable, - tableFeatures, - columnSizingFeature, - rowSortingFeature, - createSortedRowModel, - sortFns, - createColumnHelper, -} from '@tanstack/react-table' -import { useVirtualizer } from '@tanstack/react-virtual' -import type { ReactTable, Row } from '@tanstack/react-table' -import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual' - -const features = tableFeatures({ - columnSizingFeature, - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('id', { header: 'ID', size: 60 }), - columnHelper.accessor('firstName', { cell: (info) => info.getValue() }), - columnHelper.accessor('lastName', { - id: 'lastName', - cell: (info) => info.getValue(), - }), -]) - -function App() { - // 1) Scroll container ref + table at App level. - const tableContainerRef = React.useRef(null) - const [data] = React.useState(() => makeData(200_000)) - - const table = useTable({ - features, - columns, - data, - }) - - return ( -
- {/* 2) display: grid — required for absolute positioning + dynamic heights */} -
- - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((h) => ( - - ))} - - ))} - - {/* 3) Virtualizer lives inside TableBody, NOT here. */} - -
-
- -
-
-
- ) -} - -interface TableBodyProps { - table: ReactTable - tableContainerRef: React.RefObject -} - -function TableBody({ table, tableContainerRef }: TableBodyProps) { - const { rows } = table.getRowModel() - - // 4) useVirtualizer in the deepest body component. - const rowVirtualizer = useVirtualizer({ - count: rows.length, - estimateSize: () => 33, - getScrollElement: () => tableContainerRef.current, - // 5) Skip dynamic measurement on Firefox — it measures border height wrong. - measureElement: - typeof window !== 'undefined' && - navigator.userAgent.indexOf('Firefox') === -1 - ? (el) => el.getBoundingClientRect().height - : undefined, - overscan: 5, - }) - - return ( - - {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const row = rows[virtualRow.index] - return ( - rowVirtualizer.measureElement(node)} - style={{ - display: 'flex', - position: 'absolute', - transform: `translateY(${virtualRow.start}px)`, - width: '100%', - }} - > - {row.getAllCells().map((cell) => ( - - - - ))} - - ) - })} - - ) -} -``` - -Source: `examples/react/virtualized-rows/src/main.tsx`. - -## Column virtualization and infinite scroll - -Column virtualization (`horizontal: true` + placeholder padding cells) and infinite scroll via `useInfiniteQuery` + `manualSorting: true` — see [column-virtualization-and-infinite-scroll.md](references/column-virtualization-and-infinite-scroll.md). That file also covers the HIGH-priority `manualSorting` failure mode and the column-virt padding-cells failure mode. - -## Experimental ref-mutation variant - -`examples/react/virtualized-rows-experimental/` and `virtualized-columns-experimental/` mutate row `style` directly via the virtualizer's `onChange` callback, skipping React reconciliation on scroll. Roughly **10% rendering perf gain** in maintainer benchmarks. The pattern is valid but the standard pattern above is the documented default; reach for the experimental version only when measured perf demands it. - -## Common Mistakes - -### CRITICAL `useVirtualizer` in the same component as `useTable` - -Wrong: - -```tsx -function App() { - const rowVirtualizer = useVirtualizer({ - /* … */ - }) // virtualizer too high - const table = useTable(opts) - return -} -``` - -Correct: - -```tsx -function App() { - const tableContainerRef = React.useRef(null) - const table = useTable(opts) - return ( -
- -
- ) -} -function TableBody({ table, tableContainerRef }) { - const rowVirtualizer = useVirtualizer({ - /* … */ - }) // virtualizer deepest - /* … */ -} -``` - -Any state change in the component owning the virtualizer re-runs it — losing scroll position and remeasuring every row. -Source: `examples/react/virtualized-rows/src/main.tsx`. - -### CRITICAL Rendering `rows.map` directly on a large dataset - -Wrong: - -```tsx - - {rows.map((row) => ( - ... - ))} - -// 200k DOM rows — browser crashes. -``` - -Correct: - -```tsx - - {rowVirtualizer.getVirtualItems().map((vr) => { - const row = rows[vr.index] - return ( - - {/* … */} - - ) - })} - -``` - -Use `getVirtualItems()` so only the visible window renders. -Source: `examples/react/virtualized-rows/src/main.tsx`. - -### CRITICAL Missing `display: grid` + absolute positioning - -Wrong: - -```tsx - - {rowVirtualizer.getVirtualItems().map((vr) => ( - {/* no transform, no absolute */} - ))} - -``` - -Correct: - -```tsx - - {rowVirtualizer.getVirtualItems().map((vr) => ( - - {/* … */} - - ))} - -``` - -The semantic `` layout collides with absolute positioning. CSS grid lets the rows position themselves freely while keeping semantic tags. Without `transform: translateY(start)px` all rows render at `top: 0`. -Source: `examples/react/virtualized-rows/src/main.tsx`. - -### HIGH Using `measureElement` on Firefox - -Wrong: - -```tsx -const rowVirtualizer = useVirtualizer({ - count: rows.length, - estimateSize: () => 33, - getScrollElement: () => ref.current, - measureElement: (el) => el.getBoundingClientRect().height, // jitters in Firefox -}) -``` - -Correct: - -```tsx -const rowVirtualizer = useVirtualizer({ - count: rows.length, - estimateSize: () => 33, - getScrollElement: () => ref.current, - measureElement: - typeof window !== 'undefined' && - navigator.userAgent.indexOf('Firefox') === -1 - ? (el) => el.getBoundingClientRect().height - : undefined, -}) -``` - -Firefox returns inconsistent row heights for table rows, causing flicker. Guard the option. -Source: `examples/react/virtualized-rows/src/main.tsx`. - -### HIGH Storing the ref instead of using the callback-ref form - -Wrong: - -```tsx -const rowRef = React.useRef(null) - -// rowVirtualizer can't remeasure when row content changes height -``` - -Correct: - -```tsx - rowVirtualizer.measureElement(node)} /*…*/ /> -``` - -The pattern is a ref callback that calls `measureElement(node)` — passing a stored ref means the virtualizer never gets a chance to remeasure. -Source: `examples/react/virtualized-rows/src/main.tsx`. - -For HIGH-priority failure modes specific to column virtualization (missing padding placeholders) and infinite scroll (`manualSorting` requirement), see [column-virtualization-and-infinite-scroll.md](references/column-virtualization-and-infinite-scroll.md). - -## See Also - -- `tanstack-table/react/production-readiness` — keep virtualizers in deepest components. -- `tanstack-table/react/compose-with-tanstack-query` — `useInfiniteQuery` integration. -- `tanstack-table/react/table-state` — the row model API the virtualizer reads from. - -## References - -- [column-virtualization-and-infinite-scroll.md](references/column-virtualization-and-infinite-scroll.md) — `horizontal: true` column virtualization with placeholder padding cells, `useInfiniteQuery` + `manualSorting: true` integration, plus HIGH-priority failure modes for both diff --git a/packages/react-table/skills/react/compose-with-tanstack-virtual/references/column-virtualization-and-infinite-scroll.md b/packages/react-table/skills/react/compose-with-tanstack-virtual/references/column-virtualization-and-infinite-scroll.md deleted file mode 100644 index 0b4067f384..0000000000 --- a/packages/react-table/skills/react/compose-with-tanstack-virtual/references/column-virtualization-and-infinite-scroll.md +++ /dev/null @@ -1,143 +0,0 @@ -# Column virtualization and infinite scroll — React + TanStack Virtual - -Extended composition patterns extracted from `SKILL.md`. The primary row-virtualization pattern and the experimental ref-mutation variant remain inline in the SKILL; this file covers column virtualization and the `useInfiniteQuery` integration. - -## Column virtualization - -Same shape as row virtualization with `horizontal: true` and left/right placeholder cells so unrendered columns still take up scroll width: - -```tsx -const columnVirtualizer = useVirtualizer({ - count: table.getVisibleLeafColumns().length, - estimateSize: (i) => table.getVisibleLeafColumns()[i].getSize(), - getScrollElement: () => tableContainerRef.current, - horizontal: true, - overscan: 3, -}) - -const virtualColumns = columnVirtualizer.getVirtualItems() -const virtualPaddingLeft = virtualColumns[0]?.start ?? 0 -const virtualPaddingRight = - columnVirtualizer.getTotalSize() - - (virtualColumns[virtualColumns.length - 1]?.end ?? 0) - -// In each row: - - {virtualPaddingLeft > 0 ? - })} - {virtualPaddingRight > 0 ? -``` - -Source: `examples/react/virtualized-columns/src/main.tsx`. - -## Infinite scroll — Virtual + `useInfiniteQuery` - -```tsx -const dataQuery = useInfiniteQuery({ - queryKey: ['people', sorting], - queryFn: ({ pageParam = 0 }) => fetchPage(pageParam, sorting), - getNextPageParam: (lastPage, allPages) => allPages.length, - placeholderData: keepPreviousData, -}) - -const flatRows = React.useMemo( - () => dataQuery.data?.pages.flatMap((p) => p.rows) ?? [], - [dataQuery.data], -) - -const table = useTable({ - features: tableFeatures({ - rowSortingFeature, - // no sortedRowModel — server sorts each page - }), - columns, - data: flatRows, - manualSorting: true, - // ... -}) - -// Inside TableBody, scroll handler: -React.useEffect(() => { - const el = tableContainerRef.current - if (!el) return - const onScroll = () => { - if ( - el.scrollHeight - el.scrollTop - el.clientHeight < 500 && - !dataQuery.isFetching - ) { - dataQuery.fetchNextPage() - } - } - el.addEventListener('scroll', onScroll) - return () => el.removeEventListener('scroll', onScroll) -}, [dataQuery]) -``` - -Source: `examples/react/virtualized-infinite-scrolling/src/main.tsx`. - -## Common Mistakes (column virt + infinite scroll) - -### HIGH For column virtualization: missing padding placeholder cells - -Wrong: - -```tsx - - {virtualColumns.map((vc) => ( - - ))} - -// Unrendered columns aren't taking up scroll space → visible columns slide left. -``` - -Correct: - -```tsx - - {virtualPaddingLeft > 0 ? - ))} - {virtualPaddingRight > 0 ? ( - -``` - -Source: `examples/react/virtualized-columns/src/main.tsx`. - -### HIGH Infinite scroll without `manualSorting` - -Wrong: - -```tsx -const table = useTable({ - features: tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, - }), - data: flatRows, -}) -// Each new page arrives → table re-sorts everything → row order scrambles between pages. -``` - -Correct: - -```tsx -const table = useTable({ - features: tableFeatures({ - rowSortingFeature, - // no sortedRowModel — server sorts each page - }), - data: flatRows, - manualSorting: true, -}) -``` - -With `useInfiniteQuery`, you must fire a fresh query on sort changes (key your `queryKey` on `sorting`) and set `manualSorting: true` so the table doesn't re-sort accumulated pages. -Source: `examples/react/virtualized-infinite-scrolling/src/main.tsx`. diff --git a/packages/react-table/skills/react/getting-started/SKILL.md b/packages/react-table/skills/react/getting-started/SKILL.md deleted file mode 100644 index f00fcd2c5e..0000000000 --- a/packages/react-table/skills/react/getting-started/SKILL.md +++ /dev/null @@ -1,394 +0,0 @@ ---- -name: react/getting-started -description: > - End-to-end first-table journey for `@tanstack/react-table` v9. Install the - React adapter, declare `features` via `tableFeatures()` (row model factories - and *Fns registries live on the features object alongside feature flags), - create a column helper with both `TFeatures` and `TData` generics, instantiate - `useTable`, and render with ``. New users land here, not on - `useLegacyTable`. -type: lifecycle -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - setup - - column-definitions - - state-management - - react/table-state -sources: - - TanStack/table:docs/installation.md - - TanStack/table:docs/framework/react/react-table.md - - TanStack/table:examples/react/basic-use-table/src/main.tsx - - TanStack/table:examples/react/basic-use-app-table/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — `features` (including row model factories) comes from the core state-management concept, and `table-state` covers how reactivity flows in React. - -## Install - -```bash -pnpm add @tanstack/react-table -# or -npm install @tanstack/react-table -``` - -`@tanstack/react-table` v9 requires React 18+ and TypeScript 5.4+ if you use TS. - -## Minimum-viable v9 table - -Three things are non-negotiable, even for the simplest possible table: - -1. `features: tableFeatures({...})` — required even if empty (`tableFeatures({})`). Row model factories and \*Fns registries live on this object alongside feature flags. -2. `createColumnHelper()` — generic order is `` in v9 (changed from v8). -3. The **core** row model is automatic; register sorted/filtered/paginated/grouped/etc. models by adding their factory to `tableFeatures(...)` when you use them. - -```tsx -import * as React from 'react' -import { useTable, tableFeatures } from '@tanstack/react-table' -import type { ColumnDef } from '@tanstack/react-table' - -type Person = { - firstName: string - lastName: string - age: number - visits: number - status: string - progress: number -} - -// 1. features — required option, even if empty. -const features = tableFeatures({}) - -// 2. Columns — defined at module scope for stable identity. -const columns: Array> = [ - { - accessorKey: 'firstName', - header: 'First Name', - cell: (info) => info.getValue(), - }, - { accessorKey: 'lastName', header: 'Last Name' }, - { accessorKey: 'age', header: 'Age' }, - { accessorKey: 'visits', header: 'Visits' }, - { accessorKey: 'status', header: 'Status' }, - { accessorKey: 'progress', header: 'Profile Progress' }, -] - -function App({ initialData }: { initialData: Person[] }) { - const [data] = React.useState(() => initialData) - - // 3. Build the table — features is required; row model factories go on features when used. - const table = useTable( - { - features, - columns, - data, - }, - (state) => state, // default selector - ) - - return ( -
: null} - {virtualColumns.map((vc) => { - const cell = row.getVisibleCells()[vc.index] - return : null} -
...
: null} - {virtualColumns.map((vc) => ( - ... - ) : null} -
- - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((h) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((cell) => ( - - ))} - - ))} - -
- {h.isPlaceholder ? null : } -
- -
- ) -} -``` - -Source: `examples/react/basic-use-table/src/main.tsx`. - -## Adding sorting - -Register the feature and its row model factory on the `features` object, then wire a click handler: - -```tsx -import { - useTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, - createColumnHelper, -} from '@tanstack/react-table' - -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('age', { header: 'Age' }), -]) - -function App({ data }: { data: Person[] }) { - const table = useTable({ - features, - columns, - data, - }) - - return ( - - - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((h) => ( - - ))} - - ))} - - {/* tbody same as above */} -
- - {{ asc: ' 🔼', desc: ' 🔽' }[ - h.column.getIsSorted() as string - ] ?? null} -
- ) -} -``` - -The `sortFns` slot is what makes the built-in sort functions tree-shakeable. Register the equivalent fns slots (`filterFns` for `createFilteredRowModel()`, `aggregationFns` for `createGroupedRowModel()`) when using those features. - -## Layering features - -Adding pagination and filtering is purely additive — register each feature, its factory, and its fns registry on `tableFeatures(...)`, then call the built-in APIs: - -```tsx -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - columnFilteringFeature, - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, - filterFns, -}) - -const table = useTable({ - features, - columns, - data, -}) - -// Built-in APIs you should reach for, NOT reimplement: -table.setSorting([{ id: 'age', desc: true }]) -table.nextPage() -table.setColumnFilters([{ id: 'firstName', value: 'tan' }]) -column.toggleSorting() -row.toggleSelected() -``` - -Source: `docs/framework/react/react-table.md`; `examples/react/basic-use-table/src/main.tsx`. - -## Optional: `createTableHook` for shared config - -If you ship the same `features` / row model factories / cell components across many tables, package them once: - -```tsx -import { createTableHook } from '@tanstack/react-table' - -const { useAppTable, createAppColumnHelper } = createTableHook({ - features: {}, - debugTable: true, -}) - -const columnHelper = createAppColumnHelper() -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { cell: (info) => info.getValue() }), -]) - -function App({ data }) { - const table = useAppTable({ columns, data }) - // ... same FlexRender markup -} -``` - -Source: `examples/react/basic-use-app-table/src/main.tsx`. - -## Common Mistakes - -### CRITICAL Forgetting `features: tableFeatures({})` - -Wrong: - -```tsx -const table = useTable({ - columns, - data, -}) -// TS: Property 'features' is missing in type -``` - -Correct: - -```tsx -const features = tableFeatures({}) -const table = useTable({ features, columns, data }) -``` - -The option is required even for a "no features" table — pass `tableFeatures({})` or `stockFeatures` if you want v8-style "everything on". -Source: `examples/react/basic-use-table/src/main.tsx`. - -### CRITICAL Reimplementing what built-in APIs already provide - -Wrong: - -```tsx -// Reimplements sorting state manually instead of using the API. -const [sorting, setSorting] = useState([]) -const sortedData = useMemo( - () => [...data].sort((a, b) => /* custom */), - [data, sorting], -) -// uses sortedData directly, bypassing the table -``` - -Correct: - -```tsx -const table = useTable({ - features: tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, - }), - columns, - data, -}) -// Then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() -``` - -Maintainer flags this as the #1 tell that "an AI wrote this." The built-ins handle reset semantics, multi-sort, internal invariants. -Source: maintainer interview (Phase 4). - -### CRITICAL API "missing" because the feature was not registered in `features` - -Wrong: - -```tsx -const features = tableFeatures({}) // empty -const table = useTable({ features, columns, data }) -table.setSorting([{ id: 'age', desc: true }]) // TS error — does not exist on this table type -``` - -Correct: - -```tsx -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ - features, - columns, - data, -}) -table.setSorting([{ id: 'age', desc: true }]) // ✓ -``` - -In v9, `features` is a tree-shakeable registry. If a feature isn't listed, TypeScript hides its APIs and the runtime atom is never created — the feature isn't broken, it's just not on. -Source: maintainer interview (Phase 4); `docs/framework/react/react-table.md`. - -### HIGH Wrong generic order on `createColumnHelper` - -Wrong: - -```tsx -const columnHelper = createColumnHelper() // v8 arity -``` - -Correct: - -```tsx -const features = tableFeatures({ - /* … */ -}) -const columnHelper = createColumnHelper() // v9: -``` - -v9 added `TFeatures` as the first generic across `Column`, `Row`, `ColumnDef`, `ColumnMeta`, etc. Use `typeof features` so the same feature set drives types and runtime. -Source: `docs/framework/react/react-table.md`. - -### HIGH Defining `features` / `columns` / `data` inside the render body - -Wrong: - -```tsx -function MyTable({ rows }) { - const features = tableFeatures({ rowSortingFeature }) // new every render - const columns = [/* … */] // new every render - return -} -``` - -Correct: - -```tsx -// Module scope = stable identity. -const features = tableFeatures({ rowSortingFeature }) -const columns: ColumnDef[] = [ - /* … */ -] -``` - -Internal memoization keys off identity. A new object every render forces full recomputation and can cause subtle re-render issues. -Source: `examples/react/basic-use-table/src/main.tsx`; FAQ #1. - -### HIGH Reaching for `useLegacyTable` for a new project - -Wrong: - -```tsx -import { useLegacyTable, getCoreRowModel } from '@tanstack/react-table/legacy' -const table = useLegacyTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), -}) -``` - -Correct: - -```tsx -import { useTable, tableFeatures } from '@tanstack/react-table' -const features = tableFeatures({}) -const table = useTable({ features, columns, data }) -``` - -`useLegacyTable` is a migration shim for incrementally upgrading v8 codebases. It bundles every feature, lacks `table.Subscribe`, and is deprecated in v9 / scheduled for removal in v10. New code uses `useTable`. -Source: `docs/framework/react/guide/use-legacy-table.md`. - -## See Also - -- `tanstack-table/react/table-state` — selectors, ``, external atoms, `createTableHook`. -- `tanstack-table/react/migrate-v8-to-v9` — for codebases upgrading from `useReactTable`. -- `tanstack-table/react/production-readiness` — once it works, optimize for shipping. -- `tanstack-table/react/client-to-server` — when you outgrow client-side row processing. diff --git a/packages/react-table/skills/react/migrate-v8-to-v9/SKILL.md b/packages/react-table/skills/react/migrate-v8-to-v9/SKILL.md deleted file mode 100644 index 02a2c89eff..0000000000 --- a/packages/react-table/skills/react/migrate-v8-to-v9/SKILL.md +++ /dev/null @@ -1,498 +0,0 @@ ---- -name: react/migrate-v8-to-v9 -description: > - Mechanical breaking-change migration from `@tanstack/react-table` v8 to v9. - Every v8-shaped option, type, or method an agent will reproduce from muscle - memory has a v9 equivalent enumerated below: `useReactTable` → `useTable`, - root `get*RowModel` options → row model factories on `tableFeatures({...})` - alongside their *Fns registries, - `createColumnHelper` → `createColumnHelper`, - `table.getState()` → `table.state` / `table.store.state` / `table.atoms.X.get()`, - `sortingFn` → `sortFn`, `enablePinning` → split, `_`-prefixed APIs unprefixed, - `ColumnSizing` split into `columnSizingFeature` + `columnResizingFeature`. - For incremental migration, `useLegacyTable` from `@tanstack/react-table/legacy` - accepts the v8 API on the v9 engine — deprecated, larger bundle, no - `table.Subscribe`. Long-term you migrate every table off it. -type: lifecycle -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - column-definitions -sources: - - TanStack/table:docs/framework/react/guide/migrating.md - - TanStack/table:docs/framework/react/guide/use-legacy-table.md - - TanStack/table:packages/react-table/src/legacy.ts - - TanStack/table:examples/react/basic-use-legacy-table/src/main.tsx - - TanStack/table:examples/react/basic-use-table/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — `state-management` explains the v9 `features`-first design (where row model factories and \*Fns registries live on `tableFeatures()`), and `table-state` shows the new reactivity model. - -## Two migration paths - -| Path | Use when | Cost | -| ---------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| **Direct: `useReactTable` → `useTable`** | New code, small/medium codebase, or you want React Compiler + Subscribe perf | Per-table refactor; no `getState()` reads in render | -| **Bridge: `useLegacyTable`** | Large v8 codebase you can't refactor in one PR | Bigger bundle (ships every feature), no `table.Subscribe`, deprecated — pay back later | - -The bridge is React-only. Angular projects must migrate directly. - -## Setup - -Imports change for v9. The legacy shim lives under `/legacy`. - -```tsx -// v9 (new code) -import { - useTable, - tableFeatures, - rowSortingFeature, - rowPaginationFeature, - columnFilteringFeature, - columnSizingFeature, - columnResizingFeature, - createColumnHelper, - createSortedRowModel, - createFilteredRowModel, - createPaginatedRowModel, - sortFns, - filterFns, -} from '@tanstack/react-table' - -// Legacy shim (migration aid only) -import { - useLegacyTable, - legacyCreateColumnHelper, - getCoreRowModel, - getSortedRowModel, - getFilteredRowModel, - getPaginationRowModel, -} from '@tanstack/react-table/legacy' -``` - -## Direct migration: v8 → v9 line-by-line - -### Hooks and helpers - -```tsx -// v8 -import { - useReactTable, - createColumnHelper, - getCoreRowModel, - getSortedRowModel, -} from '@tanstack/react-table' -const columnHelper = createColumnHelper() -const table = useReactTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), -}) - -// v9 -import { - useTable, - tableFeatures, - rowSortingFeature, - createColumnHelper, - createSortedRowModel, - sortFns, -} from '@tanstack/react-table' - -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const columnHelper = createColumnHelper() -const table = useTable({ - features, - columns, - data, -}) -``` - -### State reads - -```tsx -// v8 -const state = table.getState() -const cells = row._getAllCellsByColumnId() - -// v9 -const all = table.state // flat snapshot -const sorting = table.atoms.sorting.get() // per-slice atom -const cells = row.getAllCellsByColumnId() // no underscore — APIs unprefixed -``` - -In components, prefer `` over `table.state` for reactivity (see `tanstack-table/react/table-state`). - -### Renames - -| v8 | v9 | -| -------------------------------- | ----------------------------------------------------------- | -| `sortingFn` (column def) | `sortFn` | -| `sortingFns` (registry) | `sortFns` | -| `getSortingFn()` | `getSortFn()` | -| `getAutoSortingFn()` | `getAutoSortFn()` | -| `SortingFn` / `SortingFns` types | `SortFn` / `SortFns` | -| `enablePinning: true` | `enableColumnPinning: true` AND/OR `enableRowPinning: true` | -| `columnSizingInfo` state | `columnResizing` | -| `onColumnSizingInfoChange` | `onColumnResizingChange` | -| `table._getFacetedRowModel` etc. | `table.getFacetedRowModel` etc. (underscore dropped) | -| `row._getAllCellsByColumnId()` | `row.getAllCellsByColumnId()` | - -### Column resizing split - -```tsx -// v8 -useReactTable({ - /* ColumnSizing feature handles BOTH widths AND drag */ -}) - -// v9 — explicit -const features = tableFeatures({ - columnSizingFeature, // fixed widths - columnResizingFeature, // drag-to-resize (separate feature) -}) -``` - -### Type generics — `TFeatures` first - -```tsx -// v8 -type MyDef = ColumnDef -declare module '@tanstack/react-table' { - interface ColumnMeta { - customProp: string - } -} - -// v9 -type MyDef = ColumnDef -declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - customProp: string - } -} -``` - -`RowData` was also tightened from `unknown | object | any[]` to `Record | Array`. - -### Mutability - -`data` and `columns` are `readonly` in v9. Any code that mutates the array in place (`data.push(...)`) will fail at the TS layer; flow changes through `setData(prev => [...prev, row])`. - -## Bridge migration: `useLegacyTable` - -When you need to keep one or many tables on the v8 API while you upgrade others, switch the import path: - -```tsx -// Before: v8 import -import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, getFilteredRowModel, createColumnHelper, flexRender } - from '@tanstack/react-table' - -// After: legacy shim, same call shape -import { - useLegacyTable, - legacyCreateColumnHelper, - getCoreRowModel, getSortedRowModel, getPaginationRowModel, getFilteredRowModel, -} from '@tanstack/react-table/legacy' -import { flexRender } from '@tanstack/react-table' - -const columnHelper = legacyCreateColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { cell: (info) => info.getValue() }), - // ... -]) - -function App({ data }) { - const [sorting, setSorting] = React.useState([]) - const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 10 }) - - const table = useLegacyTable({ - columns, - data, - // v8-style root options — mapped to v9 rowModels under the hood - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getFilteredRowModel: getFilteredRowModel(), - state: { sorting, pagination }, - onSortingChange: setSorting, - onPaginationChange: setPagination, - }) - - // Rendering: with useLegacyTable, prefer `flexRender(header.column.columnDef.header, header.getContext())`. - return (/* same JSX shape as v8 */) -} -``` - -Source: `examples/react/basic-use-legacy-table/src/main.tsx`; `packages/react-table/src/legacy.ts`. - -Tradeoffs of the bridge: - -- Bundles every feature (no tree-shaking benefit). -- No `table.Subscribe`, no `table.atoms`, no fine-grained reactivity — subscribes to all state like v8. -- **Deprecated in v9, removed in v10.** Use it to unblock incremental migration; don't ship new features against it. - -## Common Mistakes - -### CRITICAL Keeping `useReactTable` + `get*RowModel` options on v9 - -Wrong: - -```tsx -import { useReactTable, getCoreRowModel } from '@tanstack/react-table' -const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), -}) -``` - -Correct: - -```tsx -import { useTable, tableFeatures } from '@tanstack/react-table' -const features = tableFeatures({}) -const table = useTable({ features, data, columns }) -``` - -`useReactTable` is the v8 entry point and won't have `table.Subscribe` / `table.atoms`. `getCoreRowModel()` as an option was removed — core is automatic; non-core models move into `tableFeatures()` as factory slots alongside their \*Fns registries. -Source: PR #6202; `packages/react-table/src/useTable.ts`. - -### CRITICAL `createSortedRowModel` without registering `sortFns` - -Wrong: - -```tsx -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - // missing sortFns — built-in sort functions not available -}) -``` - -Correct: - -```tsx -import { createSortedRowModel, sortFns } from '@tanstack/react-table' -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, // registers built-in sort functions for tree-shaking -}) -``` - -The `sortFns` slot on `tableFeatures()` is what makes the built-in sort functions tree-shakeable: `filterFns` for `createFilteredRowModel()`, `aggregationFns` for `createGroupedRowModel()`. -Source: `docs/framework/react/guide/migrating.md`. - -### CRITICAL `createColumnHelper()` (v8 arity) - -Wrong: - -```tsx -const columnHelper = createColumnHelper() -``` - -Correct: - -```tsx -const columnHelper = createColumnHelper() -``` - -v9 requires ``. `typeof features` is the standard idiom — declare features once and reuse the type. -Source: `docs/framework/react/guide/migrating.md`. - -### CRITICAL `table.getState()` reads on v9 - -Wrong: - -```tsx -function Toolbar({ table }) { - const { rowSelection } = table.getState() // exists on v8, removed on v9 - return
{Object.keys(rowSelection).length} selected
-} -``` - -Correct: - -```tsx -function Toolbar({ table }) { - return ( - Object.keys(s.rowSelection).length}> - {(count) =>
{count} selected
} -
- ) -} -``` - -`getState` was removed. Use `table.state` for a flat snapshot, `table.state` if you passed a `useTable` selector, or `` for reactive reads. -Source: `docs/framework/react/guide/migrating.md`; `examples/react/basic-subscribe/src/main.tsx`. - -### HIGH `enablePinning: true` on v9 - -Wrong: - -```tsx -useTable({ features, columns, data, enablePinning: true }) -``` - -Correct: - -```tsx -useTable({ - features, - columns, - data, - enableColumnPinning: true, - enableRowPinning: true, -}) -``` - -`enablePinning` was split. Pick one or both depending on what you actually want. -Source: `docs/framework/react/guide/migrating.md`. - -### HIGH `_`-prefixed APIs - -Wrong: - -```tsx -row._getAllCellsByColumnId() -table._getFacetedRowModel() -table._getFacetedMinMaxValues() -``` - -Correct: - -```tsx -row.getAllCellsByColumnId() -table.getFacetedRowModel() -table.getFacetedMinMaxValues() -``` - -All went public — drop the underscore. -Source: `docs/framework/react/guide/migrating.md`. - -### HIGH Module augmentation with v8 generic arity - -Wrong: - -```tsx -declare module '@tanstack/react-table' { - interface ColumnMeta { - customProp: string - } -} -``` - -Correct: - -```tsx -declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - customProp: string - } -} -``` - -v9 added `TFeatures` as the first generic on `ColumnMeta` / `Column` / `Row` / `ColumnDef`. Wrong arity silently widens the augmentation. -Source: `examples/react/basic-use-legacy-table/src/main.tsx` (correct shape). - -### MEDIUM Mutating `data` or `columns` in place - -Wrong: - -```tsx -const data = [] -function addRow(row) { - data.push(row) - rerender() -} -``` - -Correct: - -```tsx -const [data, setData] = React.useState([]) -function addRow(row: Person) { - setData((prev) => [...prev, row]) -} -``` - -PR #6183 made `data` and `columns` `readonly` to force changes through React state. -Source: `docs/framework/react/guide/migrating.md`. - -### MEDIUM Treating `useLegacyTable` as a long-term answer - -Wrong: - -```tsx -// New feature shipped on the legacy shim — locks in the bigger bundle indefinitely. -import { useLegacyTable } from '@tanstack/react-table/legacy' -``` - -Correct: - -```tsx -// New tables: useTable. Reach for the legacy shim only when migrating an existing v8 table piecemeal. -import { useTable, tableFeatures } from '@tanstack/react-table' -``` - -`useLegacyTable` is deprecated in v9 and scheduled for removal in v10. It exists to unblock incremental migration, not to be a permanent API. -Source: `docs/framework/react/guide/use-legacy-table.md`. - -### CRITICAL Hallucinating react-table v7 / `useTable(opts, useSortBy)` shape - -Wrong: - -```tsx -import { useTable, useSortBy } from 'react-table' // v7 package name + plugin hooks -const table = useTable({ columns, data }, useSortBy) -``` - -Correct: - -```tsx -import { - useTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, -} from '@tanstack/react-table' -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ - features, - columns, - data, -}) -``` - -The `react-table` package (v7) was renamed to `@tanstack/react-table` in v8 and reshaped again in v9. Agents trained on pre-v9 data will produce all three shapes — only the v9 shape compiles today. -Source: maintainer interview (Phase 4). - -## See Also - -- `tanstack-table/react/getting-started` — the v9 minimum-viable shape. -- `tanstack-table/react/table-state` — replacing `getState()` with selectors / ``. -- `tanstack-table/react/production-readiness` — tree-shaking with `features` (the whole point of the v9 redesign). -- `tanstack-table/react/react-subscribe-compiler-compat` — fixes the v8-React-Compiler "incompatible library" warning. diff --git a/packages/react-table/skills/react/production-readiness/SKILL.md b/packages/react-table/skills/react/production-readiness/SKILL.md deleted file mode 100644 index dda82b20cb..0000000000 --- a/packages/react-table/skills/react/production-readiness/SKILL.md +++ /dev/null @@ -1,349 +0,0 @@ ---- -name: react/production-readiness -description: > - Ship-ready optimizations for `@tanstack/react-table` v9: tree-shake the - bundle by registering ONLY the `features` you actually use; memoize - `features`, `data`, and `columns` for stable identity; replace - `(state) => state` with narrow selectors or per-slice - `useSelector(table.atoms.)` subscriptions; and push state-driven - re-renders down the tree with `` / `` so the - expensive table body doesn't re-render every time you toggle a sort - indicator. Don't over-optimize small tables — the default selector + - inline rendering is fine until measured perf demands more. -type: lifecycle -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - react/table-state -sources: - - TanStack/table:docs/guide/features.md - - TanStack/table:docs/framework/react/guide/table-state.md - - TanStack/table:examples/react/basic-subscribe/src/main.tsx - - TanStack/table:examples/react/basic-external-atoms/src/main.tsx - - TanStack/table:examples/react/kitchen-sink/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — `features` tree-shaking and the atom reactivity model are the foundation; this skill is about _which_ of the patterns introduced there you actually need in production. - -## When to optimize - -The default `useTable` selector is `(state) => state` — the component re-renders on any state change. That's correct and ergonomic, and for tables with a few hundred rows and basic features it's the right default. Don't reach for `` walls or per-slice atom subscriptions until you've **measured** a problem (slow keystrokes in a filter input, dropped frames during scrolling, long-running renders). On small tables the optimization noise costs more than it saves. - -## Setup — stable references - -The biggest single perf win is keeping `features` and `columns` references stable across renders. Internal memoization keys off identity, so a new object every render forces full recomputation. Row model factories live on `features`, so they are also stable at module scope. - -```tsx -// ✓ Module scope = stable identity -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, -}) -const columnHelper = createColumnHelper() -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('age', { header: 'Age' }), -]) - -// Module-scope empty array for the "no data yet" branch. -const EMPTY: Person[] = [] - -function MyTable({ data }: { data: Person[] | undefined }) { - const table = useTable({ - features, - columns, - data: data ?? EMPTY, - }) -} -``` - -## Core Patterns - -### 1. Tree-shake `features` to only what you use - -Avoid `stockFeatures` in production. A sort-only table is ~6–7kb registered explicitly versus ~15–20kb if you import the whole stock set. - -```tsx -// ✓ Pay only for what you render -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, -}) - -// ✗ Ships filtering, faceting, grouping, pinning, expanding, sizing, -// resizing, visibility, ordering, row-selection, row-pinning… -const features = tableFeatures(stockFeatures) -``` - -Source: `docs/guide/features.md`; maintainer guidance. - -### 2. Narrow the `useTable` selector - -`(state) => state` re-renders the holding component on any state change. If only one component cares about one slice, pass a narrow selector — or pass `() => null` and rely on `` walls inside. - -```tsx -// Narrow to specific slices at the table level. -const table = useTable({ features, columns, data }, (state) => ({ - sorting: state.sorting, - pagination: state.pagination, -})) - -// Or: opt out completely at the top, subscribe surgically inside. -const table = useTable(opts, () => null) -``` - -Source: `examples/react/basic-subscribe/src/main.tsx` (uses `() => null`). - -### 3. Push re-renders down with `` - -A noisy footer that re-renders on every keystroke in a filter doesn't need to re-render the whole ``. Wrap each consumer in `` with its own selector. - -```tsx -function MyTable({ data, columns }) { - const table = useTable( - { features, columns, data }, - () => null, // top-level opt-out - ) - return ( - <> - {/* heavy — keep stable */} - {/* Footer re-renders only on pagination changes */} - s.pagination}> - {(pagination) => } - - - ) -} -``` - -Source: `examples/react/basic-subscribe/src/main.tsx`. - -### 4. Per-slice `useSelector(table.atoms.)` for narrowest scope - -Even narrower than ``: subscribe a leaf component to a single atom. Skips constructing a state snapshot entirely. - -```tsx -import { useSelector } from '@tanstack/react-store' - -function SelectedCount({ table }) { - // Re-renders ONLY when rowSelection changes — not sorting / pagination / etc. - const selection = useSelector(table.atoms.rowSelection) - return {Object.keys(selection).length} selected -} -``` - -Source: `examples/react/basic-external-atoms/src/main.tsx`. - -### 5. React Compiler — read state via `` in nested components - -The compiler can't see through the `table` closure, so reads via builder APIs (`column.getIsPinned()`, `row.getIsSelected()`) in memoized child components go stale. Wrap them in `` (see `tanstack-table/react/react-subscribe-compiler-compat`). - -### 6. Virtualization in the deepest possible component - -Keep `useVirtualizer` in the deepest component (`TableBody`, not `App`). Any state change in the holder of the virtualizer re-runs it and tanks scroll perf. See `tanstack-table/react/compose-with-tanstack-virtual`. - -## Common Mistakes - -### HIGH Using `stockFeatures` in production - -Wrong: - -```tsx -import { useTable, stockFeatures, tableFeatures } from '@tanstack/react-table' -const features = tableFeatures(stockFeatures) // ships every feature -``` - -Correct: - -```tsx -import { - useTable, - tableFeatures, - rowSortingFeature, - rowPaginationFeature, -} from '@tanstack/react-table' -const features = tableFeatures({ rowSortingFeature, rowPaginationFeature }) -``` - -Tree-shaking via `features` is one of the headline reasons for the v9 rewrite. `stockFeatures` exists for migration / "everything on" smoke tests, not production. -Source: maintainer guidance; `docs/guide/features.md`. - -### HIGH Unstable `features` / `columns` references - -Wrong: - -```tsx -function MyTable({ data }) { - const features = tableFeatures({ - // new every render - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, - }) - const table = useTable({ features, columns, data }) -} -``` - -Correct: - -```tsx -// Module scope — declared once. -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -function MyTable({ data }) { - const table = useTable({ features, columns, data }) -} -``` - -Internal memoization keys off identity. A new object every render busts memos and forces full recomputation. -Source: FAQ #1; `examples/react/basic-use-table/src/main.tsx`. - -### HIGH `data={rows ?? []}` in JSX - -Wrong: - -```tsx - -``` - -Correct: - -```tsx -const EMPTY: Person[] = [] // module scope - - -// or memoize the fallback: -const data = React.useMemo(() => query.data?.rows ?? [], [query.data]) -``` - -The `?? []` produces a new array identity each render, busting internal memos that depend on `data` reference. -Source: `examples/react/with-tanstack-query/src/main.tsx`. - -### MEDIUM Leaving `(state) => state` when only one component cares - -Wrong: - -```tsx -// Default selector — whole tree re-renders on every state change. -const table = useTable(opts) -return -``` - -Correct: - -```tsx -const table = useTable(opts, () => null) -return ( - <> - - s.pagination}> - {(p) => } - - -) -``` - -Once you've measured a problem, narrow the top selector and add `` walls around the components that actually need state. -Source: `examples/react/basic-subscribe/src/main.tsx`. - -### MEDIUM Subscribing to the whole `table.store` when a single atom would do - -Wrong: - -```tsx - s.rowSelection}> - {(rs) => {Object.keys(rs).length} selected} - -``` - -Correct: - -```tsx -import { useSelector } from '@tanstack/react-store' - -function SelectedCount({ table }) { - const selection = useSelector(table.atoms.rowSelection) - return {Object.keys(selection).length} selected -} -``` - -`` still selects from `table.state` (the full state). For a single slice, `useSelector(table.atoms.X)` skips even constructing the snapshot. -Source: `docs/framework/react/guide/table-state.md`. - -### MEDIUM Hoisting heavy table state reads above virtualizers - -Wrong: - -```tsx -function App() { - const rowVirtualizer = useVirtualizer({ - /* … */ - }) // virtualizer too high - const table = useTable(opts) - return -} -``` - -Correct: - -```tsx -function App() { - const tableContainerRef = React.useRef(null) - const table = useTable(opts) - return ( -
- -
- ) -} -function TableBody({ table, tableContainerRef }) { - const rowVirtualizer = useVirtualizer({ - /* … */ - }) // virtualizer at the bottom - /* … */ -} -``` - -The virtualizer in the deepest component avoids re-running on unrelated state changes. -Source: `examples/react/virtualized-rows/src/main.tsx`. - -### MEDIUM Premature `` / narrow selectors on small tables - -Wrong: - -```tsx -// 50-row table with Subscribe around every cell. -header: ({ table }) => ( - s.sorting}> - {() => } - -) -``` - -Correct: - -```tsx -const table = useTable({ features, columns, data }) -// Reach for Subscribe later, scoped to actual hotspots. -``` - -Advanced state-management patterns are for advanced cases. On small tables the boundary churn costs more than it saves. -Source: maintainer guidance (Phase 4). - -## See Also - -- `tanstack-table/react/table-state` — the API surface this skill optimizes against. -- `tanstack-table/react/react-subscribe-compiler-compat` — required reading if React Compiler is on. -- `tanstack-table/react/compose-with-tanstack-store` — fine-grained subscriptions via external atoms. -- `tanstack-table/react/compose-with-tanstack-virtual` — row/column virtualization patterns. -- `tanstack-table/react/compose-with-tanstack-devtools` — `/production` import for live devtools in prod. diff --git a/packages/react-table/skills/react/react-subscribe-compiler-compat/SKILL.md b/packages/react-table/skills/react/react-subscribe-compiler-compat/SKILL.md deleted file mode 100644 index 727a80d9c5..0000000000 --- a/packages/react-table/skills/react/react-subscribe-compiler-compat/SKILL.md +++ /dev/null @@ -1,269 +0,0 @@ ---- -name: react/react-subscribe-compiler-compat -description: > - React Compiler compatibility for `@tanstack/react-table` v9. When you read - table state via builder APIs (`column.getIsPinned()`, `row.getIsSelected()`, - `cell.getIsAggregated()`, `header.column.getIsSorted()`) inside a nested - custom component, React Compiler memoizes the child against the stable - `column` / `row` / `cell` reference and never re-runs when the underlying - atom changes. Symptom: stale checkboxes, frozen sort indicators, dead pin - buttons. Fix: wrap the JSX in `` - or `` so the dependency is visible to the - compiler. Routing keywords: Subscribe, table.Subscribe, React Compiler, - stale checkbox, memoized header/cell, builder API. -type: framework -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - react/table-state -sources: - - TanStack/table:docs/framework/react/guide/table-state.md - - TanStack/table:packages/react-table/src/Subscribe.ts - - TanStack/table:examples/react/basic-subscribe/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — the atom model is what makes builder reads invisible to React Compiler, and `table-state` covers the basics of `` / ``. This skill is laser-focused on the **one** React-specific failure mode that comes up when React Compiler is enabled. - -## Why this exists - -Under React Compiler, JSX is memoized against the props your component receives. A custom `DraggableHeader({ header })` receives a stable `header` reference; the compiler hashes the JSX it produces against that reference. When you call `header.column.getIsPinned()` inside that component, the compiler **cannot see the atom read** hidden behind the method — it returns the cached JSX, and the UI goes stale. - -The fix is to make the dependency visible: read the slice via `` or ``. The compiler sees the selector function, picks up the dependency edge, and re-runs the children whenever the subscribed slice changes. - -## Setup - -You only need `` from `@tanstack/react-table`. It's the same component shown in `table-state`, applied specifically around builder-pattern reads in custom nested components. - -```tsx -import { Subscribe } from '@tanstack/react-table' -``` - -## Core Pattern: wrap nested builder reads in `` - -Whenever a child component reads state via a builder method (`getIs*`, `getCan*`, etc.) inside JSX that the compiler memoizes, wrap it in `` keyed on the relevant slice. - -### Pin / sort indicator on a custom header component - -```tsx -import { Subscribe } from '@tanstack/react-table' - -function DraggableHeader({ header, table }) { - return ( - ({ columnPinning: s.columnPinning, sorting: s.sorting })} - > - {() => { - // Reads run inside the Subscribe child — re-evaluated on selected slice changes. - const isPinned = header.column.getIsPinned() - const sortDir = header.column.getIsSorted() - return ( -
- ) - }} - - ) -} -``` - -Source: `docs/framework/react/guide/table-state.md` (Subscribe for React Compiler Compatibility); `packages/react-table/src/Subscribe.ts`. - -### Row-selection checkbox inside a cell — narrowest subscription - -```tsx -columnHelper.display({ - id: 'select', - cell: ({ row, table }) => ( - // Subscribe to the rowSelection ATOM (not table.store) and project to ONE row. - // Re-renders ONLY when this row's selection flips. - rowSelection[row.id]} - > - {(isSelected) => ( - - )} - - ), -}) -``` - -Source: `examples/react/basic-subscribe/src/main.tsx` (this exact pattern). - -### Component-level vs cell-level — which API - -| Context | `table` is | API | -| ------------------------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------------- | -| Component receiving `table` from `useTable` | `ReactTable<…>` | `` works | -| Inside `cell: ({ table }) => …` / `header: ({ table }) => …` | core `Table` | `table.Subscribe` is **undefined**. Use the standalone `` import. | - -## Common Mistakes - -### CRITICAL Builder read in a nested component without `` - -Wrong: - -```tsx -function DraggableHeader({ header }) { - const isPinned = header.column.getIsPinned() // hidden atom read - return -} -``` - -Correct: - -```tsx -import { Subscribe } from '@tanstack/react-table' - -function DraggableHeader({ header, table }) { - return ( - s.columnPinning}> - {() => { - const isPinned = header.column.getIsPinned() - return - }} - - ) -} -``` - -React Compiler memoizes the child's JSX against the stable `header` reference. The state-dependent builder method hides its atom dependency, so the memoized JSX never re-runs. -Source: `docs/framework/react/guide/table-state.md`; `examples/react/basic-subscribe/src/main.tsx`; `packages/react-table/src/Subscribe.ts`. - -### HIGH Using `table.Subscribe` from inside a cell or header definition - -Wrong: - -```tsx -cell: ({ row, table }) => ( - s[row.id]} - > - {(isSelected) => } - -) -``` - -Correct: - -```tsx -import { Subscribe } from '@tanstack/react-table' - -cell: ({ row, table }) => ( - s[row.id]}> - {(isSelected) => ( - - )} - -) -``` - -Cell and header render contexts type `table` as `Table`, not `ReactTable` — `table.Subscribe` is undefined. Import the standalone ``. -Source: `docs/framework/react/guide/table-state.md` (Tips); `packages/react-table/src/Subscribe.ts`. - -### MEDIUM Wrapping every cell in `` by default - -Wrong: - -```tsx -// Inline cell that already re-runs on every parent render — wrap is unnecessary. -{ - row.getVisibleCells().map((cell) => ( - s.rowSelection}> - {() => ( - - )} - - )) -} -``` - -Correct: - -```tsx -{ - row.getVisibleCells().map((cell) => ( - - )) -} -``` - -`` is overhead. For inline JSX in the parent component the compiler always re-evaluates on parent re-render, so wrapping adds subscription churn without correctness benefit. Reach for `` only at custom-component boundaries that the compiler memoizes. -Source: `docs/framework/react/guide/table-state.md`. - -### MEDIUM Subscribing to the whole `table.store` for one row's checkbox - -Wrong: - -```tsx - s.rowSelection[row.id]}> - {(isSelected) => } - -``` - -Correct: - -```tsx - s[row.id]}> - {(isSelected) => ( - - )} - -``` - -Every change to `table.store` re-runs the Subscribe child. Subscribing to `table.atoms.rowSelection` (a single slice atom) with a per-row projection limits work to actual selection changes for that row. -Source: `examples/react/basic-subscribe/src/main.tsx`; `docs/framework/react/guide/table-state.md` (Tips). - -### CRITICAL Reading raw state with `table.getState()` on v9 instead of `` - -Wrong: - -```tsx -function Toolbar({ table }) { - // v8 muscle memory — does NOT subscribe in v9. - const { rowSelection } = table.getState() - return
{Object.keys(rowSelection).length} selected
-} -``` - -Correct: - -```tsx -function Toolbar({ table }) { - return ( - Object.keys(s.rowSelection).length}> - {(count) =>
{count} selected
} -
- ) -} -``` - -`table.getState()` is a current-value read in v9; it does not subscribe the component. The default `useTable` selector subscribes the parent, but deeply-nested children should opt in explicitly. -Source: PR #6246; `packages/react-table/src/useTable.ts` JSDoc. - -## See Also - -- `tanstack-table/react/table-state` — base `` / `` API and external atoms. -- `tanstack-table/react/production-readiness` — selector narrowing and per-slice `useSelector(table.atoms.X)`. -- `tanstack-table/react/migrate-v8-to-v9` — replacing `useReactTable` with `useTable` to fix the React Compiler "incompatible library" warning. diff --git a/packages/react-table/skills/react/table-state/SKILL.md b/packages/react-table/skills/react/table-state/SKILL.md deleted file mode 100644 index 00cd5fe372..0000000000 --- a/packages/react-table/skills/react/table-state/SKILL.md +++ /dev/null @@ -1,425 +0,0 @@ ---- -name: react/table-state -description: > - Wiring reactivity for `@tanstack/react-table` v9. Covers `useTable` (and its - second-argument selector), reading state via `table.state` / `table.store` / - `table.atoms.`, rendering with `table.FlexRender`, opting subtrees into - fine-grained reactivity with `` and the standalone - ``, owning slices with external atoms via `useCreateAtom` + - `options.atoms`, and packaging shared config into a reusable hook with - `createTableHook` (`useAppTable`, `createAppColumnHelper`, `table.AppTable` / - `table.AppHeader` / `table.AppCell` / `table.AppFooter`). Routing keywords: - useTable, useSelector, useCreateAtom, atoms, react-store, table.Subscribe, - FlexRender. -type: framework -library: tanstack-table -framework: react -library_version: '9.0.0-alpha.48' -requires: - - state-management - - setup -sources: - - TanStack/table:docs/framework/react/guide/table-state.md - - TanStack/table:packages/react-table/src/useTable.ts - - TanStack/table:packages/react-table/src/Subscribe.ts - - TanStack/table:packages/react-table/src/FlexRender.tsx - - TanStack/table:packages/react-table/src/createTableHook.tsx - - TanStack/table:examples/react/basic-use-table/src/main.tsx - - TanStack/table:examples/react/basic-subscribe/src/main.tsx - - TanStack/table:examples/react/basic-external-atoms/src/main.tsx - - TanStack/table:examples/react/basic-external-state/src/main.tsx - - TanStack/table:examples/react/basic-use-app-table/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/setup`. Read those first — `state-management` explains the v9 atom model (per-slice readonly `table.atoms`, internal writable `table.baseAtoms`, flat `table.store`), and this skill shows how each surface is consumed in React. - -## Setup - -Every React v9 table follows the same shape. Define `features` and `columns` at module scope so their references are stable, then call `useTable` and render with ``. - -```tsx -import { - useTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, - createColumnHelper, -} from '@tanstack/react-table' -import type { ColumnDef } from '@tanstack/react-table' - -type Person = { firstName: string; lastName: string; age: number } - -// Module-scope = stable identity. Critical for re-render perf. -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('lastName', { header: 'Last' }), - columnHelper.accessor('age', { header: 'Age' }), -]) - -function PeopleTable({ data }: { data: Person[] }) { - const table = useTable( - { - features, - columns, - data, - }, - (state) => state, // default selector — matches v8 ergonomics - ) - - return ( -
- {header.column.id} - {sortDir === 'asc' ? ' 🔼' : sortDir === 'desc' ? ' 🔽' : null} - {header.column.id}{header.column.id}{flexRender(cell.column.columnDef.cell, cell.getContext())} - -
- - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((h) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((cell) => ( - - ))} - - ))} - -
- {h.isPlaceholder ? null : } -
- -
- ) -} -``` - -Source: `examples/react/basic-use-table/src/main.tsx`. - -## Core Patterns - -### 1. `useTable` selector (second argument) - -The default is `(state) => state` — the component re-renders on any state change. Pass a narrower selector once you have a measurable perf problem, or pass `() => null` to opt out at the top level and use `` walls instead. - -```tsx -// Narrow selector — only re-render this component on sorting/pagination changes. -const table = useTable({ features, columns, data }, (state) => ({ - sorting: state.sorting, - pagination: state.pagination, -})) - -// Or: subscribe to nothing at the top level; do all reads inside . -const table = useTable(opts, () => null) -``` - -Source: `docs/framework/react/guide/table-state.md`; `examples/react/basic-subscribe/src/main.tsx` (uses `() => null`). - -### 2. `` and standalone `` - -Use `` at the component level. Inside cell/header render contexts, `table` is the core `Table` (not `ReactTable`), so `table.Subscribe` is **not on the object** — import the standalone `` and pass `source={table.store}` or `source={table.atoms.X}`. - -```tsx -import { Subscribe } from '@tanstack/react-table' - -// Component-level: table.Subscribe with a state selector. - s.pagination}> - {(pagination) => Page {pagination.pageIndex + 1}} - - -// Subscribe to a single atom (narrower than table.store). - - {(rowSelection) => {Object.keys(rowSelection).length} selected} - - -// Inside a cell — table here is the CORE Table, no .Subscribe. Use the import. -columnHelper.display({ - id: 'select', - cell: ({ row, table }) => ( - s[row.id]} - > - {(isSelected) => ( - - )} - - ), -}) -``` - -Source: `packages/react-table/src/Subscribe.ts`; `examples/react/basic-subscribe/src/main.tsx`. - -### 3. External atoms with `useCreateAtom` + `options.atoms` - -Move ownership of any slice to an atom you create with `useCreateAtom` (from `@tanstack/react-store`). Pass it via `options.atoms.`. The table writes to your atom when you call `table.setSorting(...)`, `table.setPageIndex(...)`, etc. — **no `on*Change` handler is needed**. - -Precedence: `options.atoms[key]` > `options.state[key]` > internal `baseAtoms[key]`. Don't pass both `state.foo` and `atoms.foo` for the same slice; `atoms` wins silently. - -```tsx -import { useCreateAtom, useSelector } from '@tanstack/react-store' -import type { PaginationState, SortingState } from '@tanstack/react-table' - -function MyTable({ columns, data }) { - const sortingAtom = useCreateAtom([]) - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - - // Independent fine-grained subscriptions. - const sorting = useSelector(sortingAtom) - const pagination = useSelector(paginationAtom) - - const table = useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom, pagination: paginationAtom }, - // NOTE: no onSortingChange / onPaginationChange — table writes directly to atoms. - }) -} -``` - -Source: `examples/react/basic-external-atoms/src/main.tsx`. - -### 4. `createTableHook` for reusable shared config - -When you ship the same `features` / row model factories / cell components across many tables, package them with `createTableHook`. You get `useAppTable`, `createAppColumnHelper`, and `table.AppTable` / `AppHeader` / `AppCell` / `AppFooter` boundaries. - -```tsx -import { createTableHook } from '@tanstack/react-table' - -const { useAppTable, createAppColumnHelper } = createTableHook({ - features: {}, - debugTable: true, -}) - -const columnHelper = createAppColumnHelper() -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { cell: (info) => info.getValue() }), - // … -]) - -function App() { - const [data] = React.useState(() => [...defaultData]) - const table = useAppTable({ columns, data }, (state) => state) - return ( - - {/* table.FlexRender header={h} */} - {/* table.FlexRender cell={c} */} -
- ) -} -``` - -Source: `examples/react/basic-use-app-table/src/main.tsx`; `packages/react-table/src/createTableHook.tsx`. - -## Common Mistakes - -### CRITICAL Reading `table.atoms.X.get()` during render and expecting re-renders - -Wrong: - -```tsx -function Pager({ table }) { - const pagination = table.atoms.pagination.get() // current-value read, NOT a subscription - return Page {pagination.pageIndex + 1} -} -``` - -Correct: - -```tsx -function Pager({ table }) { - return ( - p.pageIndex} - > - {(pageIndex) => Page {pageIndex + 1}} - - ) -} -``` - -`.get()` and `table.state` are current-value reads, not subscriptions. The component never re-renders when the atom changes. -Source: `docs/framework/react/guide/table-state.md`; `examples/react/basic-subscribe/src/main.tsx`. - -### HIGH Passing both `atoms.X` and `state.X` for the same slice - -Wrong: - -```tsx -const table = useTable({ - features, - columns, - data, - atoms: { pagination: paginationAtom }, - state: { pagination }, // silently ignored - onPaginationChange: setPagination, // silently ignored -}) -``` - -Correct: - -```tsx -// Pick exactly one source of truth per slice. -const table = useTable({ - features, - columns, - data, - atoms: { pagination: paginationAtom }, -}) -``` - -Precedence is `options.atoms[key]` > `options.state[key]` > internal — `state` is dropped without a warning. -Source: `docs/framework/react/guide/table-state.md`; `examples/react/basic-external-atoms/src/main.tsx`. - -### HIGH Using `table.Subscribe` inside a column cell or header render - -Wrong: - -```tsx -cell: ({ row, table }) => ( - s[row.id]} - > - {(isSelected) => } - -) -``` - -Correct: - -```tsx -import { Subscribe } from '@tanstack/react-table' - -cell: ({ row, table }) => ( - s[row.id]}> - {(isSelected) => ( - - )} - -) -``` - -In cell and header render contexts, `table` is the core `Table`, not `ReactTable` — `table.Subscribe` is undefined. Use the standalone import. -Source: `docs/framework/react/guide/table-state.md` (Tips); `packages/react-table/src/Subscribe.ts`. - -### CRITICAL Creating an atom inside the render body without `useCreateAtom` - -Wrong: - -```tsx -function MyTable() { - const sortingAtom = createAtom([]) // new atom every render - useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, - }) -} -``` - -Correct: - -```tsx -function MyTable() { - const sortingAtom = useCreateAtom([]) // stable across renders - useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, - }) -} -``` - -A fresh atom each render unbinds the table from the slice and resets the state to the initial value on every render. -Source: `examples/react/basic-external-atoms/src/main.tsx`. - -### HIGH Unstable `data` / `columns` / `features` references - -Wrong: - -```tsx -function MyTable({ rows }) { - const features = tableFeatures({ rowSortingFeature }) // new every render - const columns = [ - /* … */ - ] // new every render - const table = useTable({ - features, - columns, - data: rows ?? [], - }) -} -``` - -Correct: - -```tsx -// Module scope — declared once. -const features = tableFeatures({ rowSortingFeature }) -const columns: ColumnDef[] = [ - /* … */ -] - -function MyTable({ rows }) { - const data = rows ?? EMPTY // EMPTY at module scope - const table = useTable({ features, columns, data }) -} -``` - -Internal memoization keys off identity. A new reference each render busts memos and forces full recomputation. -Source: `docs/framework/react/guide/table-state.md` (FAQ #1). - -### MEDIUM Premature `Subscribe` / custom selector on small tables - -Wrong: - -```tsx -// 50-row table with Subscribe wrapped around every cell. -header: ({ table }) => ( - s.sorting}> - {() => } - -) -``` - -Correct: - -```tsx -// Default selector + inline rendering. Reach for Subscribe later, scoped to actual hotspots. -const table = useTable({ features, columns, data }) -``` - -Subscribe and narrow selectors are for large / expensive tables where full re-renders measurably hurt. On a small table they only add complexity. -Source: maintainer guidance (Phase 4). - -## See Also - -- `tanstack-table/react/react-subscribe-compiler-compat` — when builder reads in nested components break under React Compiler memoization. -- `tanstack-table/react/getting-started` — first-table walkthrough. -- `tanstack-table/react/production-readiness` — narrowing selectors, tree-shaking, reference stability. -- `tanstack-table/react/compose-with-tanstack-store` — sharing slice atoms across components, persistence. diff --git a/packages/react-table/skills/table-state/SKILL.md b/packages/react-table/skills/table-state/SKILL.md new file mode 100644 index 0000000000..f03b051397 --- /dev/null +++ b/packages/react-table/skills/table-state/SKILL.md @@ -0,0 +1,255 @@ +--- +name: table-state +description: > + Read, select, subscribe to, and control React Table v9 state with useTable selectors, table.state, table.Subscribe, table.atoms, table.store, and external TanStack Store atoms. Load for controlled state, render performance, or React Compiler builder-method subscription problems. +metadata: + type: framework + library: '@tanstack/react-table' + library_version: '9.0.0-beta.38' + framework: react +requires: + - '@tanstack/table-core#core' + - getting-started +sources: + - 'TanStack/table:docs/framework/react/guide/table-state.md' + - 'TanStack/table:examples/react/basic-subscribe' + - 'TanStack/table:packages/react-table/src/Subscribe.ts' + - 'TanStack/table:packages/react-table/src/useTable.ts' +--- + +This skill builds on `@tanstack/table-core#core` and `getting-started`. Read them first for table construction and feature-owned state. + +## State Mental Model + +TanStack Table is primarily a state coordinator. Keep state internal unless another subsystem needs to read, persist, validate, or drive it. With no `initialState`, `atoms`, `state`, or `on[State]Change` options, the table owns all registered slices. + +- `table.baseAtoms` are the internal writable atoms initialized from resolved initial state. +- `table.atoms` are readonly derived atoms for the active owner of each registered slice. +- `table.store` combines those atoms into one readonly flat store. +- `table.state` is only the value selected by the second `useTable` argument. + +State is feature-based. Registering `rowPaginationFeature` creates pagination state and APIs; without it, `pagination` must not exist in `initialState`, `state`, `atoms`, `table.atoms`, `table.store`, or `table.state`. Treat a missing state API as a likely missing feature import, not a typing problem. + +Keep `features`, `data`, and `columns` stable. State subscriptions do not compensate for new model-input references on every render. + +## Setup + +```tsx +import { + rowSelectionFeature, + tableFeatures, + useTable, +} from '@tanstack/react-table' + +const features = tableFeatures({ rowSelectionFeature }) + +export function SelectionCount({ + data, + columns, +}: { + data: Array<{ id: string }> + columns: any[] +}) { + const table = useTable({ features, data, columns }, (state) => ({ + rowSelection: state.rowSelection, + })) + return {Object.keys(table.state.rowSelection).length} +} +``` + +The optional selector controls which state changes rerender the component and which selected fields appear on `table.state`. Omitting it selects all registered slices. + +## Core Patterns + +### Subscribe at the expensive boundary + +```tsx +function SelectedRows({ + table, +}: { + table: ReturnType> +}) { + return ( + state.rowSelection}> + {(rowSelection) => {Object.keys(rowSelection).length}} + + ) +} +``` + +At a top-level component holding the adapter's table instance, `table.Subscribe` selects from `table.store`. Use this after measuring or when React Compiler cannot see state reads hidden behind table builder methods. + +### Control a slice with an external atom + +```tsx +import { useCreateAtom } from '@tanstack/react-store' + +const selection = useCreateAtom>({}) +const table = useTable({ + features, + columns, + data, + atoms: { rowSelection: selection }, +}) +``` + +An external atom is both ownership and subscription source; it avoids value-or-updater glue. + +### Control a slice with React state + +```tsx +const [rowSelection, setRowSelection] = useState>({}) +const table = useTable({ + features, + columns, + data, + state: { rowSelection }, + onRowSelectionChange: setRowSelection, +}) +``` + +## Choose State Ownership + +Choose exactly one owner for each slice: + +1. Use internal state by default and call feature APIs such as `table.setSorting`, `table.nextPage`, `column.toggleVisibility`, or `row.toggleSelected`. +2. Use `initialState.` only to set the starting and reset value. Changing `initialState` later does not reset the table. +3. Prefer a stable external atom in `atoms.` when Table, Query, routing, or another component must share the slice. Table APIs write that atom directly; do not also add `on[State]Change`. +4. Use `state.` plus its matching `on[State]Change` for simple React-controlled state or v8-style integrations. Always resolve both raw values and updater functions. + +External atoms take precedence over external `state`; external `state` synchronizes into the internal base atom. Do not declare the same slice in multiple ownership options and rely on precedence as application logic. The global v8 `onStateChange` callback is gone in v9; control slices individually or subscribe to `table.store` to observe all state. + +## Initialize, Update, and Reset + +Prefer feature methods over direct atom writes because feature methods preserve related behavior. `table.baseAtoms.pagination.set(...)` is a low-level escape hatch only for internally owned state; write the supplied external atom when `atoms.pagination` owns the slice. + +Feature reset methods reset to `table.initialState` by default: + +```tsx +table.resetSorting() +table.resetPagination() +table.resetPagination(true) // feature blank/default state +``` + +Slice reset methods flow through that feature's updater and can update an external owner. Core `table.reset()` resets internal base atoms, so it is not the primary reset mechanism for externally owned atoms. + +Use feature-specific types for owned slices and infer the full state from the feature set: + +```tsx +import type { PaginationState, TableState } from '@tanstack/react-table' + +type AppTableState = TableState +const initialPagination: PaginationState = { pageIndex: 0, pageSize: 20 } +``` + +## Common Mistakes + +### HIGH Treating a snapshot as subscription + +Wrong: + +```tsx +const count = Object.keys(table.atoms.rowSelection.get()).length +``` + +Correct: + +```tsx +const count = Object.keys(table.state.rowSelection).length +``` + +`atoms.*.get()` and `table.store.state` return current values but do not subscribe a React render. + +Source: `packages/react-table/src/useTable.ts` + +### HIGH Supplying only the change callback + +Wrong: + +```tsx +const table = useTable({ + features, + columns, + data, + onRowSelectionChange: setRowSelection, +}) +``` + +Correct: + +```tsx +const table = useTable({ + features, + columns, + data, + state: { rowSelection }, + onRowSelectionChange: setRowSelection, +}) +``` + +Once a callback takes ownership, the corresponding controlled value must be written back. + +Source: `docs/framework/react/guide/table-state.md` + +### HIGH Hiding builder reads from React Compiler + +Wrong: + +```tsx +const SelectionCell = memo(({ row }) => ( + +)) +``` + +Correct: + +```tsx +import { Subscribe } from '@tanstack/react-table' + +const SelectionCell = memo(({ row }) => ( + selection[row.id]} + > + {(selected) => ( + + )} + +)) +``` + +`useTable` already returns a fresh table reference on state changes. The remaining hazard is a nested component receiving a stable row, cell, column, or header and hiding a state read behind its methods. Inside cell and header render contexts, `table` is typed as core `Table`, so import standalone `Subscribe`; use `source={table.store}` with a selector for multiple slices, or a specific atom for the narrowest boundary. + +Source: `docs/framework/react/guide/table-state.md` + +### MEDIUM Optimizing every cell preemptively + +Wrong: + +```tsx + + {() => } + +``` + +Correct: + +```tsx + +``` + +Default `useTable` state selection is the simpler starting point; introduce fine-grained boundaries where measurement or compiler behavior justifies them. + +Source: `docs/framework/react/guide/table-state.md` + +## API Discovery + +Inspect `node_modules/@tanstack/react-table/src/useTable.ts` and `Subscribe.tsx`. Core atom precedence and state slices live under `node_modules/@tanstack/table-core/src/`. diff --git a/packages/react-table/skills/with-tanstack-query/SKILL.md b/packages/react-table/skills/with-tanstack-query/SKILL.md new file mode 100644 index 0000000000..8d62b45dc6 --- /dev/null +++ b/packages/react-table/skills/with-tanstack-query/SKILL.md @@ -0,0 +1,186 @@ +--- +name: with-tanstack-query +description: > + Compose React Table v9 with TanStack Query for server filtering, sorting, pagination, and infinite data. Load for query-key table state, manual* processing boundaries, server rowCount, keepPreviousData, or avoiding duplicated query-result state. +metadata: + type: composition + library: '@tanstack/react-table' + library_version: '9.0.0-beta.38' + framework: react +requires: + - '@tanstack/table-core#client-vs-server' + - getting-started + - table-state +sources: + - 'TanStack/table:examples/react/with-tanstack-query' + - 'TanStack/table:examples/react/virtualized-infinite-scrolling' + - 'TanStack/table:docs/framework/react/guide/pagination.md' +--- + +This skill builds on `@tanstack/table-core#client-vs-server`, `getting-started`, and `table-state`. Query owns fetching/cache; Table owns grid state and receives rows already processed by every manual server stage. + +## Setup + +```tsx +import { keepPreviousData, useQuery } from '@tanstack/react-query' +import { useCreateAtom, useSelector } from '@tanstack/react-store' +import { + rowPaginationFeature, + tableFeatures, + useTable, +} from '@tanstack/react-table' +import type { PaginationState } from '@tanstack/react-table' + +const features = tableFeatures({ rowPaginationFeature }) +const emptyRows: Array<{ id: string }> = [] + +function ServerTable() { + const paginationAtom = useCreateAtom({ + pageIndex: 0, + pageSize: 20, + }) + const pagination = useSelector(paginationAtom, (value) => value) + const result = useQuery({ + queryKey: ['people', pagination], + queryFn: async () => + fetch( + `/api/people?page=${pagination.pageIndex}&size=${pagination.pageSize}`, + ).then( + (r) => + r.json() as Promise<{ + rows: Array<{ id: string }> + rowCount: number + }>, + ), + placeholderData: keepPreviousData, + }) + return useTable({ + features, + columns, + data: result.data?.rows ?? emptyRows, + rowCount: result.data?.rowCount, + atoms: { pagination: paginationAtom }, + manualPagination: true, + }) +} +``` + +## Core Patterns + +### Put every server-owned slice in the key + +```tsx +const result = useQuery({ + queryKey: ['people', pagination, sorting, columnFilters], + queryFn: () => fetchPeople({ pagination, sorting, columnFilters }), +}) +``` + +### Feed the query result directly to Table + +```tsx +const table = useTable({ + features, + columns, + data: result.data?.rows ?? emptyRows, +}) +``` + +## Common Mistakes + +### HIGH Duplicating query rows into state + +Wrong: + +```tsx +useEffect(() => setRows(result.data?.rows ?? []), [result.data]) +const table = useTable({ features, columns, data: rows }) +``` + +Correct: + +```tsx +const table = useTable({ + features, + columns, + data: result.data?.rows ?? emptyRows, +}) +``` + +The second state layer can lag behind the query cache and creates an extra synchronization path. + +Source: `examples/react/with-tanstack-query` + +### HIGH Omitting state from the query key + +Wrong: + +```tsx +useQuery({ queryKey: ['people'], queryFn: () => fetchPeople({ pagination }) }) +``` + +Correct: + +```tsx +useQuery({ + queryKey: ['people', pagination], + queryFn: () => fetchPeople({ pagination }), +}) +``` + +Query otherwise reuses cache entries for different server requests. + +Source: `examples/react/with-tanstack-query` + +### HIGH Expecting manual mode to fetch + +Wrong: + +```tsx +useTable({ features, columns, data, manualPagination: true }) +``` + +Correct: + +```tsx +useTable({ + features, + columns, + data: result.data?.rows ?? emptyRows, + rowCount: result.data?.rowCount, + manualPagination: true, +}) +``` + +`manualPagination` only bypasses client pagination; the application must fetch a processed page and provide its total count. + +Source: `docs/framework/react/guide/pagination.md` + +### MEDIUM Flashing an empty page during fetch + +Wrong: + +```tsx +useQuery({ + queryKey: ['people', pagination], + queryFn: () => fetchPeople({ pagination }), +}) +``` + +Correct: + +```tsx +useQuery({ + queryKey: ['people', pagination], + queryFn: () => fetchPeople({ pagination }), + placeholderData: keepPreviousData, +}) +``` + +Preserve the previous page intentionally when an empty loading transition is undesirable. + +Source: `examples/react/with-tanstack-query` + +## API Discovery + +Inspect `node_modules/@tanstack/react-table/src/index.ts` and the relevant core feature source; inspect the installed `@tanstack/react-query` source for current query option types. diff --git a/packages/react-table/skills/with-tanstack-virtual/SKILL.md b/packages/react-table/skills/with-tanstack-virtual/SKILL.md new file mode 100644 index 0000000000..cf18cfa96f --- /dev/null +++ b/packages/react-table/skills/with-tanstack-virtual/SKILL.md @@ -0,0 +1,197 @@ +--- +name: with-tanstack-virtual +description: > + Virtualize final React Table row or column models with TanStack Virtual. Load for useVirtualizer counts, scroll elements, stable keys, data-index measurement, dynamic heights, sticky headers/columns, grid/flex geometry, or infinite fetching; Virtual is renderer composition, not a Table feature. +metadata: + type: composition + library: '@tanstack/react-table' + library_version: '9.0.0-beta.38' + framework: react +requires: + - '@tanstack/table-core#core' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/react/guide/virtualization.md' + - 'TanStack/table:examples/react/virtualized-rows' + - 'TanStack/table:examples/react/virtualized-columns' + - 'TanStack/table:examples/react/virtualized-infinite-scrolling' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Build the Table model first, then virtualize its final rendered rows or visible columns. + +## Setup + +```tsx +import { useRef } from 'react' +import { useVirtualizer } from '@tanstack/react-virtual' + +function VirtualBody({ table }: { table: any }) { + const scrollRef = useRef(null) + const rows = table.getRowModel().rows + const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 36, + getItemKey: (index) => rows[index].id, + overscan: 5, + }) + return ( +
+
+ {virtualizer.getVirtualItems().map((item) => ( +
+ {rows[item.index].getAllCells().map((cell: any) => ( + + + + ))} +
+ ))} +
+
+ ) +} +``` + +## Core Patterns + +### Keep the virtualizer near its render loop + +```tsx +const rows = table.getRowModel().rows +const rowVirtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => container.current, + estimateSize: () => 36, +}) +``` + +This limits unrelated parent updates and keeps count, measurement, and rendered items together. + +### Use Table sizes in renderer CSS + +```tsx + + + +``` + +Table calculates size state; the renderer must apply it. + +## Common Mistakes + +### HIGH Registering Virtual as a feature + +Wrong: + +```tsx +const features = tableFeatures({ rowVirtualizer: useVirtualizer(options) }) +``` + +Correct: + +```tsx +const rows = table.getRowModel().rows +const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 36, +}) +``` + +Virtual controls rendering geometry and is not a Table feature or row model. + +Source: `docs/framework/react/guide/virtualization.md` + +### HIGH Virtualizing raw input data + +Wrong: + +```tsx +const virtualizer = useVirtualizer({ + count: data.length, + getScrollElement, + estimateSize, +}) +``` + +Correct: + +```tsx +const rows = table.getRowModel().rows +const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement, + estimateSize, +}) +``` + +Raw data ignores filtering, sorting, expansion, grouping, and pagination already applied by Table. + +Source: `examples/react/virtualized-rows` + +### HIGH Measuring against incomplete identity + +Wrong: + +```tsx +{row.id} +``` + +Correct: + +```tsx + + {row.id} + +``` + +Dynamic measurement needs the virtual index, and stable row identity prevents measurements moving to the wrong row. + +Source: `examples/react/virtualized-rows` + +### HIGH Omitting positioning geometry + +Wrong: + +```tsx +{virtualizer.getVirtualItems().map(renderRow)} +``` + +Correct: + +```tsx + + {virtualizer.getVirtualItems().map(renderRow)} + +``` + +Virtual only computes positions; the renderer must provide total spacer size and position items using each virtual start. + +Source: `examples/react/virtualized-rows` + +## API Discovery + +Inspect `node_modules/@tanstack/react-table/src/index.ts` for Table render APIs and installed `node_modules/@tanstack/react-virtual/src/` for the exact virtualizer options. Copy layout contracts from the maintained example matching rows, columns, or infinite loading. diff --git a/packages/solid-table-devtools/skills/devtools/SKILL.md b/packages/solid-table-devtools/skills/devtools/SKILL.md new file mode 100644 index 0000000000..055f1dae6a --- /dev/null +++ b/packages/solid-table-devtools/skills/devtools/SKILL.md @@ -0,0 +1,83 @@ +--- +name: devtools +description: > + Connect Solid Table v9 instances to Devtools with tableDevtoolsPlugin and useTanStackTableDevtools in the proper reactive owner. Load for missing targets, required options.key, enabled state, cleanup, or development versus explicit production exports. +metadata: + type: framework + library: '@tanstack/solid-table-devtools' + framework: solid + library_version: '9.0.0-beta.40' +requires: + - '@tanstack/table-core#core' + - '@tanstack/table-devtools#devtools' +sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/solid-table-devtools/src/index.ts' + - 'TanStack/table:packages/solid-table-devtools/src/useTanStackTableDevtools.ts' +--- + +This skill builds on @tanstack/table-core#core and @tanstack/table-devtools#devtools. + +## Setup + +```tsx +import { TanStackDevtools } from '@tanstack/solid-devtools' +import { createTable, tableFeatures } from '@tanstack/solid-table' +import { + tableDevtoolsPlugin, + useTanStackTableDevtools, +} from '@tanstack/solid-table-devtools' + +const features = tableFeatures({}) + +export function App() { + const table = createTable({ + key: 'users-table', + features, + columns: [], + data: [], + }) + useTanStackTableDevtools(table) + return +} +``` + +## Hooks and Components + +Register inside the component's Solid owner. Pass `{ enabled }` to the hook for conditional registration. + +## Common Mistakes + +### HIGH Registration created outside an owner + +Wrong: call the Solid registration hook at module scope. + +Correct: call it in the component that owns the table. + +The hook uses Solid reactive cleanup to unregister the target. + +Source: TanStack/table:packages/solid-table-devtools/src/useTanStackTableDevtools.ts + +### HIGH Missing or reused table key + +Wrong: omit `options.key` or share one key between mounted tables. + +Correct: assign a stable unique key per live table. + +The target registry skips missing keys and replaces duplicate identities. + +Source: TanStack/table:packages/table-devtools/src/tableTarget.ts + +### MEDIUM Production entrypoint assumed active + +Wrong: expect normal Devtools exports to inspect a production table. + +Correct: keep default guidance development-only; use `/production` only on explicit request. + +The package index selects no-op implementations outside development. + +Source: TanStack/table:packages/solid-table-devtools/src/index.ts + +## API Discovery + +Inspect `node_modules/@tanstack/solid-table-devtools/src/index.ts`, `useTanStackTableDevtools.ts`, and `production.ts`. diff --git a/packages/solid-table-devtools/skills/solid/compose-with-tanstack-devtools/SKILL.md b/packages/solid-table-devtools/skills/solid/compose-with-tanstack-devtools/SKILL.md deleted file mode 100644 index e8320e90c3..0000000000 --- a/packages/solid-table-devtools/skills/solid/compose-with-tanstack-devtools/SKILL.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -name: solid/compose-with-tanstack-devtools -description: > - Wire up TanStack Devtools for TanStack Table in Solid. Mount `TanStackDevtools` - with `tableDevtoolsPlugin()` once at the app root and call - `useTanStackTableDevtools(table)` after each `createTable` so the table - is registered as a devtools target. Live devtools are tree-shaken to no-ops in - production unless you import from `@tanstack/solid-table-devtools/production`. -type: composition -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - state-management - - solid/table-state -sources: - - TanStack/table:docs/devtools.md - - TanStack/table:packages/solid-table-devtools/src/index.ts - - TanStack/table:packages/solid-table-devtools/src/plugin.tsx - - TanStack/table:packages/solid-table-devtools/src/useTanStackTableDevtools.ts - - TanStack/table:packages/solid-table-devtools/src/production.ts ---- - -This skill builds on `tanstack-table/solid/table-state`. Read that first — the devtools panel inspects whatever table instance you register, so you need a working `createTable` before this skill is useful. - -## Setup - -Install the TanStack Devtools host and the Solid Table adapter: - -```sh -pnpm add @tanstack/solid-devtools @tanstack/solid-table-devtools -``` - -The recommended pattern has two parts: - -1. Mount `` once at the app root with `tableDevtoolsPlugin()`. -2. Call `useTanStackTableDevtools(table)` right after every `createTable()`. - -```tsx -import { render } from 'solid-js/web' -import { createTable } from '@tanstack/solid-table' -import { TanStackDevtools } from '@tanstack/solid-devtools' -import { - tableDevtoolsPlugin, - useTanStackTableDevtools, -} from '@tanstack/solid-table-devtools' - -function UsersScreen() { - const table = createTable({ - features, - key: 'users-table', - columns, - data, - }) - - // Register this table with the devtools panel. - useTanStackTableDevtools(table) - - return -} - -render( - () => ( - <> - - {/* Mount once, anywhere in the tree. */} - - - ), - document.getElementById('root')!, -) -``` - -`tableDevtoolsPlugin()` returns a plugin descriptor for the multi-panel TanStack Devtools UI. `useTanStackTableDevtools` registers the table inside a `createRenderEffect` and removes it via `onCleanup`, so it tracks the table's reactive scope. - -## Patterns - -### Keying Tables - -Add a stable `key` option to the table options. Devtools use this key as both the registration id and the panel selector label. - -```tsx -useTanStackTableDevtools(table) -``` - -### Multiple Tables - -Register as many tables as you like. The Table panel renders a selector. Name each one. - -```tsx -function Dashboard() { - const ordersTable = createTable(ordersOptions) - const usersTable = createTable(usersOptions) - - useTanStackTableDevtools(ordersTable) - useTanStackTableDevtools(usersTable) - - return -} -``` - -### Disabling Per Table - -`useTanStackTableDevtools` accepts an `enabled` option. When `false`, the registration is removed (the table disappears from the panel) but the hook still runs cleanly. - -```tsx -useTanStackTableDevtools(table, { - enabled: import.meta.env.DEV && showTableDevtools(), -}) -``` - -### Production Builds - -The default `@tanstack/solid-table-devtools` entrypoint swaps to no-op implementations when `process.env.NODE_ENV !== 'development'`. To ship the real devtools to production, switch BOTH imports to the `/production` entrypoint: - -```tsx -import { TanStackDevtools } from '@tanstack/solid-devtools' -import { - tableDevtoolsPlugin, - useTanStackTableDevtools, -} from '@tanstack/solid-table-devtools/production' -``` - -If you mix entrypoints (one from `/production`, one from the default), one side is a no-op in production and the panel will appear empty. - -### Conditional Devtools by Env - -For a code-split production-only devtools bundle, dynamically import the `/production` entrypoint behind a flag: - -```tsx -import { lazy, Show, Suspense } from 'solid-js' - -const TableDevtoolsRoot = lazy(async () => { - const { tableDevtoolsPlugin } = - await import('@tanstack/solid-table-devtools/production') - const { TanStackDevtools } = await import('@tanstack/solid-devtools') - return { - default: () => , - } -}) - -function Root() { - return ( - <> - - - - - - - - ) -} -``` - -## Common Mistakes - -### Forgetting to mount `` at the app root - -Calling `useTanStackTableDevtools(table)` alone does nothing visible — it only registers the table with the devtools target store. Without a `` somewhere in the tree, there is no panel to render the registration. Symptom: hook runs without errors, no devtools button appears. - -### Importing devtools from the default path in a prod-only bundle - -If you only deploy production builds, `@tanstack/solid-table-devtools` resolves to no-op implementations. The plugin will mount, but the panel will be empty. Use `@tanstack/solid-table-devtools/production` if you want the real devtools available there. - -### Accidentally shipping devtools to end users via `/production` - -The flip side: importing from `/production` in your default app bundle means every visitor downloads and runs the devtools UI. That is usually not what you want. Restrict `/production` imports to dev/preview entrypoints or code-split them behind a flag. - -### Calling `useTanStackTableDevtools` outside a reactive scope - -The hook uses `createRenderEffect` + `onCleanup`. Call it inside a component (or another Solid reactive scope) that owns the `table`. Calling it in a top-level module body bypasses the reactive owner and the cleanup never fires. - -### Multiple tables without keys - -A table registered without `options.key` is skipped and devtools log an error. Add a unique `key` option to every table that should appear in devtools. diff --git a/packages/solid-table/skills/create-table-hook/SKILL.md b/packages/solid-table/skills/create-table-hook/SKILL.md new file mode 100644 index 0000000000..c79b599664 --- /dev/null +++ b/packages/solid-table/skills/create-table-hook/SKILL.md @@ -0,0 +1,122 @@ +--- +name: create-table-hook +description: > + Create reusable Solid table infrastructure with createTableHook, createAppTable, createAppColumnHelper, shared features/defaults, component registries, App wrappers, and typed context hooks. Load for recurring app conventions, reactive per-table getters, or prop drilling. +metadata: + { + type: framework, + library: '@tanstack/solid-table', + library_version: '9.0.0-beta.38', + framework: solid, + } +requires: ['@tanstack/table-core#core', getting-started, table-state] +sources: + - 'TanStack/table:docs/framework/solid/guide/composable-tables.md' + - 'TanStack/table:examples/solid/composable-tables' + - 'TanStack/table:packages/solid-table/src/createTableHook.tsx' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Use a factory when tables share policy; keep one-off tables on `createTable`. + +## Setup + +```tsx +import { + createTableHook, + rowSelectionFeature, + tableFeatures, +} from '@tanstack/solid-table' + +export const { createAppColumnHelper, createAppTable, useTableContext } = + createTableHook({ + features: tableFeatures({ rowSelectionFeature }), + getRowId: (row: { id: string }) => row.id, + }) +``` + +## Core Patterns + +### Infer columns with the bound helper + +```tsx +type Person = { id: string; name: string } +const helper = createAppColumnHelper() +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) +``` + +### Preserve per-table reactivity + +```tsx +const table = createAppTable({ + columns, + get data() { + return data() + }, +}) +return {() => } +``` + +## Common Mistakes + +### MEDIUM Abstracting a one-off table + +Wrong: + +```tsx +const app = createTableHook({ features: tableFeatures({}) }) +``` + +Correct: + +```tsx +const table = createTable({ features, columns, data }) +``` + +The factory should encode repeated app conventions rather than add ceremony. + +Source: `docs/framework/solid/guide/composable-tables.md` + +### HIGH Passing per-table snapshots + +Wrong: + +```tsx +createAppTable({ columns, data: data() }) +``` + +Correct: + +```tsx +createAppTable({ + columns, + get data() { + return data() + }, +}) +``` + +The getter preserves Solid tracking when the signal changes. + +Source: `examples/solid/composable-tables` + +### HIGH Reading context outside wrappers + +Wrong: + +```tsx +return +``` + +Correct: + +```tsx +return {() => } +``` + +Returned context hooks must run under the matching factory wrapper; use them instead of prop drilling registered components. + +Source: `packages/solid-table/src/createTableHook.tsx` + +## API Discovery + +Inspect `node_modules/@tanstack/solid-table/src/createTableHook.tsx` for exact returned names, component binding, context providers, and reactive option merging. diff --git a/packages/solid-table/skills/getting-started/SKILL.md b/packages/solid-table/skills/getting-started/SKILL.md new file mode 100644 index 0000000000..9ded6f2f3d --- /dev/null +++ b/packages/solid-table/skills/getting-started/SKILL.md @@ -0,0 +1,175 @@ +--- +name: getting-started +description: > + Create a Solid Table v9 table with createTable, explicit tableFeatures, reactive data getters, stable static inputs, and Solid JSX/FlexRender. Load when starting a Solid table, replacing createSolidTable, or adapting React examples. +metadata: + { + type: framework, + library: '@tanstack/solid-table', + library_version: '9.0.0-beta.38', + framework: solid, + } +requires: ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] +sources: + - 'TanStack/table:docs/framework/solid/guide/migrating.md' + - 'TanStack/table:examples/solid/basic-use-table' + - 'TanStack/table:packages/solid-table/src/index.tsx' +--- + +This skill builds on `@tanstack/table-core#core` and `@tanstack/table-core#table-features`. Solid Table supplies reactive models; the application still renders and styles its own headless markup. + +## Setup + +```tsx +import { For, createSignal } from 'solid-js' +import { + createColumnHelper, + createTable, + tableFeatures, +} from '@tanstack/solid-table' + +type Person = { name: string } +const features = tableFeatures({}) +const helper = createColumnHelper() +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) + +export function PeopleTable() { + const [data] = createSignal([{ name: 'Ada' }]) + const table = createTable({ + features, + columns, + get data() { + return data() + }, + }) + return ( + + + + {(group) => ( + + + {(header) => ( + + )} + + + )} + + + + + {(row) => ( + + + {(cell) => ( + + )} + + + )} + + +
+ +
+ +
+ ) +} +``` + +## Core Patterns + +### Expose changing inputs through getters + +```tsx +const table = createTable({ + features, + columns, + get data() { + return data() + }, +}) +``` + +### Keep feature definitions static + +```tsx +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +``` + +## Common Mistakes + +### HIGH Using the v8 constructor + +Wrong: + +```tsx +const table = createSolidTable({ data: data(), columns }) +``` + +Correct: + +```tsx +const table = createTable({ + features, + columns, + get data() { + return data() + }, +}) +``` + +V9 uses `createTable`, explicit features, and reactive option access. + +Source: `docs/framework/solid/guide/migrating.md` + +### HIGH Passing a signal snapshot + +Wrong: + +```tsx +const table = createTable({ features, columns, data: data() }) +``` + +Correct: + +```tsx +const table = createTable({ + features, + columns, + get data() { + return data() + }, +}) +``` + +The snapshot is read once; the getter lets the adapter track later signal changes. + +Source: `examples/solid/basic-use-table` + +### HIGH Expecting Table to render UI + +Wrong: + +```tsx +return
{table}
+``` + +Correct: + +```tsx +return {(row) =>
{row.id}
}
+``` + +Table is headless; Solid markup, CSS, semantics, and interactions remain renderer-owned. + +Source: `packages/solid-table/src/createTable.ts` + +## API Discovery + +Inspect `node_modules/@tanstack/solid-table/src/index.tsx`, then `createTable.ts`, `FlexRender.tsx`, and installed core feature directories. diff --git a/packages/solid-table/skills/migrate-v8-to-v9/SKILL.md b/packages/solid-table/skills/migrate-v8-to-v9/SKILL.md new file mode 100644 index 0000000000..3b3cd66b5c --- /dev/null +++ b/packages/solid-table/skills/migrate-v8-to-v9/SKILL.md @@ -0,0 +1,211 @@ +--- +name: migrate-v8-to-v9 +description: > + Perform a complete @tanstack/solid-table v8-to-v9 migration: createTable, explicit features and row-model slots, getter-based Solid reactivity, atom state, rendering, composable tables, type helpers, and every shared API rename and semantic change. Use for migration plans, implementation, or audits. +metadata: + type: lifecycle + library: '@tanstack/solid-table' + library_version: '9.0.0-beta.38' + framework: solid +requires: + - '@tanstack/table-core#migrate-v8-to-v9' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/solid/guide/migrating.md' + - 'TanStack/table:packages/solid-table/src/index.tsx' + - 'TanStack/table:examples/solid/basic-use-table' +--- + +Read `@tanstack/table-core#migrate-v8-to-v9`, `getting-started`, and `table-state`. Use this as the exhaustive Solid migration checklist. Check the installed source before emitting APIs for another beta. + +Framework prerequisite: Solid 1.3 or newer (`solid-js >=1.3`). + +## Target architecture + +```tsx +import { + createSortedRowModel, + createTable, + rowSortingFeature, + tableFeatures, +} from '@tanstack/solid-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) + +const table = createTable({ + features, + columns, + get data() { + return data() + }, +}) +``` + +Keep static features and columns outside reactive component work. Prefer explicit features as the end state; `stockFeatures` is a kitchen-sink migration shortcut. + +## Complete breaking-change map + +### Construction and feature registration + +| v8 | v9 | +| --------------------------------------------------------- | ------------------------------------------------------- | +| `createSolidTable(options)` | `createTable({ ...options, features })` | +| Every feature bundled | Register used `*Feature` objects with `tableFeatures()` | +| `getCoreRowModel()` option | Remove it; core is automatic | +| `getFilteredRowModel()` | `filteredRowModel: createFilteredRowModel()` | +| `getSortedRowModel()` | `sortedRowModel: createSortedRowModel()` | +| `getPaginationRowModel()` | `paginatedRowModel: createPaginatedRowModel()` | +| `getExpandedRowModel()` | `expandedRowModel: createExpandedRowModel()` | +| `getGroupedRowModel()` | `groupedRowModel: createGroupedRowModel()` | +| `getFacetedRowModel()` | `facetedRowModel: createFacetedRowModel()` | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues: createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues: createFacetedUniqueValues()` | +| Table/factory `sortingFns`, `filterFns`, `aggregationFns` | `sortFns`, `filterFns`, `aggregationFns` feature slots | +| Early-beta `rowModels: { ... }` | Direct named slots in `tableFeatures()` | + +Place each prerequisite feature before its row-model slot. Stock features are `columnFilteringFeature`, `globalFilteringFeature`, `rowSortingFeature`, `rowPaginationFeature`, `rowSelectionFeature`, `rowExpandingFeature`, `rowPinningFeature`, `columnPinningFeature`, `columnVisibilityFeature`, `columnOrderingFeature`, `columnSizingFeature`, `columnResizingFeature`, `columnGroupingFeature`, and `columnFacetingFeature`. + +### Solid state and reactivity + +| v8 | v9 | +| ------------------------------ | --------------------------------------------------------------------------- | +| `table.getState()` | `table.atoms..get()` in tracked scopes, or broad `table.store.get()` | +| Top-level `onStateChange` | Per-slice callbacks or `table.store.subscribe()` | +| Eager signal values in options | Getters for reactive `data` and controlled state slices | +| Whole-state rendering | Narrow atom reads, `createMemo`, or `table.Subscribe` | + +```tsx +const [sorting, setSorting] = createSignal([]) +const table = createTable({ + features, + columns, + get data() { + return data() + }, + state: { + get sorting() { + return sorting() + }, + }, + onSortingChange: setSorting, +}) +``` + +`table.Subscribe` passes atoms to its child. A Solid component child body is untracked, so read atoms inside JSX expressions or a thunk invoked by JSX: + +```tsx + + {(atoms) => Page {atoms.pagination.get().pageIndex + 1}} + +``` + +Use `createAtom`/`useSelector` from `@tanstack/solid-store` for externally owned slices. An external atom wins over `state` for the same slice; do not combine ownership models accidentally. + +### Rendering and composition + +- Replace `flexRender(def, context)` with `` or ``. +- Use `tableOptions()` for typed reusable option fragments. +- Use `createTableHook({ features, ...defaults })` for repeated conventions; it returns helpers such as `createAppTable` and `createAppColumnHelper`. +- Invoke row/cell/column/header methods through their instance. Prototype methods lose `this` when extracted and are absent from object spread, `Object.keys`, and JSON. Table-instance methods are not affected. + +### TypeScript and helper changes + +| v8 | v9 | +| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | +| `createColumnHelper()` | `createColumnHelper()` | +| Plain nested column arrays | `columnHelper.columns([...])` for `TValue` inference | +| `ColumnDef` | `ColumnDef` | +| `Column`, `Row`, `Table` | Add `TFeatures` first | +| `Cell` | `Cell` | +| `TableMeta` / `ColumnMeta` | Add `TFeatures`, or use per-table `tableMeta` / `columnMeta` with `metaHelper()` | +| Global `FilterFns`, `SortFns`, `AggregationFns`, and `FilterMeta` augmentation | `filterFns`, `sortFns`, `aggregationFns`, and `filterMeta` slots | +| `RowData = unknown` | Record or array row data | + +Infer with `typeof features`; use `StockFeatures` only when deliberately typing `stockFeatures`. + +### Shared API and semantic changes + +| v8 / pre-beta.38 pinning | v9 beta.38+ | +| -------------------------------------------------------------- | ------------------------------------------------------------- | +| `columnPinning.left` / `.right` | `.start` / `.end` | +| `column.pin('left' \| 'right')` | `column.pin('start' \| 'end')` | +| `getIsPinned() === 'left' \| 'right'` | `'start' \| 'end'` | +| `row.getLeftVisibleCells()` / `getRightVisibleCells()` | `getStartVisibleCells()` / `getEndVisibleCells()` | +| `getLeftHeaderGroups()` / `getRightHeaderGroups()` | `getStartHeaderGroups()` / `getEndHeaderGroups()` | +| `getLeftFooterGroups()` / `getRightFooterGroups()` | `getStartFooterGroups()` / `getEndFooterGroups()` | +| `getLeftFlatHeaders()` / `getRightFlatHeaders()` | `getStartFlatHeaders()` / `getEndFlatHeaders()` | +| `getLeftLeafHeaders()` / `getRightLeafHeaders()` | `getStartLeafHeaders()` / `getEndLeafHeaders()` | +| `getLeftLeafColumns()` / `getRightLeafColumns()` | `getStartLeafColumns()` / `getEndLeafColumns()` | +| `getLeftVisibleLeafColumns()` / `getRightVisibleLeafColumns()` | `getStartVisibleLeafColumns()` / `getEndVisibleLeafColumns()` | +| `getLeftTotalSize()` / `getRightTotalSize()` | `getStartTotalSize()` / `getEndTotalSize()` | +| `column.getStart('left')` | `column.getStart('start')` | +| `column.getAfter('right')` | `column.getAfter('end')` | +| `column.getIndex('left' \| 'right')` | `column.getIndex('start' \| 'end')` | + +Logical pinning does not implement DOM direction styling; use logical CSS insets. `columnResizeDirection` is unchanged. + +| v8 | v9 | +| ----------------------------------------------- | -------------------------------------------------------------------------- | +| Table `enablePinning` | `enableColumnPinning` plus `enableRowPinning`; column-level option remains | +| Combined sizing/resizing | `columnSizingFeature`; add `columnResizingFeature` for interaction | +| `columnSizingInfo` / `onColumnSizingInfoChange` | `columnResizing` / `onColumnResizingChange` | +| `setColumnSizingInfo()` | `setColumnResizing()` | +| `sortingFn` / `sortingFns` | `sortFn` / `sortFns` | +| `getSortingFn()` / `getAutoSortingFn()` | `getSortFn()` / `getAutoSortFn()` | +| `SortingFn` / `SortingFns` | `SortFn` / `SortFns` | +| `row._getAllCellsByColumnId()` | `row.getAllCellsByColumnId()` | +| `table._getPinnedRows()` | `getTopRows()`, `getCenterRows()`, or `getBottomRows()` | +| `table._getFacetedRowModel()` | Public faceting APIs on the relevant column/table | +| `table._getFacetedMinMaxValues()` | `getFacetedMinMaxValues()` | +| `table._getFacetedUniqueValues()` | `getFacetedUniqueValues()` | + +All other underscore-prefixed internals are removed. `getIsSomeRowsSelected()` and `getIsSomePageRowsSelected()` now mean at least one even when all are selected. Use `getIsSomeRowsSelected() && !getIsAllRowsSelected()` or `getIsSomePageRowsSelected() && !getIsAllPageRowsSelected()` for checkbox indeterminate state. + +## Migration procedure + +1. Replace `createSolidTable` with `createTable` and inventory used features, processing, state, and APIs. +2. Build `tableFeatures()` in prerequisite order; remove `getCoreRowModel` and old/early-beta row-model placement. +3. Apply all pinning, sizing, sorting, row, and selection mappings above. +4. Update helpers/types with `typeof features`; migrate meta and function augmentation to per-feature slots where appropriate. +5. Preserve Solid tracking with getters for `data` and controlled state; replace `getState()` and `onStateChange`. +6. Choose internal, controlled-signal, or external-atom ownership per slice. +7. Replace unbound row/cell/column/header methods and migrate rendering. +8. Add `tableOptions`, `table.Subscribe`, or `createTableHook` only where composition or fine-grained rendering calls for them. +9. Type-check and exercise every enabled client- and server-side flow. +10. Audit away `stockFeatures` when production tree-shaking matters. + +## Final migration checklist + +- [ ] Replace createSolidTable with createTable and preserve reactive inputs through getters. +- [ ] Register every used stock feature explicitly and order prerequisites before slots. +- [ ] Remove `getCoreRowModel`; move all eight optional row models into `tableFeatures`. +- [ ] Move `filterFns`, `sortFns`, `aggregationFns`, and `filterMeta` into feature slots. +- [ ] Replace `table.getState()` and onStateChange with tracked atom/store reads, per-slice callbacks, subscriptions, or external atoms. +- [ ] Audit Solid tracking scopes, controlled getters, atom precedence, and reset ownership. +- [ ] Replace unbound/copied row, cell, column, and header methods. +- [ ] Apply the complete logical pinning map and CSS changes. +- [ ] Split pinning options and sizing/resizing; rename the resizing state, setter, and callback. +- [ ] Apply every sorting rename and remove each listed internal API. +- [ ] Pair some-selected with the matching all-selected predicate. +- [ ] Update TFeatures helpers/types, `columns()`, `StockFeatures`, meta/registry slots, and RowData. +- [ ] Migrate Solid FlexRender and use tableOptions/createTableHook only for repeated conventions. +- [ ] Type-check and exercise every enabled client/manual feature and LTR/RTL layout flow. +- [ ] Audit away temporary stockFeatures usage when explicit tree-shaking is intended. + +## Common migration failures + +- Calling `createTable({ data: data() })` and freezing the initial array instead of providing a getter. +- Leaving row models in table options or omitting their prerequisite feature. +- Reading atoms in an untracked component child body and expecting Solid updates. +- Supplying controlled state without getter properties or per-slice callbacks. +- Destructuring prototype-backed object methods. +- Updating pinning state names but not CSS and region APIs. +- Treating changed “some selected” semantics as checkbox indeterminate state. + +## API discovery + +Inspect `node_modules/@tanstack/solid-table/src/index.tsx` and `node_modules/@tanstack/table-core/src/index.ts`. Compare v8 names only against the migration guide, not current main-branch assumptions. diff --git a/packages/solid-table/skills/solid/client-to-server/SKILL.md b/packages/solid-table/skills/solid/client-to-server/SKILL.md deleted file mode 100644 index d6fd33b0c0..0000000000 --- a/packages/solid-table/skills/solid/client-to-server/SKILL.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -name: solid/client-to-server -description: > - Convert a client-side `@tanstack/solid-table` to server-side. Lift the - sort/filter/pagination state into Solid signals or external atoms (`createAtom` - + `useSelector` from `@tanstack/solid-store`), set the corresponding - `manual*` options, supply `rowCount`, and skip the matching row-model factory - (the server already did that work). -type: lifecycle -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - state-management - - pagination - - filtering - - sorting - - solid/table-state -sources: - - docs/framework/solid/guide/table-state.md - - examples/solid/basic-external-atoms/ - - examples/solid/with-tanstack-query/ ---- - -# Client-to-Server — `@tanstack/solid-table` - -When the server (not the browser) owns sort/filter/pagination, you need to -(a) lift those slices out of the table, (b) tell the table not to do that work -itself, and (c) keep the same UI APIs. - -## Mental model - -Each row-model stage has a `manual*` switch: - -| Slice | `manual*` option | What the server now owns | -| ---------- | ------------------------ | -------------------------------- | -| Pagination | `manualPagination: true` | Slicing rows to the current page | -| Sorting | `manualSorting: true` | Ordering rows | -| Filtering | `manualFiltering: true` | Column filters + global filter | -| Grouping | `manualGrouping: true` | Group buckets | -| Expanding | `manualExpanding: true` | Subrow expansion | - -When `manual*` is on: - -- The matching row-model factory is **not required** — the server already did - the work. Skip `createPaginatedRowModel()` for a paginated server endpoint. -- The table will not re-derive that slice. It hands you the new state through - `on[State]Change` / external atoms and trusts the next `data` you give it. -- You typically need to provide `rowCount` so APIs like `table.getPageCount()` - return the server's totals. - -## Recommended pattern: external atoms + `useSelector` - -External atoms are the cleanest cross-component pattern in Solid v9 — the -fetcher and the table can both subscribe to the same atoms. - -```tsx -import { - createTable, - createColumnHelper, - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - tableFeatures, - type PaginationState, - type SortingState, - type ColumnFiltersState, -} from '@tanstack/solid-table' -import { createAtom, useSelector } from '@tanstack/solid-store' -import { createResource } from 'solid-js' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, -}) - -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) -const sortingAtom = createAtom([]) -const filtersAtom = createAtom([]) - -function ServerTable() { - // Read each atom as an Accessor for fetcher dependency tracking - const pagination = useSelector(paginationAtom) - const sorting = useSelector(sortingAtom) - const filters = useSelector(filtersAtom) - - const [resource] = createResource( - () => ({ - pagination: pagination(), - sorting: sorting(), - filters: filters(), - }), - (params) => - fetch('/api/people?' + serialize(params)).then((r) => - r.json(), - ) as Promise<{ - rows: Array - rowCount: number - }>, - ) - - const table = createTable({ - features, - columns, - get data() { - return resource()?.rows ?? [] - }, - get rowCount() { - return resource()?.rowCount ?? 0 - }, - atoms: { - pagination: paginationAtom, - sorting: sortingAtom, - columnFilters: filtersAtom, - }, - manualPagination: true, - manualSorting: true, - manualFiltering: true, - }) - - return /* same JSX as a client table */ -} -``` - -Anywhere else in the app you can also call `useSelector(paginationAtom)` to -read the same state — for a "Reset filters" header button, a URL-sync hook, -etc. - -## Alternative: `state` + `on*Change` with `createSignal` - -If you prefer not to introduce `@tanstack/solid-store`, use plain Solid signals. -Slightly less ergonomic for cross-component sharing. - -```tsx -const [pagination, setPagination] = createSignal({ - pageIndex: 0, - pageSize: 10, -}) -const [sorting, setSorting] = createSignal([]) -const [filters, setFilters] = createSignal([]) - -const table = createTable({ - features, - columns, - get data() { - return resource()?.rows ?? [] - }, - get rowCount() { - return resource()?.rowCount ?? 0 - }, - state: { - get pagination() { - return pagination() - }, - get sorting() { - return sorting() - }, - get columnFilters() { - return filters() - }, - }, - onPaginationChange: setPagination, - onSortingChange: setSorting, - onColumnFiltersChange: setFilters, - manualPagination: true, - manualSorting: true, - manualFiltering: true, -}) -``` - -> Don't mix: providing both `atoms.pagination` and `state.pagination`+`onPaginationChange` -> for the same slice is ambiguous. The atom wins. Pick one. - -## Pair with TanStack Query - -`@tanstack/solid-query` is the canonical fetcher. See the -`compose-with-tanstack-query` skill for the full pattern — key the query on -your pagination/sort/filter accessors, use `keepPreviousData` to avoid -"0 rows" flashes between pages, and feed `data.rows` / `data.rowCount` into the -table. - -## Partial server-side - -You don't have to flip every switch. Mixed modes are valid: - -- `manualPagination: true` + client-side sort/filter: server slices the page, browser orders + filters that slice (rare — usually fights you, but supported). -- `manualSorting: true` only: full dataset in the browser, but the server already ordered rows. Useful for very large pre-sorted dumps. -- `manualPagination: true` + `manualFiltering: true`, client-side `sortedRowModel`: filter + paginate server-side, sort the visible page in the browser. - -When in doubt, flip them all and let the server own everything. - -## Failure modes - -### CRITICAL — flipped `manualPagination` without auditing filtering/sorting - -If you only set `manualPagination: true` but still rely on a client `createFilteredRowModel`, the filter runs against the **current server page**, not the full dataset. Either also set `manualFiltering: true`, or have the server do the filtering and remove the client filter row-model. - -### CRITICAL — forgot `rowCount` - -Without `rowCount`, `table.getPageCount()` is computed from the local `data` -length, which under `manualPagination: true` is one page. `lastPage()`, -`canNextPage`, the page input — all wrong. Always supply -`get rowCount() { return resource()?.rowCount ?? 0 }`. - -### HIGH — mixed ownership of the same slice - -Providing both `atoms.pagination` and `state.pagination` / `onPaginationChange` -for the same slice is ambiguous. The atom wins; the `state`/`on*Change` is -silently ignored. Pick one ownership model per slice. - -### HIGH — kept the client row-model factory after going manual - -If you flipped `manualSorting: true`, keeping `sortedRowModel: createSortedRowModel()` in `tableFeatures()` does no harm but is dead weight in the bundle and confusing to readers. Remove it. - -### MEDIUM — `data: data()` instead of `get data()` - -Same Solid pitfall as a client table: `data` must be a tracked reactive read. -With a resource: `get data() { return resource()?.rows ?? [] }`. - -### MEDIUM — paginating with `manualPagination` but rebuilding `data` reference unnecessarily - -If `data` identity changes on every render (e.g. always returning a new `[]` -when loading), expect spurious row-model recomputes. Memoize a stable empty -fallback: `const empty: Array = []` outside the component, then -`get data() { return resource()?.rows ?? empty }`. - -### MEDIUM — `autoResetPageIndex` surprise on data swap - -When the underlying `data` reference changes, the paginator may reset -`pageIndex` to 0 by default. With a server-driven `pagination` atom you usually -don't want that — set `autoResetPageIndex: false` on the table options. - -### LOW — assuming `getSelectedRowModel()` covers all rows - -Under `manualPagination: true`, `getSelectedRowModel()` only walks the currently -loaded rows. If the user "selected all" across pages, the table cannot know -that — track that intent in your own atom and reconcile server-side. diff --git a/packages/solid-table/skills/solid/compose-with-tanstack-form/SKILL.md b/packages/solid-table/skills/solid/compose-with-tanstack-form/SKILL.md deleted file mode 100644 index d5bdead65c..0000000000 --- a/packages/solid-table/skills/solid/compose-with-tanstack-form/SKILL.md +++ /dev/null @@ -1,320 +0,0 @@ ---- -name: solid/compose-with-tanstack-form -description: > - Editable cells in `@tanstack/solid-table` with `@tanstack/solid-form`. The - table is the layout primitive; the form owns the state. Build the form with - `createFormHook` + `createFormHookContexts` to register reusable field - components (`TextField`, `NumberField`, `SelectField`), source `data` from - `form.state.values.data` via a reactive `get data()` getter, and render - `` inside each column's `cell`. -type: composition -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - row-selection - - column-definitions -sources: - - examples/solid/with-tanstack-form/src/App.tsx - - examples/solid/with-tanstack-form/src/form.tsx ---- - -# Compose with `@tanstack/solid-form` - -Editable spreadsheet-style cells. The table doesn't track values; the form -does. The table just lays out which input goes in which cell. - -## Install - -```bash -pnpm add @tanstack/solid-form zod -``` - -## Step 1 — Register field components with `createFormHook` - -```tsx -// form.tsx -import { - createFormHook, - createFormHookContexts, - useStore, -} from '@tanstack/solid-form' -import { For, Show } from 'solid-js' - -export const { fieldContext, useFieldContext, formContext, useFormContext } = - createFormHookContexts() - -function TextField() { - const field = useFieldContext() - const errors = useStore(field().store, (s) => s.meta.errors) - return ( -
- field().handleChange(e.currentTarget.value)} - onBlur={() => field().handleBlur()} - /> - 0}> -
{errors().join(', ')}
-
-
- ) -} - -function NumberField() { - const field = useFieldContext() - const errors = useStore(field().store, (s) => s.meta.errors) - return ( -
- field().handleChange(Number(e.currentTarget.value))} - onBlur={() => field().handleBlur()} - /> - 0}> -
{errors().join(', ')}
-
-
- ) -} - -function SelectField() { - const field = useFieldContext() - return ( - - ) -} - -function SubmitButton(props: { label: string }) { - const form = useFormContext() - return ( - - ) -} - -export const { useAppForm } = createFormHook({ - fieldComponents: { TextField, NumberField, SelectField }, - formComponents: { SubmitButton }, - fieldContext, - formContext, -}) -``` - -`field` is itself an accessor in `@tanstack/solid-form` — it's `field().state.value`, -not `field.state.value`. Same accessor-call pattern as table state. - -## Step 2 — Build the table with form-driven cells - -The trick: `get data()` reads from `form.state.values.data`. The form owns the -rows; the table reflects them. - -```tsx -import { - createTable, - createColumnHelper, - rowPaginationFeature, - columnFilteringFeature, - createPaginatedRowModel, - createFilteredRowModel, - filterFns, - tableFeatures, - FlexRender, -} from '@tanstack/solid-table' -import { z } from 'zod' -import { createMemo, For } from 'solid-js' -import { useAppForm } from './form' - -const features = tableFeatures({ - rowPaginationFeature, - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - filterFns, -}) - -const columnHelper = createColumnHelper() - -function App() { - const form = useAppForm(() => ({ - defaultValues: { data: makeData(100) }, - onSubmit: ({ value }) => console.log(value), - validators: { onChange: formSchema }, - })) - - // columns depend on `form` (reactive). Wrap in createMemo for stable identity per inputs. - const columns = createMemo(() => - columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First Name', - cell: ({ row }) => ( - - {(field) => } - - ), - }), - columnHelper.accessor('age', { - header: 'Age', - cell: ({ row }) => ( - - {(field) => } - - ), - }), - columnHelper.accessor('status', { - header: 'Status', - cell: ({ row }) => ( - - {(field) => } - - ), - }), - ]), - ) - - const table = createTable({ - features, - get columns() { - return columns() - }, - get data() { - return form.state.values.data - }, - }) - - return ( -
{ - e.preventDefault() - void form.handleSubmit() - }} - > - - - - {(hg) => ( - - - {(h) => ( - - )} - - - )} - - - - - {(row) => ( - - - {(c) => ( - - )} - - - )} - - -
- -
- -
- - - -
- ) -} -``` - -## Why this layering works - -- **`form.state.values.data` is the source of truth.** Editing a `` calls - `field().handleChange(...)`, which mutates the form's `data` array. -- **`get data() { return form.state.values.data }`** subscribes the table to - the form. Each keystroke flows: input → form store → `data()` accessor → table → new row model → `` repaints only what changed (Solid's fine-grained reactivity). -- **The table doesn't know about editing.** No `editingRowId` state, no inline - cell mode toggles. Each row index is always editable; the column `cell` - renderer chooses what to draw. - -## Adding rows - -```tsx -const addRow = () => form.pushFieldValue('data', emptyPerson()) -const removeRow = (i: number) => form.removeFieldValue('data', i) -``` - -Combined with `manualPagination: false` (the default), the table picks up the -new row automatically. - -## Combining with row-selection - -If you want "delete selected rows": - -1. Register `rowSelectionFeature` in `features`. -2. Add a checkbox display column. Use `row.getIsSelected()` / `row.getToggleSelectedHandler()`. -3. On delete: read `table.getSelectedRowModel().rows`, find each `row.index`, call `form.removeFieldValue('data', index)` (highest index first to avoid shifting). - -## Failure modes - -### CRITICAL — `field.state.value` instead of `field().state.value` - -`field` is an accessor. Call it. Same trap as `table.state()`. - -### CRITICAL — `data: form.state.values.data` without the getter - -Same Solid pitfall as everywhere else. Reads once at construction. Use a -getter so edits flow through. - -### HIGH — `columns` array not memoized when it references `form` - -`columns` literally embeds `` per column. If `columns` is a -plain array re-evaluated per render (or per data change), the table sees a new -columns identity and recomputes column metadata. Wrap in `createMemo`. - -### HIGH — using row identity that doesn't survive add/remove - -If you set `getRowId: (row, index) => String(index)`, then deleting row 0 turns -old row 1 into new row 0 — the input that was focused jumps to a different -person. Either accept that, or give each person a stable id (`row.id`) and -`getRowId: (row) => row.id`. - -### MEDIUM — focus lost on every keystroke - -Almost always caused by inline-defined cell components whose identity changes -each render. Define `TextField` / `NumberField` / `SelectField` once (via -`createFormHook`), reference them via `field.TextField`, and let -`` own the field lifecycle. The example pattern handles this. - -### MEDIUM — re-running validation on every cell render - -`` should reference a -schema that has stable identity. Inline `z.string().min(1)` per render creates -a new schema each time; pull it to module scope or memoize. - -### LOW — submitting with stale rows - -Filtering or sorting affects what the table **displays**, not what the form -**holds**. `form.handleSubmit()` submits all rows in `form.state.values.data` -regardless of the table's current filter. If you only want visible rows, map -through `table.getRowModel().rows` and pull their indexes. diff --git a/packages/solid-table/skills/solid/compose-with-tanstack-pacer/SKILL.md b/packages/solid-table/skills/solid/compose-with-tanstack-pacer/SKILL.md deleted file mode 100644 index b9ffc6f336..0000000000 --- a/packages/solid-table/skills/solid/compose-with-tanstack-pacer/SKILL.md +++ /dev/null @@ -1,209 +0,0 @@ ---- -name: solid/compose-with-tanstack-pacer -description: > - Debounce or throttle high-frequency writes that drive a `@tanstack/solid-table` - with `@tanstack/solid-pacer`. Typical targets: column filter inputs (debounce - text → `column.setFilterValue`) and column-resize state (throttle pointer - moves). Pattern: wrap the setter in `createDebouncer`/`createThrottler` (or - use the matching hook), call it from input handlers; the table's atoms still - drive the row model. -type: composition -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - filtering - - column-layout -sources: - - examples/solid/filters/ - - examples/solid/column-resizing-performant/ ---- - -# Compose with `@tanstack/solid-pacer` - -`@tanstack/solid-pacer` rate-limits writes. The two table use cases that pay -back the most: - -1. **Filter inputs** — every keystroke triggers a full filter pass. Debounce - the call to `column.setFilterValue` (or to your filter atom's setter). -2. **Column resizing** — drag events fire dozens per second. Throttle the - `columnSizing` write so the row model doesn't re-derive on every pointer - move. - -## Install - -```bash -pnpm add @tanstack/solid-pacer -``` - -## Pattern 1 — Debounced column filter - -```tsx -import { - createTable, - createColumnHelper, - columnFilteringFeature, - createFilteredRowModel, - filterFns, - tableFeatures, - FlexRender, -} from '@tanstack/solid-table' -import { useDebouncedCallback } from '@tanstack/solid-pacer/debouncer' -import { For, createSignal } from 'solid-js' - -const features = tableFeatures({ - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, -}) -const columnHelper = createColumnHelper() - -function FirstNameFilter(props: { - column: Column -}) { - // Local input value updates immediately (good UX). - const [text, setText] = createSignal('') - - // Debounced commit to the table's filter state (expensive — runs after 250ms idle). - const commit = useDebouncedCallback( - (value: string) => props.column.setFilterValue(value), - { wait: 250 }, - ) - - return ( - { - const v = e.currentTarget.value - setText(v) - commit(v) - }} - placeholder="Filter first name..." - /> - ) -} - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - header: ({ header }) => , - cell: (info) => info.getValue(), - }), - // ... -]) - -function App() { - const [data] = createSignal(makeData(10_000)) - - const table = createTable({ - features, - columns, - get data() { - return data() - }, - }) - - return {/* ... */}
-} -``` - -The local `text()` signal gives the input its responsiveness; the debounced -`commit` keeps the row model from rebuilding on every keystroke. - -### Variant: debounce an external filter atom - -If you've lifted filters into a `@tanstack/solid-store` atom, debounce the -atom write instead: - -```tsx -import { createAtom } from '@tanstack/solid-store' - -const filtersAtom = createAtom([]) - -const commitFilter = useDebouncedCallback( - (id: string, value: string) => { - filtersAtom.set((old) => { - const others = old.filter((f) => f.id !== id) - return value ? [...others, { id, value }] : others - }) - }, - { wait: 250 }, -) -``` - -Combine with `manualFiltering: true` for server-driven filters — the debounce -also gates the network request. - -## Pattern 2 — Throttled column resize - -`columnResizingFeature` drives state updates while the user drags. With many -columns this can cause noticeable lag on slower machines. - -```tsx -import { useThrottledCallback } from '@tanstack/solid-pacer/throttler' - -const table = createTable({ - features: tableFeatures({ columnSizingFeature, columnResizingFeature }), - columns, - get data() { - return data() - }, - columnResizeMode: 'onChange', // or 'onEnd' for the simplest perf win -}) - -// Throttle programmatic columnSizing writes (e.g. when synced from a hot atom) -const setColumnSizing = useThrottledCallback( - (sizing: ColumnSizingState) => table.setColumnSizing(sizing), - { wait: 16 }, // ~60fps -) -``` - -If you can accept committing only at the end of the drag, just set -`columnResizeMode: 'onEnd'` — that's a free speedup without pacer at all. - -## When NOT to use pacer - -- **Pagination, sort.** Click-driven, low-frequency. No debounce needed. -- **Row selection.** Click-driven. No debounce. -- **Global filter that's already server-side and uses TanStack Query.** If the - query has its own `staleTime`/debounce hooked up upstream, double-throttling - hurts. -- **Tiny client-side tables.** A 100-row table re-filters in <1ms; debouncing - introduces a perceived lag for no measurable gain. - -## Failure modes - -### CRITICAL — debouncing the source of truth instead of the commit - -If you debounce the _input value_ signal itself, the input feels laggy -(characters trail). Debounce the **commit** (`setFilterValue` / atom set), not -the local input signal. Keep input echo immediate; throttle the work. - -### HIGH — recreating the debounced function per render - -Solid components don't re-render in the React sense, but if you call -`useDebouncedCallback` inside a `` body or a derived JSX expression you -may create new debouncers per row. Keep them at component scope, alongside -`createSignal`. - -### HIGH — debounce wait larger than user's expectation - -250ms is the typical sweet spot for filter inputs. 1000ms feels broken. -50ms gives almost no benefit. Tune to your dataset size. - -### MEDIUM — throttling the wrong layer for resize - -Setting `columnResizeMode: 'onEnd'` already accomplishes what most resize -throttling tries to do. Reach for `useThrottledCallback` only if you need -during-drag visual sync at a lower rate (e.g. virtualized layouts). - -### MEDIUM — debounce on a manual-pagination/query path adds to network jitter - -If `useQuery` is keyed on `filtersAtom` and you debounce the atom write, the -network request waits for the debounce. That's usually desirable, but be -explicit about the total latency budget (debounce + fetch). - -### LOW — using a debouncer where `untrack` is sufficient - -If your problem is "this computation runs too often inside an effect", reach -for Solid's `untrack` or restructure the effect first. Pacer is for -user-driven event streams, not for fixing Solid graph mistakes. diff --git a/packages/solid-table/skills/solid/compose-with-tanstack-query/SKILL.md b/packages/solid-table/skills/solid/compose-with-tanstack-query/SKILL.md deleted file mode 100644 index 184aa0f075..0000000000 --- a/packages/solid-table/skills/solid/compose-with-tanstack-query/SKILL.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -name: solid/compose-with-tanstack-query -description: > - Server-side data flow for `@tanstack/solid-table` with `@tanstack/solid-query`. - Lift pagination/sort/filter into atoms (`createAtom` + `useSelector`), key - `useQuery` on those accessors, use `keepPreviousData` to avoid the "0 rows - flash", set `manualPagination` (etc.) on the table, supply `data.rows` via a - reactive `get data()` getter, and feed `data.rowCount` via `get rowCount()`. -type: composition -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - solid/client-to-server - - pagination - - state-management -sources: - - examples/solid/with-tanstack-query/src/App.tsx - - examples/solid/with-tanstack-query/src/fetchData.ts - - docs/framework/solid/guide/table-state.md ---- - -# Compose with `@tanstack/solid-query` - -`@tanstack/solid-query` (`useQuery`, `keepPreviousData`) is the canonical async -fetcher for a server-driven Solid table. The Solid example -`examples/solid/with-tanstack-query/` is the reference pattern. - -## Install - -```bash -pnpm add @tanstack/solid-query @tanstack/solid-store -``` - -Wrap your app in `` once at the root. The table itself -never imports from `@tanstack/solid-query` — it just sees the rows. - -## Pattern - -```tsx -import { keepPreviousData, useQuery } from '@tanstack/solid-query' -import { createAtom, useSelector } from '@tanstack/solid-store' -import { - createTable, - createColumnHelper, - rowPaginationFeature, - tableFeatures, - FlexRender, - type PaginationState, -} from '@tanstack/solid-table' -import { For } from 'solid-js' - -const features = tableFeatures({ rowPaginationFeature }) -const columnHelper = createColumnHelper() -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First Name' }), - columnHelper.accessor('lastName', { header: 'Last Name' }), - columnHelper.accessor('age', { header: 'Age' }), -]) - -// Module-scope atom — every component that needs pagination subscribes here. -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) -const defaultRows: Array = [] - -function App() { - const pagination = useSelector(paginationAtom) - - // 1. Query keyed on the atom's accessor. Solid Query re-runs on changes. - const dataQuery = useQuery(() => ({ - queryKey: ['people', pagination()], - queryFn: () => fetchData(pagination()), - placeholderData: keepPreviousData, - })) - - // 2. Hand server data + rowCount to the table via getters. - const table = createTable({ - features, - columns, - get data() { - return dataQuery.data?.rows ?? defaultRows - }, - get rowCount() { - return dataQuery.data?.rowCount - }, - atoms: { pagination: paginationAtom }, - manualPagination: true, - }) - - return ( -
- - - - {(hg) => ( - - - {(h) => ( - - )} - - - )} - - - - - {(row) => ( - - - {(c) => ( - - )} - - - )} - - -
- -
- -
- - - - - Page {pagination().pageIndex + 1} of {table.getPageCount()} - - {dataQuery.isFetching ? Loading... : null} -
- ) -} -``` - -## Why each piece is the way it is - -- **`queryKey: ['people', pagination()]`** — the function-form `useQuery(() => ({ ... }))` is reactive. Calling `pagination()` inside it tracks the atom. When the user clicks "next page", the table writes to the atom, the query key changes, Solid Query fetches. -- **`placeholderData: keepPreviousData`** — without this, switching pages shows the loading state with zero rows, then "pops" to the next page. With it, the previous page stays visible until the new page resolves. -- **`atoms.pagination`** — sharing the atom between the query (read) and the table (read+write) is what wires the two together. No event bus, no `useEffect`. -- **`manualPagination: true`** — tells the table not to slice rows. The server already did. -- **No `paginatedRowModel`** — no factory needed for the manual slice. Do not register `paginatedRowModel` in `tableFeatures()` when `manualPagination: true`. -- **`rowCount`** — necessary so `table.getPageCount()`, `getCanNextPage()`, and `lastPage()` know the true total. Without it the table only sees one page of rows. - -## Adding sorting and filtering - -Same pattern. Lift each slice to its own atom, set the matching `manual*`, key -the query on every atom you depend on. - -```tsx -const sortingAtom = createAtom([]) -const filtersAtom = createAtom([]) -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) - -const sorting = useSelector(sortingAtom) -const filters = useSelector(filtersAtom) -const pagination = useSelector(paginationAtom) - -const dataQuery = useQuery(() => ({ - queryKey: [ - 'people', - { sorting: sorting(), filters: filters(), pagination: pagination() }, - ], - queryFn: () => - fetchData({ - sorting: sorting(), - filters: filters(), - pagination: pagination(), - }), - placeholderData: keepPreviousData, -})) - -const table = createTable({ - features: tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - }), - columns, - get data() { - return dataQuery.data?.rows ?? defaultRows - }, - get rowCount() { - return dataQuery.data?.rowCount - }, - atoms: { - sorting: sortingAtom, - columnFilters: filtersAtom, - pagination: paginationAtom, - }, - manualSorting: true, - manualFiltering: true, - manualPagination: true, -}) -``` - -## Mutations + cache invalidation - -```tsx -import { useMutation, useQueryClient } from '@tanstack/solid-query' - -const queryClient = useQueryClient() - -const deleteRow = useMutation(() => ({ - mutationFn: (id: string) => fetch(`/api/people/${id}`, { method: 'DELETE' }), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['people'] }), -})) -``` - -The table's atoms don't change; the query simply refetches with the same key. - -## Failure modes - -### CRITICAL — forgot `rowCount` - -Without `rowCount`, `table.getPageCount()` returns 1 (it only sees one page's -worth of rows). Pagination controls misbehave. - -### CRITICAL — forgot `manualPagination` (and friends) - -If you don't set `manualPagination: true`, the table will try to paginate the -already-paginated server response (slicing the single page further). Same trap -for `manualSorting` / `manualFiltering`. - -### HIGH — query key doesn't include the slices that drive the request - -If `queryKey: ['people']` is static but pagination changes, the cache returns -the same page forever. Always include every atom value (or the whole filter -object) in the key. - -### HIGH — `data: dataQuery.data?.rows ?? []` without a stable empty fallback - -A fresh `[]` on every read changes the `data` identity → row models recompute. -Module-scope `const EMPTY: Array = []` and use `?? EMPTY`. - -### HIGH — `placeholderData` left off - -Without `keepPreviousData`, every page change blanks the table while the next -page loads. Almost always a regression UX. - -### MEDIUM — reading `dataQuery.data?.rows` directly without the getter - -```tsx -// ❌ Snapshots once -createTable({ /* ... */ data: dataQuery.data?.rows ?? EMPTY }) - -// ✅ -createTable({ - /* ... */ get data() { - return dataQuery.data?.rows ?? EMPTY - }, -}) -``` - -### MEDIUM — `autoResetPageIndex` on - -Default behavior resets `pageIndex` to 0 when `data` reference changes. With a -server-driven pagination atom you usually don't want that. - -```tsx -createTable({ /* ... */ autoResetPageIndex: false }) -``` - -### MEDIUM — calling `useQuery` with a static object - -`useQuery({...})` (static form) doesn't track Solid signals. Use the function -form: `useQuery(() => ({...}))`. This is a Solid Query API rule, not a table -issue, but it's the most common breakage. - -### LOW — `getSelectedRowModel` only walks loaded rows - -Under server-side pagination, the table only knows about the current page. -"Select all" across pages must be tracked separately (e.g. a "select-all-mode" -atom + a list of explicit exclusions). diff --git a/packages/solid-table/skills/solid/compose-with-tanstack-store/SKILL.md b/packages/solid-table/skills/solid/compose-with-tanstack-store/SKILL.md deleted file mode 100644 index 742749aeb1..0000000000 --- a/packages/solid-table/skills/solid/compose-with-tanstack-store/SKILL.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -name: solid/compose-with-tanstack-store -description: > - Use `@tanstack/solid-store` (`createAtom`, `useSelector`, `shallow`) with - `@tanstack/solid-table` v9. The table is built on TanStack Store: every state - slice is an atom. Three read surfaces — `table.atoms.` (per-slice - readonly memo), `table.store` (flat readonly), and `table.state()` (Solid - accessor from the selector). Own slices externally by passing - `atoms: { sorting: someAtom }`. -type: composition -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - state-management - - solid/table-state -sources: - - docs/framework/solid/guide/table-state.md - - packages/solid-table/src/createTable.ts - - packages/solid-table/src/reactivity.ts - - examples/solid/basic-external-atoms/ ---- - -# Compose with `@tanstack/solid-store` - -v9 is built on TanStack Store. The Solid adapter installs `solidReactivity` so -readonly atoms are `createMemo` and writable atoms are `createSignal` under the -hood. Everything Solid-Store offers — atoms, selectors, shallow comparison — -works directly with the table's state surfaces. - -## The three read surfaces - -| Surface | Shape | Use for | -| --------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -| `table.atoms.` | `ReadonlyAtom` (memo-backed). `.get()` for current value, `.subscribe(fn)` for raw subscription | Per-slice fine-grained reads, especially with `useSelector`. | -| `table.store` | Readonly flat store; `.state.` for current value | Snapshot reads, debugging, JSON-dumps. | -| `table.state()` | `Accessor` — call it | Solid-idiomatic reactive read of the selector projection. | - -`table.baseAtoms.` exists too — those are the writable internal signals. -Reach for them only if you genuinely need a low-level write; prefer the table's -feature APIs (`table.setSorting(...)`, `table.nextPage()`, etc.). - -## Reading slices reactively - -### Native: just read `table.state()` or `table.atoms..get()` in a tracked scope - -```tsx -// JSX — automatically reactive -Selected: {Object.keys(table.atoms.rowSelection.get()).length} -Page {table.state().pagination.pageIndex + 1} - -// createMemo — also tracked -const pageCount = createMemo(() => table.getPageCount()) -``` - -### `useSelector` from `@tanstack/solid-store` - -`useSelector` returns a Solid accessor with shallow equality by default. - -```tsx -import { useSelector } from '@tanstack/solid-store' - -const pagination = useSelector(table.atoms.pagination) -// pagination is Accessor -const pageIndex = () => pagination().pageIndex -``` - -Narrow further with a selector + optional `shallow`: - -```tsx -import { shallow, useSelector } from '@tanstack/solid-store' - -const visibleColumnIds = useSelector( - table.store, - (s) => - Object.keys(s.columnVisibility ?? {}).filter( - (id) => s.columnVisibility[id] !== false, - ), - { compare: shallow }, -) -``` - -### `table.Subscribe` - -When you want an explicit isolation boundary: - -```tsx - s.rowSelection}> - {(rowSelection) => ( - - )} - -``` - -With a `source`: - -```tsx - !!rs[row.id]} -> - {(isSelected) => ( - - )} - -``` - -## Owning a slice externally with `createAtom` - -This is the recommended cross-component / cross-app pattern in v9. - -```tsx -import { createAtom, useSelector } from '@tanstack/solid-store' -import { - createTable, - type PaginationState, - type SortingState, -} from '@tanstack/solid-table' - -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) -const sortingAtom = createAtom([]) - -const pagination = useSelector(paginationAtom) -const sorting = useSelector(sortingAtom) - -const table = createTable({ - features, - columns, - get data() { - return data() - }, - atoms: { - pagination: paginationAtom, - sorting: sortingAtom, - }, -}) -``` - -When `atoms.` is provided: - -- The table reads/writes the **external** atom for that slice. -- `table.atoms.` is a readonly derived view over the external atom. -- Don't also supply `state.` + `on[State]Change` for the same slice; - the atom wins. -- `table.setSorting(...)` etc. still work — they call through to the external - atom's setter. - -### Mutating the atom directly - -You don't need `table.setSorting(...)` to update — write to the atom from -anywhere: - -```tsx -paginationAtom.set((old) => ({ ...old, pageIndex: 0 })) -sortingAtom.set([{ id: 'lastName', desc: true }]) -``` - -This is what makes external atoms valuable: a "Clear filters" button in the -page header or a URL-sync effect can talk to the same atom without holding a -reference to the table. - -## Pattern: per-slice readers in other components - -```tsx -// pagination.ts -import { createAtom, useSelector } from '@tanstack/solid-store' -export const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) -export const usePagination = () => useSelector(paginationAtom) - -// PageStatus.tsx — no `table` reference at all -import { usePagination } from './pagination' -export function PageStatus() { - const pagination = usePagination() - return Page {pagination().pageIndex + 1} -} - -// UsersTable.tsx -const table = createTable({ - features, - columns, - get data() { - return data() - }, - atoms: { pagination: paginationAtom }, -}) -``` - -## When to use atoms vs. `state`+`on*Change` - -| Use atoms when | Use `state`+`on*Change` when | -| -------------------------------------- | ----------------------------------------------------- | -| Sharing the slice across components | One component owns the slice | -| Driving a server fetcher (Query) | Local UI-only state | -| Syncing with URL params / storage | Migrating from v8 | -| You want one source of truth per slice | You want a Solid signal pattern with explicit setters | - -Both work. Atoms are more atomic; `state`+`on*Change` is more familiar. - -## Failure modes - -### CRITICAL — `table.state` treated as a value - -`table.state` is an Accessor in Solid. Call it. Same caveat applies if you used -`useSelector(table.atoms.pagination)` — the return is also an accessor. - -### CRITICAL — supplying both `atoms.pagination` and `state.pagination` - -Both surfaces compete for the same slice. The atom silently wins; the `state` +`on*Change` pair is ignored. Pick one. - -### HIGH — re-creating atoms in render - -`createAtom(...)` inside a component creates a new atom every call. Atoms must -be module-scoped (or memoized at component creation, never inside a JSX render -expression). - -### MEDIUM — passing `table.store` to `useSelector` without a selector - -`useSelector(table.store)` works but subscribes to the whole flat store — -shallow-equality on a big object is wasted work. Pass a selector to narrow. - -### MEDIUM — confusing `table.atoms` with `table.baseAtoms` - -`table.atoms.` is the **readonly** outward-facing atom (memo-backed) — -even when an external atom is supplied, this is what consumers read. -`table.baseAtoms.` is the internal writable signal used when the table -owns the slice. Writes should go through feature APIs or the external atom you -own; not through `baseAtoms` unless you have a specific reason. diff --git a/packages/solid-table/skills/solid/compose-with-tanstack-virtual/SKILL.md b/packages/solid-table/skills/solid/compose-with-tanstack-virtual/SKILL.md deleted file mode 100644 index edda032029..0000000000 --- a/packages/solid-table/skills/solid/compose-with-tanstack-virtual/SKILL.md +++ /dev/null @@ -1,288 +0,0 @@ ---- -name: solid/compose-with-tanstack-virtual -description: > - Virtualize a `@tanstack/solid-table` with `@tanstack/solid-virtual`'s - `createVirtualizer`. Take rows from `table.getRowModel().rows`, feed - `() => rows().length` as a reactive `count`, render only the visible rows - inside `` with absolute positioning + a `translateY` per row. Keep - the virtualizer in the same component as the scroll container ref. -type: composition -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - solid/table-state - - row-expanding -sources: - - docs/guide/virtualization.md - - examples/solid/virtualized-rows/src/App.tsx - - examples/solid/virtualized-columns/ - - examples/solid/virtualized-infinite-scrolling/ ---- - -# Compose with `@tanstack/solid-virtual` - -TanStack Table does **not** include virtualization. For >>visible-row datasets, -pair with `@tanstack/solid-virtual`'s `createVirtualizer`. The table still -manages every row model (sort, filter, group, expand); the virtualizer only -decides which rows to **paint**. - -## Install - -```bash -pnpm add @tanstack/solid-virtual -``` - -## Reference pattern (dynamic row height) - -```tsx -import { - createTable, - rowSortingFeature, - createSortedRowModel, - sortFns, - columnSizingFeature, - tableFeatures, - FlexRender, - type Row, - type SolidTable, -} from '@tanstack/solid-table' -import { - createVirtualizer, - type VirtualItem, - type Virtualizer, -} from '@tanstack/solid-virtual' -import { For, createSignal } from 'solid-js' - -const features = tableFeatures({ - columnSizingFeature, - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -function App() { - const [data] = createSignal(makeData(200_000)) - - const table = createTable({ - features, - columns, - get data() { - return data() - }, - }) - - return -} - -// Keep the virtualizer + scroll container ref in the SAME component. -function VirtualizedTable(props: { - table: SolidTable -}) { - let tableContainerRef: HTMLDivElement | undefined - - const rows = () => props.table.getRowModel().rows - - const rowVirtualizer = createVirtualizer( - { - get count() { - return rows().length - }, - estimateSize: () => 33, - getScrollElement: () => tableContainerRef ?? null, - // Optional: measure for dynamic row height - measureElement: - typeof window !== 'undefined' && - navigator.userAgent.indexOf('Firefox') === -1 - ? (el) => el.getBoundingClientRect().height - : undefined, - overscan: 5, - }, - ) - - return ( -
- - - - {(hg) => ( - - - {(header) => ( - - )} - - - )} - - - - - {(virtualRow) => ( - - )} - - -
- -
-
- ) -} - -function TableBodyRow(props: { - row: Row - virtualRow: VirtualItem - rowVirtualizer: Virtualizer - table: SolidTable -}) { - return ( - props.rowVirtualizer.measureElement(node)} - style={{ - display: 'flex', - position: 'absolute', - transform: `translateY(${props.virtualRow.start}px)`, - width: '100%', - }} - > - - {(cell) => ( - - - - )} - - - ) -} -``` - -## Why the structure looks like this - -- **`display: grid` / `display: flex` on `table`/`thead`/`tbody`/`tr`.** A - classic semantic `` cannot do absolute-positioned rows. CSS grid + - flex preserves the tag tree while letting the browser absolutely position - rows inside the body. -- **`height: rowVirtualizer.getTotalSize()` on ``.** Makes the - scrollbar correctly sized for the **virtual** dataset, not just the painted - rows. -- **`translateY(virtualRow.start)` per row.** Each rendered row jumps to its - virtual position. -- **`ref={(node) => rowVirtualizer.measureElement(node)}`** on each row. - Required for dynamic heights. If your rows are fixed-height, you can drop - `measureElement` and the ref. - -## Reactive `count` is critical - -```tsx -// ❌ Captures length once -createVirtualizer({ count: rows().length /* ... */ }) - -// ✅ Reactive — virtualizer re-derives when rows() changes -createVirtualizer({ - get count() { - return rows().length - } /* ... */, -}) -``` - -Same applies to `getScrollElement: () => tableContainerRef ?? null` (function), -not `getScrollElement: tableContainerRef` (snapshot). - -## Component scoping - -Keep `createVirtualizer` in the lowest component that owns the scroll -container. Putting it high in the tree means every scroll event recomputes -ancestor components. - -The example pulls `TableBodyRow` out into its own component for the same -reason — narrow re-render boundary. - -## Virtualized columns - -Same idea for wide tables: virtualize across `table.getVisibleLeafColumns()`. -See `examples/solid/virtualized-columns/`. The pattern is identical: -`count = columns.length`, `estimateSize = (i) => columns[i].getSize()`, -position cells with `translateX`. - -## Combining with `row-expanding` - -When `rowExpandingFeature` is registered and a row has subrows, the row count -the virtualizer sees is the **flattened** count -(`table.getRowModel().rows.length`, which already includes expanded subrows). -You don't need to compute that manually. - -For variable subrow heights, keep `measureElement` so the virtualizer -remeasures after expand/collapse. - -## Failure modes - -### CRITICAL — virtualizer scoped above the scroll container - -If `createVirtualizer` lives in a parent component, the ref-as-`undefined` -trick in the example only works because Solid evaluates the ref binding before -the JSX returns. Moving it up breaks that. Keep them together. - -### CRITICAL — `count` not reactive - -`count: rows().length` reads once. Use `get count() { return rows().length }`. -Same for `getScrollElement` — it must be a function. - -### HIGH — missing `height: rowVirtualizer.getTotalSize()` on `` - -Without it, the scrollbar is sized only for the painted rows. User scrolls -once and hits the bottom of "the table" even though 99% of rows are off-screen. - -### HIGH — virtualized rows with `display: table-row` - -A semantic `` with `display: table-row` cannot be absolutely positioned. -Use `display: flex` on `` and `display: grid` on `` (as shown). - -### MEDIUM — `measureElement` left enabled in Firefox - -Firefox measures table border heights incorrectly. The example guards this -with a userAgent check — keep that guard for cross-browser correctness. - -### MEDIUM — over-eager `overscan` - -Default `overscan` of 5 is usually fine. Cranking it to 50 paints more rows -than necessary; cranking it to 0 causes blank flashes at scroll boundaries. - -### MEDIUM — reading `table.state()` inside the virtualized row - -Each painted row that calls `table.state()` subscribes to the selected state. -If your row only needs `column.getSize()` and `cell.getValue()`, don't read -state at all — they are already tracking their own atoms. - -### LOW — column sizing without `columnSizingFeature` registered - -The example uses `header.getSize()` and `cell.column.getSize()`, which require -`columnSizingFeature` in `features`. Without it those APIs are missing. diff --git a/packages/solid-table/skills/solid/getting-started/SKILL.md b/packages/solid-table/skills/solid/getting-started/SKILL.md deleted file mode 100644 index e462ac7b05..0000000000 --- a/packages/solid-table/skills/solid/getting-started/SKILL.md +++ /dev/null @@ -1,276 +0,0 @@ ---- -name: solid/getting-started -description: > - End-to-end first table with `@tanstack/solid-table` v9. Install, declare - `features` via `tableFeatures()` (including row model factories such as - `sortedRowModel: createSortedRowModel()` and the matching `sortFns` slot), - create a column helper with `createColumnHelper()`, - build the table with `createTable(options)` using reactive `get data() {...}` - getters, and render rows via `FlexRender` (or `table.FlexRender`). -type: lifecycle -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - setup - - column-definitions - - state-management - - solid/table-state -sources: - - docs/installation.md - - docs/framework/solid/solid-table.md - - docs/framework/solid/guide/table-state.md - - packages/solid-table/src/createTable.ts - - examples/solid/basic-use-table/ - - examples/solid/basic-app-table/ ---- - -# Getting Started — `@tanstack/solid-table` - -A working Solid table from a clean install. The five steps below cover every -concept you will need before reaching for feature-specific skills. - -## 1. Install - -```bash -pnpm add @tanstack/solid-table -# or -npm install @tanstack/solid-table -``` - -`@tanstack/solid-table` re-exports everything from `@tanstack/table-core` plus -the Solid-specific `createTable`, `FlexRender`, and `createTableHook`. You -should not install `@tanstack/table-core` separately. - -## 2. Declare features (`features`) - -v9 is explicit about what a table uses. Only registered features expose APIs -and state slices. This is what makes the bundle tree-shake. - -```tsx -import { - tableFeatures, - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, -} from '@tanstack/solid-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, -}) -``` - -`tableFeatures()` is **essential** — it produces a stable typed `TFeatures` -object used everywhere (column helper, table options, etc.). Use it even for a -no-feature table: `tableFeatures({})`. - -## 3. Register row-model factories in `tableFeatures()` - -Each non-core row-model feature needs its factory registered inside `tableFeatures()`. -The core row model is included by default. Pass the matching `*Fns` registry as -a slot so it is tree-shakeable too. - -```tsx -import { - tableFeatures, - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - createPaginatedRowModel, - createSortedRowModel, - createFilteredRowModel, - sortFns, - filterFns, -} from '@tanstack/solid-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - sortFns, - filterFns, -}) -``` - -You may also pass a narrowed fns object like `sortFns: { alphanumeric: sortFns.alphanumeric }` -instead of the full registry. - -## 4. Define columns - -`createColumnHelper` takes **both** generics: `typeof features` first, then -`TData`. (This is the v9 ordering. v8 only had `TData`.) - -```tsx -import { createColumnHelper } from '@tanstack/solid-table' - -type Person = { firstName: string; lastName: string; age: number } - -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First Name', - cell: (info) => info.getValue(), - }), - columnHelper.accessor((row) => row.lastName, { - id: 'lastName', - header: () => Last Name, - }), - columnHelper.accessor('age', { header: 'Age' }), -]) -``` - -`columnHelper.columns([...])` preserves each column's individual `TValue` type. -Prefer it over a bare array. - -## 5. Create the table and render - -```tsx -import { createTable, FlexRender } from '@tanstack/solid-table' -import { For, createSignal } from 'solid-js' - -function App() { - const [data, setData] = createSignal>([ - { firstName: 'tanner', lastName: 'linsley', age: 24 }, - { firstName: 'kevin', lastName: 'vandy', age: 12 }, - ]) - - const table = createTable({ - features, - columns, - get data() { - return data() // reactive getter - }, - }) - - return ( -
- - - {(hg) => ( - - - {(header) => ( - - )} - - - )} - - - - - {(row) => ( - - - {(cell) => ( - - )} - - - )} - - -
- {header.isPlaceholder ? null : ( - - )} -
- -
- ) -} -``` - -That's a complete v9 Solid table. - -## Alternative entry point: `createTableHook` - -If you have more than one table sharing features and row models, declare them -once with `createTableHook`: - -```tsx -import { createTableHook } from '@tanstack/solid-table' - -const { createAppTable, createAppColumnHelper } = createTableHook({ - features: tableFeatures({}), -}) - -const columnHelper = createAppColumnHelper() - -function App(props: { data: Array }) { - const table = createAppTable({ - columns, - get data() { - return props.data - }, - }) - // ... -} -``` - -`createAppTable` returns a normal `SolidTable` plus app-wrapper components -(`AppTable`, `AppCell`, `AppHeader`, `AppFooter`). See the table-state skill. - -## Common pitfalls - -### Forgetting `get` on reactive options - -```tsx -// ❌ Reads once at construction — table never updates when data() changes -createTable({ features, columns, data: data() }) - -// ✅ Tracked -createTable({ - features, - columns, - get data() { - return data() - }, -}) -``` - -Use getters for any option that depends on a reactive source: `data`, dynamic -`columns`, controlled `state` slices, `rowCount`, etc. - -### Calling `table.state` as a value - -`table.state` is an Accessor (a function). Always call it: - -```tsx -// ❌ -table.state.pagination - -// ✅ -table.state().pagination -``` - -### Missing feature → missing API - -If you write `table.setSorting(...)` without `rowSortingFeature` in `features`, -TS errors and the method is undefined at runtime. The fix is registration, not -a cast. - -### Bundling `stockFeatures` defeats the v9 tree-shake - -Don't import everything. Register only the features you use. A no-feature table -is `tableFeatures({})` — not `stockFeatures`. - -### Reimplementing built-ins - -v9 already exposes `table.setSorting`, `table.nextPage`, `column.toggleVisibility`, -`row.toggleSelected`, `column.setFilterValue`, etc. Reach for the API before -rolling your own state update. - -### Hallucinated names from older versions - -v9 is `createTable`, not `createSolidTable` (that was v8). Row model factories -go inside `tableFeatures()`, not as top-level `getCoreRowModel` / `getSortedRowModel` -options and not in a separate `rowModels` option. -`createColumnHelper` takes `` (two generics, features -first). diff --git a/packages/solid-table/skills/solid/migrate-v8-to-v9/SKILL.md b/packages/solid-table/skills/solid/migrate-v8-to-v9/SKILL.md deleted file mode 100644 index 914a1713ac..0000000000 --- a/packages/solid-table/skills/solid/migrate-v8-to-v9/SKILL.md +++ /dev/null @@ -1,324 +0,0 @@ ---- -name: solid/migrate-v8-to-v9 -description: > - Mechanical breaking-change migration from `@tanstack/solid-table` v8 to v9. - Renames (`createSolidTable` → `createTable`, `getCoreRowModel`/`getSortedRowModel`/... - → row model factories registered inside `tableFeatures()`), new required `features` - registration via `tableFeatures()`, two-generic `createColumnHelper`, - the v9 atom state model, and the lack of a `/legacy` entrypoint for Solid - (full rewrite, no `useLegacyTable`). -type: lifecycle -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - column-definitions -sources: - - docs/framework/react/guide/migrating.md - - docs/framework/solid/solid-table.md - - docs/framework/solid/guide/table-state.md - - packages/solid-table/src/createTable.ts ---- - -# Migrate v8 → v9 for `@tanstack/solid-table` - -The Solid adapter has **no `/legacy` entrypoint**. Unlike React (which ships -`useLegacyTable` from `@tanstack/react-table/legacy`), Solid migrations are a -direct rewrite. Plan to do it incrementally per table, not per file. - -## What changed (high-level) - -1. **Adapter API renamed.** `createSolidTable(...)` → `createTable(...)`. -2. **Features must be registered.** v9 introduced `features` via `tableFeatures({...})`. Without it, feature APIs and state slices don't exist (TS error + runtime undefined). -3. **Row model factories moved into `tableFeatures()`.** No more top-level `getCoreRowModel: getCoreRowModel()` options; instead pass factories like `paginatedRowModel: createPaginatedRowModel()` directly inside `tableFeatures({...})`. -4. **State is atom-based.** Powered by TanStack Store. The classic `state`+`on*Change` pattern still works for compatibility, but `atoms` is the new recommended hand-off for per-slice external ownership. -5. **`createColumnHelper` takes two generics.** `createColumnHelper()` (features first). -6. **Some method/option renames.** Most notably `sortingFn` → `sortFn`; the `*Fns` registries (`sortFns`, `filterFns`, `aggregationFns`) are now registered as slots inside `tableFeatures()`. -7. **No `onStateChange` top-level callback.** Use per-slice `on[State]Change` paired with `state.`, or use `atoms`. - -## Rename map - -| v8 (Solid) | v9 (Solid) | -| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `createSolidTable(options)` | `createTable(options, selector?)` | -| `getCoreRowModel: getCoreRowModel()` | (core row model included by default; no option needed) | -| `getSortedRowModel: getSortedRowModel()` | `tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns })` | -| `getFilteredRowModel: getFilteredRowModel()` | `tableFeatures({ columnFilteringFeature, filteredRowModel: createFilteredRowModel(), filterFns })` | -| `getPaginationRowModel: getPaginationRowModel()` | `tableFeatures({ rowPaginationFeature, paginatedRowModel: createPaginatedRowModel() })` | -| `getGroupedRowModel: getGroupedRowModel()` | `tableFeatures({ columnGroupingFeature, groupedRowModel: createGroupedRowModel(), aggregationFns })` | -| `getExpandedRowModel: getExpandedRowModel()` | `tableFeatures({ rowExpandingFeature, expandedRowModel: createExpandedRowModel() })` | -| `getFacetedRowModel` / `getFacetedUniqueValues` / `getFacetedMinMaxValues` | `tableFeatures({ ...facetingFeatures, facetedRowModel, facetedUniqueValues, facetedMinMaxValues })` | -| `createColumnHelper()` | `createColumnHelper()` | -| `sortingFn: 'alphanumeric'` on a column | `sortFn: 'alphanumeric'` on a column | -| `onStateChange` (whole-state) | (gone — use per-slice `on*Change` or `atoms`) | -| `state: { ... }` + `onStateChange` | `state: { ... }` + `on[State]Change`, OR `atoms: { ... }` | -| (no atoms) | `atoms: { sorting: someAtom, pagination: someAtom, ... }` | - -## Before → after - -### v8 Solid table - -```tsx -import { - createSolidTable, - getCoreRowModel, - getSortedRowModel, - getPaginationRowModel, - createColumnHelper, - flexRender, - type SortingState, -} from '@tanstack/solid-table' -import { For, createSignal } from 'solid-js' - -const columnHelper = createColumnHelper() - -const columns = [ - columnHelper.accessor('firstName', { header: 'First Name' }), - columnHelper.accessor('age', { header: 'Age', sortingFn: 'alphanumeric' }), -] - -function App(props: { data: Person[] }) { - const [sorting, setSorting] = createSignal([]) - - const table = createSolidTable({ - get data() { - return props.data - }, - columns, - state: { - get sorting() { - return sorting() - }, - }, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - }) - - return /* ... flexRender(header.column.columnDef.header, header.getContext()) ... */ -} -``` - -### v9 equivalent - -```tsx -import { - createTable, - FlexRender, - createColumnHelper, - createSortedRowModel, - createPaginatedRowModel, - rowPaginationFeature, - rowSortingFeature, - sortFns, - tableFeatures, - type SortingState, -} from '@tanstack/solid-table' -import { For, createSignal } from 'solid-js' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, -}) - -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First Name' }), - columnHelper.accessor('age', { header: 'Age', sortFn: 'alphanumeric' }), -]) - -function App(props: { data: Person[] }) { - const [sorting, setSorting] = createSignal([]) - - const table = createTable({ - features, - columns, - get data() { - return props.data - }, - state: { - get sorting() { - return sorting() - }, - }, - onSortingChange: setSorting, - }) - - // FlexRender component replaces calls to flexRender(def, ctx) - return ( - - {(hg) => ( - - {(header) => ( - - - - )} - - )} - - ) -} -``` - -## Reading state — `table.state()` is now an accessor - -In Solid v9, the `table.state` returned by `createTable` is an **Accessor** — -call it. If you migrated from a non-Solid v8 pattern, double-check this. - -```tsx -// v8 muscle memory: -table.getState().sorting - -// v9 Solid: -table.state().sorting -``` - -`table.getState()` still exists on the table-core surface for parity, but -`table.state()` (the accessor) is the Solid-idiomatic read. - -## State ownership choices in v9 - -- **Default**: Pass `initialState`. Table owns everything. -- **Per-slice signal**: Use `state` getters + `on[State]Change` setters (works with `createSignal`). -- **Per-slice external atom (recommended for cross-component / server-driven)**: - - ```tsx - import { createAtom } from '@tanstack/solid-store' - - const sortingAtom = createAtom([]) - - createTable({ - features, - columns, - get data() { - return data() - }, - atoms: { sorting: sortingAtom }, - }) - ``` - - External atoms take precedence over `state`. Don't supply both for the same slice. - -## Filter / sort / aggregation `*Fns` - -v9 separates the registry (what comparator functions are bundled) from the row -model factory. Register the `*Fns` maps as slots in `tableFeatures()` alongside -the factory, so they are tree-shakeable. - -```tsx -import { - sortFns, - filterFns, - aggregationFns, - createSortedRowModel, - createFilteredRowModel, - createGroupedRowModel, - tableFeatures, -} from '@tanstack/solid-table' - -// All sort fns -tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -// Narrowed — only the comparators you use -tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns: { alphanumeric: sortFns.alphanumeric }, -}) - -tableFeatures({ - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, -}) -tableFeatures({ - columnGroupingFeature, - groupedRowModel: createGroupedRowModel(), - aggregationFns, -}) -``` - -On columns, the option name is now `sortFn`, `filterFn`, `aggregationFn` -(singular), not `sortingFn` / `filteringFn`. - -## What does NOT change - -- `` style rendering. -- `accessorKey` / `accessorFn` shapes. -- `header`, `cell`, `footer` definitions (modulo using `FlexRender` instead of raw `flexRender`). -- Reactive getters for `data` (you should already be doing this). -- Feature APIs like `table.nextPage`, `column.toggleSorting`, `row.toggleSelected`. - -## Failure modes - -### CRITICAL — `features` missing → API gone - -The single biggest v8→v9 trap. `table.setSorting` won't exist unless -`rowSortingFeature` is in `features`. Likewise for every other feature. v8 had -no such requirement. - -### CRITICAL — `getCoreRowModel: getCoreRowModel()` pattern - -v9 has no such option. The core row model is included by default. Move every -`get*RowModel` option to a factory slot inside `tableFeatures()` — call the -matching `create*RowModel()` factory and register it alongside the feature. -Don't leave the v8 options in place. - -### CRITICAL — `createColumnHelper()` missing the features generic - -v8: `createColumnHelper()`. v9: `createColumnHelper()`. -Forgetting the first generic gives bad inference and broken cell typing. - -### HIGH — `createSolidTable` no longer exists - -The function is named `createTable` in Solid v9. (`createSolidTable` was a v8 -spelling and is gone.) There is no `useReactTable` analogue on Solid; it's just -`createTable`. - -### HIGH — `sortingFn` / `filteringFn` rename - -Column-level `sortingFn` is now `sortFn`. Filter is `filterFn`. Aggregation is -`aggregationFn`. Old names typecheck-fail. - -### HIGH — top-level `onStateChange` - -There is no whole-state `onStateChange` in v9. Migrate to per-slice -`on[State]Change` paired with `state.`, or move to `atoms`. - -### MEDIUM — `state` shape without getters - -v8 worked with `state: { sorting: sorting() }` if `sorting()` was read in a -tracked scope. v9 still needs reactive getters: `state: { get sorting() { return sorting() } }`. -Plain values disconnect reactivity. - -### MEDIUM — no `/legacy` entrypoint for Solid - -If you're searching for `@tanstack/solid-table/legacy` or `useLegacyTable` (the -React fast-path migration), they don't exist on Solid. There is no escape hatch -that preserves the v8 shape — migrate to v9 directly. - -### MEDIUM — `flexRender(def, ctx)` calls - -Still works, but use the `FlexRender` component for cleaner JSX: - -```tsx -// Old -{ - flexRender(cell.column.columnDef.cell, cell.getContext()) -} - -// New -; -``` diff --git a/packages/solid-table/skills/solid/production-readiness/SKILL.md b/packages/solid-table/skills/solid/production-readiness/SKILL.md deleted file mode 100644 index 813fd5b0de..0000000000 --- a/packages/solid-table/skills/solid/production-readiness/SKILL.md +++ /dev/null @@ -1,259 +0,0 @@ ---- -name: solid/production-readiness -description: > - Ship-ready optimizations for `@tanstack/solid-table` v9. Tree-shake by - registering only the `features` you use; keep `features`, `columns`, `data` - stable; prefer per-slice external atoms (`createAtom` + `useSelector`) and - narrow selectors over `(state) => state`; leverage Solid's fine-grained - reactivity (`createMemo`, JSX-level reads) so most components subscribe to - exactly the slice they render; reach for `table.Subscribe` only at coarse - isolation boundaries. -type: lifecycle -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - solid/table-state -sources: - - docs/guide/features.md - - docs/framework/solid/guide/table-state.md - - packages/solid-table/src/createTable.ts - - examples/solid/basic-external-atoms/ - - examples/solid/virtualized-rows/ ---- - -# Production Readiness — `@tanstack/solid-table` - -A v9 table that "works in dev" can still ship slow if you copied the -getting-started shape unchanged into a 10k-row scenario. Solid's fine-grained -reactivity rewards a few production habits. - -## 1. Tree-shake `features` aggressively - -v9's biggest bundle win. Register only the features you use. - -```tsx -// ❌ Pulls everything; defeats the v9 redesign -import { stockFeatures } from '@tanstack/solid-table' -tableFeatures(stockFeatures) - -// ✅ Only what you use -import { - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, -} from '@tanstack/solid-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, -}) -``` - -Same rule for the `*Fns` registries: register only the comparators you use as a `sortFns` slot on `tableFeatures`: - -```tsx -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - // Only the comparators you actually use - sortFns: { - alphanumeric: sortFns.alphanumeric, - basic: sortFns.basic, - }, -}) -``` - -## 2. Keep `features`, `data`, `columns` stable - -The Solid signal model already protects you from React-style re-creation, but -two patterns still break things: - -- **Re-creating `features` per render.** Module-scope it. Don't call - `tableFeatures({...})` inside a component. -- **Returning a fresh `[]` on every read.** When `data` is unloaded - (`resource()?.rows ?? []`), the fallback `[]` should be a module-scope - constant so identity is stable. - -```tsx -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const EMPTY_ROWS: Array = [] - -function App() { - const table = createTable({ - features, // stable — factories are registered on the features object - columns, // module-scope constant - get data() { - return query.data?.rows ?? EMPTY_ROWS - }, - }) -} -``` - -For `columns` that depend on a reactive source (e.g. a column visibility -preset), wrap them in `createMemo` so identity is stable per inputs: - -```tsx -const columns = createMemo(() => - columnHelper.columns([ - /* ... */ - ]), -) -createTable({ - features, - get columns() { - return columns() - }, - get data() { - return data() - }, -}) -``` - -## 3. Prefer narrow selectors over `(state) => state` - -The default `createTable(options)` selector is identity. Anything reading -`table.state()` then depends on every slice. Pass a selector: - -```tsx -const table = createTable( - { - features, - columns, - get data() { - return data() - }, - }, - (state) => ({ pagination: state.pagination, sorting: state.sorting }), -) -``` - -For `table.Subscribe` boundaries, the same rule applies — narrow the selector. - -## 4. Trust signal-level reactivity over Subscribe wrappers - -In Solid, a JSX-level read tracks itself. You usually don't need -`table.Subscribe`: - -```tsx -// This updates when pagination changes — fine-grained, no Subscribe needed -Page {table.state().pagination.pageIndex + 1} -``` - -Reach for `table.Subscribe` when: - -- You want an isolated re-render boundary for a large sub-tree. -- You need to subscribe to a single atom/store with `source` (e.g. a specific - `table.atoms.rowSelection` slice for a single row checkbox). - -Not when: - -- It's "what React does." Solid is fine without it most of the time. - -## 5. Use external atoms for cross-component / server-driven state - -`@tanstack/solid-store`'s `createAtom` + `useSelector` is the cleanest pattern -for sharing pagination/sort/filter with TanStack Query, URL params, devtools, -etc. - -```tsx -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 25, -}) - -// In the table: -createTable({ /* ... */ atoms: { pagination: paginationAtom } }) - -// In the URL sync hook elsewhere: -const pagination = useSelector(paginationAtom) -createEffect(() => syncUrl(pagination())) -``` - -This wins over `state`+`on*Change` because every consumer can subscribe to the -atom independently with shallow equality. - -## 6. Virtualize when row count >> visible rows - -TanStack Table does not include virtualization. For 10k+ row tables, pair with -`@tanstack/solid-virtual` (`createVirtualizer`). Keep the virtualizer ref in -the lowest possible component to avoid re-running it on unrelated updates. See -the `compose-with-tanstack-virtual` skill. - -## 7. Use APIs, not handwritten state edits - -The #1 AI tell in v9 code: reimplementing what's already there. Examples: - -| ❌ Handwritten | ✅ Built-in | -| ------------------------------------------------------------------ | ------------------------------------------------------ | -| `setPagination((p) => ({ ...p, pageIndex: p.pageIndex + 1 }))` | `table.nextPage()` | -| Recomputing `selectedRows` from `data` and a `rowSelection` object | `table.getSelectedRowModel().rows` | -| Building a "can sort this column?" predicate | `column.getCanSort()` | -| Manual range `setPageIndex(Math.min(...))` clamping | `table.setPageIndex(idx)` (table clamps internally) | -| Tracking expanded ids in a parallel structure | `row.toggleExpanded()` / `table.getExpandedRowModel()` | - -If you find yourself recomputing something the table tracks, look for the -matching API. - -## 8. Use `tableFeatures()` even if you only need one feature - -`tableFeatures()` is the essential, stable wiring for v9. Always go through it. -It's not the "experimental" part — that's only custom-feature **authoring**, -which is excluded from v9 alpha. - -## Failure modes - -### CRITICAL — registering features you don't use - -Every registered feature adds state slices, derivations, and code to the -bundle. Keep `features` minimal. - -### CRITICAL — recreating `features` / `columns` / `data` identity on every render - -Solid's reactivity assumes stable references for options that are not behind -getters. `features` and `columns` should be module-scoped; reactive options -should use getters; fallbacks should be module-scope constants. - -### HIGH — `(state) => state` default selector in a frequently-reading component - -If you pass no selector to `createTable` and you read `table.state()` in many -places, you've coupled every component to every slice. Narrow it. - -### HIGH — premature `table.Subscribe` on small tables - -`Subscribe` is for advanced isolation. A 50-row table doesn't need it. Native -Solid reactivity is already fine-grained. - -### HIGH — `stockFeatures` in production - -A clear "didn't think about the bundle" tell. Use only the features you render. - -### MEDIUM — virtualizer at the wrong scope - -Keep `createVirtualizer` in the component that owns the scroll container, not -high up in the tree. Otherwise scroll-driven recompute fires across the page. - -### MEDIUM — re-reading `table.state` in JSX - -Use `table.state()` for component-level reactive reads, or -`table.atoms.pagination.get()` / `useSelector(table.atoms.pagination)` for -per-slice reads. Avoid direct `table.state` reads in JSX. - -### MEDIUM — `autoResetPageIndex: true` on a server-driven table - -When `data` changes (a new server page arrives), the auto-reset can fight your -external atom. Set it to `false` for server-side tables. - -### LOW — measuring perf with `debugTable: true` left on - -`debugTable: true` is a development helper that logs row-model rebuilds. Turn -it off in production builds. diff --git a/packages/solid-table/skills/solid/table-state/SKILL.md b/packages/solid-table/skills/solid/table-state/SKILL.md deleted file mode 100644 index a7ff1be887..0000000000 --- a/packages/solid-table/skills/solid/table-state/SKILL.md +++ /dev/null @@ -1,426 +0,0 @@ ---- -name: solid/table-state -description: > - Reactivity, atom subscription, and rendering for `@tanstack/solid-table` v9. - Covers `createTable(options, selector?)`, the `table.state()` accessor (callable, not a value), - `table.Subscribe`, `FlexRender`, native `createSignal`/`createMemo` reactivity, - `solidReactivity` (readonly atoms = memos, writable atoms = signals), and - `@tanstack/solid-store` (`createAtom`, `useSelector`) for external slices. -type: framework -library: tanstack-table -framework: solid -library_version: '9.0.0-alpha.48' -requires: - - state-management - - setup -sources: - - docs/framework/solid/guide/table-state.md - - docs/framework/solid/solid-table.md - - packages/solid-table/src/createTable.ts - - packages/solid-table/src/reactivity.ts - - packages/solid-table/src/FlexRender.tsx - - packages/solid-table/src/createTableHook.tsx - - examples/solid/basic-use-table/ - - examples/solid/basic-external-atoms/ - - examples/solid/basic-external-state/ ---- - -# Solid Table State, Subscribe & createTableHook - -TanStack Table v9 is a state-management coordinator. The Solid adapter wires that -coordinator into Solid's fine-grained reactivity. Readonly atoms are backed by -`createMemo`. Writable atoms are backed by `createSignal`. Most Solid tables read -state directly through table APIs inside reactive scopes and never need -`table.Subscribe`. - -## Mental model - -A `createTable(...)` call produces a `SolidTable` with several state surfaces: - -- `table.baseAtoms.` — internal writable TanStack Store atoms. Treat these as write plumbing, not a render read surface. -- `table.atoms.` — readonly derived atoms (memos). One per registered feature slice. Use `table.atoms.pagination.get()` for slice-level reactive reads. -- `table.store` — flat readonly TanStack Store snapshot for explicit subscriptions. Prefer `table.state()` or `table.atoms..get()` in JSX. -- `table.state()` — **a Solid accessor**, not a value. Returns the result of the selector passed as the second argument to `createTable`. Default selector is identity. - -State slices only exist for features registered through `features`. If -`rowSortingFeature` is not in `features`, then `table.atoms.sorting`, -`table.state().sorting`, and `state.sorting` are all absent (TS error + missing at runtime). - -## Creating a table — native signals - -`createTable(options, selector?)`. Use getters on reactive options like `data` so -the table tracks the upstream signal. - -```tsx -import { createTable, tableFeatures, type ColumnDef } from '@tanstack/solid-table' -import { createSignal, For } from 'solid-js' - -const features = tableFeatures({}) - -function App() { - const [data, setData] = createSignal>([]) - - const table = createTable({ - features, - columns, - // Reactive getter — required so the table re-derives when data() changes. - get data() { - return data() - }, - }) - - return {(row) => /* ... */} -} -``` - -> Missing the getter on `data` is the most common Solid-specific bug. -> `data: data()` reads once at table construction; `get data() { return data() }` -> tracks the signal. - -## Reading state — `table.state()` is an accessor - -This is the **#1 Solid failure mode**: agents who learned React patterns try to -read `table.state.sorting`. In Solid, `table.state` is an `Accessor` — -it must be called. - -```tsx -// ❌ WRONG — `state` is the accessor function itself, not a value -table.state.sorting - -// ✅ CORRECT — call the accessor first, then read the slice -table.state().sorting -``` - -Pass a selector to narrow what `table.state()` returns: - -```tsx -const table = createTable( - { - features, - columns, - get data() { - return data() - }, - }, - (state) => ({ pagination: state.pagination }), -) - -// Reactive in JSX / createMemo / createEffect: -const pageIndex = () => table.state().pagination.pageIndex -``` - -`table.state()` is also reactive inside Solid computations — `createMemo`, -`createEffect`, JSX expressions, and `` all track it. - -## Three ways to read state, ranked - -1. **Native Solid reactive read.** Inside any tracking scope (JSX, `createMemo`, - `createEffect`), call any atom-driven API or `table.state()` directly. Solid - handles the dependency. - - ```tsx -
Page {table.state().pagination.pageIndex + 1}
- - ``` - -2. **Untracked current value.** Read `.get()` or the flat store outside a - tracking scope (e.g. inside a handler) when you only need a snapshot. - - ```tsx - const onClick = () => { - const current = table.atoms.pagination.get() - console.log(current.pageIndex) - } - ``` - -3. **`table.Subscribe`** — explicit subscription boundary. Less common on Solid - because signal-based reactivity handles most cases natively. Useful when you - want a sub-render isolated to one slice. - - ```tsx - s.rowSelection}> - {(rowSelection) => ( - Selected: {Object.keys(rowSelection()).length} - )} - - ``` - - Or with a `source` to subscribe to a single atom/store: - - ```tsx - !!rs[row.id]} - > - {(isSelected) => } - - ``` - - The child function receives a Solid `Accessor` — call it. - -## Setting state — use the feature APIs - -```tsx -table.setPageIndex(0) -table.nextPage() -table.setSorting([{ id: 'age', desc: true }]) -table.setColumnFilters((old) => [...old, { id: 'firstName', value: 'kev' }]) -row.toggleSelected() -column.toggleVisibility() -table.resetSorting() -``` - -Almost never reach for `table.baseAtoms..set(...)` directly. - -## State ownership: `initialState` vs `state`/`on*Change` vs `atoms` - -| Mode | When | -| ---------------------------- | ------------------------------------------------------------------------------------------------ | -| `initialState` only | You only want starting values; table owns state. | -| `state` + `on[State]Change` | Migrating v8 code, or simple Solid signal integration. | -| `atoms: { slice: someAtom }` | App owns the slice. Best for sharing pagination/sort/filter across components (e.g. with Query). | - -External atoms take precedence over `state`. Don't mix them on the same slice. - -### `state` + `on*Change` (signal-backed external state) - -```tsx -const [sorting, setSorting] = createSignal([]) -const [pagination, setPagination] = createSignal({ - pageIndex: 0, - pageSize: 10, -}) - -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, -}) - -const table = createTable({ - features, - columns, - get data() { - return data() - }, - state: { - get sorting() { - return sorting() - }, - get pagination() { - return pagination() - }, - }, - onSortingChange: setSorting, - onPaginationChange: setPagination, -}) -``` - -> Getters in `state` are required. `state: { sorting: sorting() }` evaluates once. - -### External atoms (recommended for shared state) - -```tsx -import { createAtom, useSelector } from '@tanstack/solid-store' - -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) -const sortingAtom = createAtom([]) - -const pagination = useSelector(paginationAtom) // Accessor - -const table = createTable({ - features, - columns, - get data() { - return data() - }, - atoms: { - sorting: sortingAtom, - pagination: paginationAtom, - }, -}) -``` - -Pair with `@tanstack/solid-store`'s `useSelector` anywhere else in the app to -read the same atom. - -## Rendering headers, cells, and footers - -`FlexRender` (top-level) handles plain values and Solid components. `table.FlexRender` -is the same component attached to the instance: - -```tsx -import { FlexRender } from '@tanstack/solid-table' - - - - {(hg) => ( - - - {(header) => ( - {header.isPlaceholder ? null : } - )} - - - )} - - - - - {(row) => ( - - - {(cell) => } - - - )} - - -``` - -`FlexRender` automatically handles grouped/aggregated/placeholder cells when the -column-grouping feature is registered. - -## `createTableHook` — app-level table conventions - -When multiple tables share `features`, default options, and component -conventions, use `createTableHook` to register them once. - -```tsx -import { - createTableHook, - tableFeatures, - rowPaginationFeature, - createPaginatedRowModel, -} from '@tanstack/solid-table' - -const { - createAppTable, - createAppColumnHelper, - useTableContext, - useCellContext, - useHeaderContext, -} = createTableHook({ - features: tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), - }), - tableComponents: { PaginationControls }, - cellComponents: { TextCell, NumberCell }, - headerComponents: { SortIndicator }, -}) - -const columnHelper = createAppColumnHelper() - -function UsersTable(props: { data: Array }) { - const table = createAppTable({ - columns, - get data() { - return props.data - }, - }) - - return ( - - - - - {(hg) => ( - - - {(h) => ( - - {(header) => ( - - )} - - )} - - - )} - - - - - {(row) => ( - - - {(c) => ( - - {(cell) => ( - - )} - - )} - - - )} - - -
- - -
- -
- -
- ) -} - -function PaginationControls() { - const table = useTableContext() // SolidTable - return ( -
- - Page {table.state().pagination.pageIndex + 1} - -
- ) -} -``` - -Use plain `createTable` when one feature config doesn't cover most of your -tables. Use `createTableHook` when it does. - -## How the Solid binding works (reference) - -`solidReactivity(owner)` is installed automatically by `createTable` as -`coreReactivityFeature`. Don't pass your own. - -- `createReadonlyAtom(fn)` → `createMemo(fn, { equals, name })` -- `createWritableAtom(value)` → `createSignal(value, { equals, name })` -- Each atom exposes `.get()`, `.subscribe()`, and (for writable) `.set()`. -- Subscriptions run with the captured owner so atoms can be read inside Solid - computations safely. - -## Failure modes - -### CRITICAL — `table.state` vs `table.state()` - -In Solid, `table.state` is an **accessor** (a function). Agents who copied React -patterns will write `table.state.sorting` and get `undefined`. Always call it: -`table.state().sorting`. Same for any selected sub-state. - -### CRITICAL — feature not registered → API missing - -If you reach for `table.atoms.sorting`, `table.setSorting`, `column.getCanSort`, -etc., the matching feature (`rowSortingFeature`) must be in `features`. -Otherwise TS errors and runtime `undefined`. v9 features are explicit. - -### CRITICAL — reactive `data` without a getter - -`data: someSignal()` reads once at construction. `get data() { return someSignal() }` -tracks. The same applies to any reactive option (`columns` when computed, -`state.sorting`, etc.). When in doubt: getter. - -### HIGH — wrong API name from a previous version - -There is no `createSolidTable` (v8 name). The Solid v9 API is `createTable`. -There is no `getCoreRowModel`/`getSortedRowModel` factory option pattern — pass -row model factories directly into `tableFeatures()` (e.g. `tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns })`). diff --git a/packages/solid-table/skills/table-state/SKILL.md b/packages/solid-table/skills/table-state/SKILL.md new file mode 100644 index 0000000000..80d02cf75f --- /dev/null +++ b/packages/solid-table/skills/table-state/SKILL.md @@ -0,0 +1,157 @@ +--- +name: table-state +description: > + Read Solid-backed table atoms inside JSX, createMemo, createEffect, or table.Subscribe; own slices with Solid signals or external TanStack Store atoms; and apply value-or-updater callbacks correctly. Load for Solid tracking and controlled-state bugs. +metadata: + { + type: framework, + library: '@tanstack/solid-table', + library_version: '9.0.0-beta.38', + framework: solid, + } +requires: ['@tanstack/table-core#core', getting-started] +sources: + - 'TanStack/table:docs/framework/solid/guide/table-state.md' + - 'TanStack/table:examples/solid/basic-external-state' + - 'TanStack/table:packages/solid-table/src/createTable.ts' +--- + +This skill builds on `@tanstack/table-core#core` and `getting-started`. Solid-backed atom reads react only inside a tracked scope; there is no React-style selected `table.state` object. + +## State Mental Model + +TanStack Table is primarily a state coordinator. Let it own state unless another subsystem needs the value. Without `initialState`, `atoms`, `state`, or `on[State]Change`, all registered slices are internal. + +- `table.baseAtoms` are internal writable atoms created from initial state. +- `table.atoms` are readonly derived atoms that resolve the active owner of each slice. +- `table.store` combines registered atoms into one readonly flat store. + +The Solid adapter backs core atoms with Solid signals and memos. An atom `.get()` participates in dependency tracking only inside JSX, `createMemo`, `createEffect`, or another tracked owner. State is feature-based: no `rowPaginationFeature` means no pagination state, atom, option, or method. Keep `features` and `columns` stable, and expose changing data through a reactive getter rather than rebuilding model inputs in a tracked computation. + +## Setup + +```tsx +import { createMemo } from 'solid-js' + +const table = createTable({ + features, + columns, + get data() { + return data() + }, +}) +const selectedCount = createMemo( + () => Object.keys(table.atoms.rowSelection.get()).length, +) +return {selectedCount()} +``` + +## Core Patterns + +### Control with a native signal + +```tsx +const [sorting, setSorting] = createSignal([]) +const table = createTable({ + features, + columns, + get data() { + return data() + }, + get state() { + return { sorting: sorting() } + }, + onSortingChange: setSorting, +}) +``` + +Solid signal setters already accept either a value or an updater function, so pass the setter directly. Wrap it only when adding validation, transformation, or a side effect: + +```tsx +onSortingChange: (updater) => + setSorting((old) => { + const next = typeof updater === 'function' ? updater(old) : updater + logSortingChange(next) + return next + }) +``` + +### Own a slice with an external atom + +```tsx +import { createAtom } from '@tanstack/solid-store' +const pagination = createAtom({ pageIndex: 0, pageSize: 20 }) +const table = createTable({ features, columns, data, atoms: { pagination } }) +``` + +## Choose State Ownership + +Use one owner for each slice: + +- Keep state internal and use feature methods for ordinary table-local interaction. +- Use `initialState` for a starting/reset value. Later changes to that object do not reset state. +- Prefer a stable `@tanstack/solid-store` atom through `atoms` when state is shared; feature methods write it directly. +- Use a Solid signal through a reactive `state` getter plus `on[State]Change` for simple controlled state. Resolve raw values and updater functions. + +External atoms take precedence over controlled `state`, which syncs into the internal base atom. Do not configure two owners for one slice. The global v8 `onStateChange` option is gone; observe `table.store` when all state changes matter. + +## Initialize, Update, and Reset + +Prefer feature APIs such as `table.setSorting`, `table.nextPage`, `column.toggleVisibility`, and `row.toggleSelected`. Write `table.baseAtoms` only as a low-level escape hatch for internally owned state, and write the supplied atom when external `atoms` own the slice. + +```tsx +table.resetSorting() +table.resetPagination() +table.resetPagination(true) +``` + +Feature resets use `table.initialState` unless `true` requests the feature default, and they can update an external owner. Core `table.reset()` resets internal base atoms only. Use `PaginationState` or another slice type for external state; use `TableState` for the complete feature-inferred state. + +## Common Mistakes + +### HIGH Reading outside a tracked scope + +Wrong: + +```tsx +const page = table.atoms.pagination.get().pageIndex +setInterval(() => console.log(page), 1000) +``` + +Correct: + +```tsx +const page = createMemo(() => table.atoms.pagination.get().pageIndex) +setInterval(() => console.log(page()), 1000) +``` + +An atom read only establishes Solid dependencies inside JSX, a memo, an effect, or another tracked owner. + +Source: `docs/framework/solid/guide/table-state.md` + +### MEDIUM Adding broad React-style rerenders + +Wrong: + +```tsx +createEffect(() => { + JSON.stringify(table.store.state) + forceUpdate() +}) +``` + +Correct: + +```tsx +const count = createMemo( + () => Object.keys(table.atoms.rowSelection.get()).length, +) +``` + +Solid should track the narrow atom reads actually used by the computation. + +Source: `packages/solid-table/src/createTable.ts` + +## API Discovery + +Inspect `node_modules/@tanstack/solid-table/src/createTable.ts` and `reactivity.ts`; state slice definitions and atom precedence are in installed `@tanstack/table-core/src/`. diff --git a/packages/solid-table/skills/with-tanstack-query/SKILL.md b/packages/solid-table/skills/with-tanstack-query/SKILL.md new file mode 100644 index 0000000000..cb96497126 --- /dev/null +++ b/packages/solid-table/skills/with-tanstack-query/SKILL.md @@ -0,0 +1,149 @@ +--- +name: with-tanstack-query +description: > + Compose Solid Query reactive options with Solid Table manual server processing. Load for tracked query-key signals, query result getters, server row counts, keepPreviousData, and avoiding React Query patterns or duplicated data state. +metadata: + { + type: composition, + library: '@tanstack/solid-table', + library_version: '9.0.0-beta.38', + framework: solid, + } +requires: + ['@tanstack/table-core#client-vs-server', getting-started, table-state] +sources: + - 'TanStack/table:examples/solid/with-tanstack-query' + - 'TanStack/table:docs/framework/solid/guide/pagination.md' +--- + +This skill builds on `@tanstack/table-core#client-vs-server`, `getting-started`, and `table-state`. Query owns remote data; Table receives already processed rows for each manual stage. + +## Setup + +```tsx +import { keepPreviousData, useQuery } from '@tanstack/solid-query' +import { createAtom, useSelector } from '@tanstack/solid-store' +import { + createTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/solid-table' + +const features = tableFeatures({ rowPaginationFeature }) +const emptyRows: Array<{ id: string }> = [] +const paginationAtom = createAtom({ pageIndex: 0, pageSize: 20 }) +const pagination = useSelector(paginationAtom) +const result = useQuery(() => ({ + queryKey: ['people', pagination()], + queryFn: () => fetchPeople(pagination()), + placeholderData: keepPreviousData, +})) +const table = createTable({ + features, + columns, + get data() { + return result.data?.rows ?? emptyRows + }, + get rowCount() { + return result.data?.rowCount + }, + atoms: { pagination: paginationAtom }, + manualPagination: true, +}) +``` + +## Core Patterns + +### Track every server-owned slice + +```tsx +const result = useQuery(() => ({ + queryKey: ['people', pagination(), sorting()], + queryFn: () => fetchPeople({ pagination: pagination(), sorting: sorting() }), +})) +``` + +### Expose query results through getters + +```tsx +const table = createTable({ + features, + columns, + get data() { + return result.data?.rows ?? emptyRows + }, +}) +``` + +## Common Mistakes + +### HIGH Snapshotting the query key + +Wrong: + +```tsx +useQuery({ queryKey: ['people', pagination()], queryFn: fetchPeople }) +``` + +Correct: + +```tsx +useQuery(() => ({ + queryKey: ['people', pagination()], + queryFn: () => fetchPeople(pagination()), +})) +``` + +Solid Query's options factory tracks signals read while constructing the key and request. + +Source: `examples/solid/with-tanstack-query` + +### HIGH Copying React Query state glue + +Wrong: + +```tsx +useEffect(() => setRows(result.data?.rows ?? []), [result.data]) +``` + +Correct: + +```tsx +get data() { return result.data?.rows ?? emptyRows } +``` + +Solid getters connect the query result directly without a second synchronization layer. + +Source: `examples/solid/with-tanstack-query` + +### HIGH Expecting manual mode to process + +Wrong: + +```tsx +createTable({ features, columns, data, manualPagination: true }) +``` + +Correct: + +```tsx +createTable({ + features, + columns, + get data() { + return result.data?.rows ?? emptyRows + }, + get rowCount() { + return result.data?.rowCount + }, + manualPagination: true, +}) +``` + +Manual pagination bypasses the client row model; it does not fetch, page, or count data. + +Source: `docs/framework/solid/guide/pagination.md` + +## API Discovery + +Inspect installed `node_modules/@tanstack/solid-table/src/createTable.ts` and the relevant core feature source; inspect `@tanstack/solid-query` source for reactive option shapes. diff --git a/packages/solid-table/skills/with-tanstack-virtual/SKILL.md b/packages/solid-table/skills/with-tanstack-virtual/SKILL.md new file mode 100644 index 0000000000..0c2f034381 --- /dev/null +++ b/packages/solid-table/skills/with-tanstack-virtual/SKILL.md @@ -0,0 +1,175 @@ +--- +name: with-tanstack-virtual +description: > + Virtualize Solid Table row/column models and infinite Query data with createVirtualizer, reactive counts, scroll targets, stable keys, dynamic measurement, transforms, sticky regions, and grid/flex sizing. Load for Solid tracking or layout bugs. +metadata: + { + type: composition, + library: '@tanstack/solid-table', + library_version: '9.0.0-beta.38', + framework: solid, + } +requires: ['@tanstack/table-core#core', getting-started, table-state] +sources: + - 'TanStack/table:docs/framework/solid/guide/virtualization.md' + - 'TanStack/table:examples/solid/virtualized-rows' + - 'TanStack/table:examples/solid/virtualized-columns' + - 'TanStack/table:examples/solid/virtualized-infinite-scrolling' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Virtualize the final Table model in the renderer; Virtual is not a Table feature. + +## Setup + +```tsx +import { createVirtualizer } from '@tanstack/solid-virtual' + +let scrollElement: HTMLDivElement | undefined +const rows = () => table.getRowModel().rows +const virtualizer = createVirtualizer({ + get count() { + return rows().length + }, + getScrollElement: () => scrollElement ?? null, + estimateSize: () => 36, + getItemKey: (index) => rows()[index].id, + overscan: 5, +}) +``` + +## Core Patterns + +### Keep count reactive + +```tsx +const virtualizer = createVirtualizer({ + get count() { + return rows().length + }, + getScrollElement, + estimateSize, +}) +``` + +### Apply measurement and geometry together + +```tsx + virtualizer.measureElement(node)} + style={{ position: 'absolute', transform: `translateY(${item.start}px)` }} +/> +``` + +## Common Mistakes + +### HIGH Snapshotting row count + +Wrong: + +```tsx +createVirtualizer({ count: rows().length, getScrollElement, estimateSize }) +``` + +Correct: + +```tsx +createVirtualizer({ + get count() { + return rows().length + }, + getScrollElement, + estimateSize, +}) +``` + +The getter lets Solid Virtual track changes to the final row model. + +Source: `examples/solid/virtualized-rows` + +### HIGH Virtualizing raw data + +Wrong: + +```tsx +const rows = () => data() +``` + +Correct: + +```tsx +const rows = () => table.getRowModel().rows +``` + +Raw data excludes Table's current sorting, filtering, grouping, expansion, and pagination. + +Source: `docs/framework/solid/guide/virtualization.md` + +### HIGH Separating ref lifecycle from virtualizer + +Wrong: + +```tsx +const scrollElement = document.querySelector('#rows') +const virtualizer = createVirtualizer({ + getScrollElement: () => scrollElement, + count: 100, + estimateSize, +}) +``` + +Correct: + +```tsx +let scrollElement: HTMLDivElement | undefined +const virtualizer = createVirtualizer({ + getScrollElement: () => scrollElement ?? null, + get count() { + return rows().length + }, + estimateSize, +}) +``` + +Keep the ref and virtualizer in the same owner so observers attach after JSX assigns the element. + +Source: `examples/solid/virtualized-rows` + +### HIGH Omitting renderer geometry + +Wrong: + +```tsx + + {(item) => {rows()[item.index].id}} + +``` + +Correct: + +```tsx + + + {(item) => ( + + {rows()[item.index].id} + + )} + + +``` + +Virtual computes positions; the renderer must apply spacer size, transforms, widths, and sticky CSS. + +Source: `examples/solid/virtualized-rows` + +## API Discovery + +Inspect `node_modules/@tanstack/solid-table/src/index.tsx` and installed `node_modules/@tanstack/solid-virtual/src/`; use the maintained row, column, or infinite example for the matching CSS geometry contract. diff --git a/packages/svelte-table/skills/create-table-hook/SKILL.md b/packages/svelte-table/skills/create-table-hook/SKILL.md new file mode 100644 index 0000000000..5c602132f8 --- /dev/null +++ b/packages/svelte-table/skills/create-table-hook/SKILL.md @@ -0,0 +1,168 @@ +--- +name: create-table-hook +description: > + Define a Svelte createAppTable/createAppColumnHelper using the adapter's rune-capable createTableHook implementation, shared features/defaults, reactive per-table getters, optional App component registries, and typed table/cell/header context hooks. +metadata: + type: framework + library: '@tanstack/svelte-table' + framework: svelte + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/svelte/guide/composable-tables.md' + - 'TanStack/table:examples/svelte/composable-tables' + - 'TanStack/table:packages/svelte-table/src/createTableHook.svelte.ts' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Use it when multiple tables share conventions; keep one-off tables on `createTable`. + +## Setup + +Use the shipped `createTableHook`; its implementation is rune-capable. The app hook module itself may be a normal `.ts` module, as in the maintained example. + +```ts +import { + createTableHook, + rowSortingFeature, + tableFeatures, +} from '@tanstack/svelte-table' + +export const { + createAppTable, + createAppColumnHelper, + useTableContext, + useCellContext, + useHeaderContext, +} = createTableHook({ + features: tableFeatures({ rowSortingFeature }), +}) +``` + +```svelte + +``` + +## Core Patterns + +### Register reusable components once + +Pass stable `tableComponents`, `cellComponents`, and `headerComponents` to `createTableHook`. Render them through the returned `AppTable`, `AppCell`, and `AppHeader` wrappers so their typed context hooks have a provider. + +### Prefer context inside registered components + +```ts +import { useCellContext } from './app-table.svelte' + +const cell = useCellContext() +const value = cell.getValue() +``` + +This avoids prop drilling and keeps feature/component types bound to the app hook. + +## Common Mistakes + +### CRITICAL Reimplementing the rune-aware hook + +Wrong: + +```ts +// app-table.ts +export function createAppTable(options) { + return constructTable(options) +} +``` + +Correct: + +```ts +// app-table.ts +export const { createAppTable } = createTableHook({ features }) +``` + +The shipped implementation supplies Svelte rune reactivity, option synchronization, wrappers, and context plumbing that a plain core wrapper omits. + +Source: `packages/svelte-table/src/createTableHook.svelte.ts` + +### HIGH Freezing per-table rune inputs + +Wrong: + +```ts +const table = createAppTable({ columns, data }) +``` + +Correct: + +```ts +const table = createAppTable({ + columns, + get data() { + return data + }, +}) +``` + +Shared defaults are static, but each table must still expose current reactive values. + +Source: `docs/framework/svelte/guide/composable-tables.md` + +### HIGH Reading context outside its wrapper + +Wrong: + +```ts +const cell = useCellContext() +``` + +Correct: + +```svelte + +``` + +Context helpers require the matching App wrapper in the rendered ancestor tree. + +Source: `packages/svelte-table/src/createTableHook.svelte.ts` + +### MEDIUM Abstracting a single table too early + +Wrong: + +```ts +const hook = createTableHook({ features: tableFeatures({}) }) +``` + +Correct: + +```ts +const table = createTable({ + features: tableFeatures({}), + columns, + get data() { + return data + }, +}) +``` + +Use the hook for recurring app conventions, not merely to rename standalone construction. + +Source: `docs/framework/svelte/guide/composable-tables.md` + +## API Discovery + +Inspect `node_modules/@tanstack/svelte-table/src/createTableHook.svelte.ts` and the `App*.svelte` wrappers for exact returned helpers and component contracts. diff --git a/packages/svelte-table/skills/getting-started/SKILL.md b/packages/svelte-table/skills/getting-started/SKILL.md new file mode 100644 index 0000000000..5928e4d974 --- /dev/null +++ b/packages/svelte-table/skills/getting-started/SKILL.md @@ -0,0 +1,172 @@ +--- +name: getting-started +description: > + Create a Svelte 5 TanStack Table v9 table with createTable, explicit tableFeatures, rune-backed data getters, stable static inputs, FlexRender, and headless markup. Load when replacing createSvelteTable or pre-rune patterns. +metadata: + type: framework + library: '@tanstack/svelte-table' + framework: svelte + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - '@tanstack/table-core#table-features' +sources: + - 'TanStack/table:docs/framework/svelte/guide/migrating.md' + - 'TanStack/table:examples/svelte/basic-create-table' + - 'TanStack/table:packages/svelte-table/src/index.ts' +--- + +This skill builds on `@tanstack/table-core#core` and `@tanstack/table-core#table-features`. Read them first for the headless model and feature registration. + +## Setup + +Keep features and columns outside reactive work; expose changing rune values through getters. + +```svelte + + + + + {#each table.getHeaderGroups() as group (group.id)} + {#each group.headers as header (header.id)}{/each} + {/each} + + + {#each table.getRowModel().rows as row (row.id)} + {#each row.getAllCells() as cell (cell.id)}{/each} + {/each} + +
{#if !header.isPlaceholder}{/if}
+``` + +## Core Patterns + +### Add only the processing feature you need + +```ts +import { + createSortedRowModel, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/svelte-table' + +export const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) +``` + +The row-model slot follows its prerequisite feature in the same call. + +### Treat markup and styles as application code + +With core-only `tableFeatures({})`, render `row.getAllCells()`. Use visibility-aware APIs such as `row.getVisibleCells()` only after registering `columnVisibilityFeature`. Call feature APIs from real Svelte event handlers. Table supplies no component-library markup, CSS, or accessibility behavior. + +## Common Mistakes + +### HIGH Passing a rune snapshot as data + +Wrong: + +```ts +const table = createTable({ features, columns, data }) +``` + +Correct: + +```ts +const table = createTable({ + features, + columns, + get data() { + return data + }, +}) +``` + +The getter makes `$effect.pre` observe current rune data rather than the value captured at construction. + +Source: `packages/svelte-table/src/createTable.svelte.ts` + +### HIGH Using the removed v8 constructor + +Wrong: + +```ts +const table = createSvelteTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), +}) +``` + +Correct: + +```ts +const table = createTable({ + features, + columns, + get data() { + return data + }, +}) +``` + +V9 requires Svelte 5, `createTable`, and explicit features; the core row model is automatic. + +Source: `docs/framework/svelte/guide/migrating.md` + +### HIGH Omitting a feature behind an API + +Wrong: + +```ts +const features = tableFeatures({}) +table.setSorting([{ id: 'age', desc: true }]) +``` + +Correct: + +```ts +const features = tableFeatures({ rowSortingFeature }) +``` + +Feature state and APIs exist only when that feature is registered. + +Source: `docs/framework/svelte/guide/migrating.md` + +## API Discovery + +Inspect `node_modules/@tanstack/svelte-table/src/index.ts`, then the exported implementation. Inspect core and feature APIs through `node_modules/@tanstack/table-core/src/index.ts` and `src/features//`. diff --git a/packages/svelte-table/skills/migrate-v8-to-v9/SKILL.md b/packages/svelte-table/skills/migrate-v8-to-v9/SKILL.md new file mode 100644 index 0000000000..88edb680ab --- /dev/null +++ b/packages/svelte-table/skills/migrate-v8-to-v9/SKILL.md @@ -0,0 +1,195 @@ +--- +name: migrate-v8-to-v9 +description: > + Complete Svelte v8-to-v9 migration reference: Svelte 5, createTable, explicit features and row-model slots, atom/rune state, rendering helpers, prototype methods, type generics, sorting, sizing, selection, and logical pinning. +metadata: + type: lifecycle + library: '@tanstack/svelte-table' + framework: svelte + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#migrate-v8-to-v9' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/svelte/guide/migrating.md' + - 'TanStack/table:packages/svelte-table/src/index.ts' + - 'TanStack/table:examples/svelte/basic-create-table' +--- + +Use this as the complete breaking-change checklist, not merely a quick start. V9 is treated as the current API. Migrate the app to Svelte 5 before migrating Table; the v9 adapter has no Svelte 3/4 compatibility layer. + +Framework prerequisite: Svelte 5 (`svelte ^5.0.0`). + +## Recommended Migration Order + +1. Upgrade to Svelte 5 and replace v8 stores with runes/getters. +2. Rename `createSvelteTable` to `createTable`. +3. Define explicit `tableFeatures`, then move row models and registries into it. +4. Update state reads/ownership and rendering. +5. Apply every shared API and type rename below. +6. Use `stockFeatures` only as a temporary audit bridge; explicit features are the production target. + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return data + }, +}) +``` + +## Construction and Feature Registration + +| v8 | v9 | +| -------------------------------------------- | ---------------------------------------------------------- | +| `createSvelteTable(options)` | `createTable(options, selector?)` | +| All features bundled | Required `features: tableFeatures({...})` | +| `getCoreRowModel()` option | Remove; the core row model is automatic | +| `get*RowModel()` table options | `create*RowModel()` slots in `tableFeatures` | +| `sortingFns` table option | `sortFns` feature slot | +| `filterFns` / `aggregationFns` table options | Same-named feature slots | +| Top-level `onStateChange` | Per-slice callbacks, external atoms, or store subscription | + +Available feature imports are `columnFilteringFeature`, `globalFilteringFeature`, `rowSortingFeature`, `rowPaginationFeature`, `rowSelectionFeature`, `rowExpandingFeature`, `rowPinningFeature`, `columnPinningFeature`, `columnVisibilityFeature`, `columnOrderingFeature`, `columnSizingFeature`, `columnResizingFeature`, `columnGroupingFeature`, and `columnFacetingFeature`. An API does not exist unless its feature is registered. Put a feature before its dependent slot in the same `tableFeatures` call. + +### Row-model mapping + +| v8 option | v9 slot and factory | +| -------------------------- | ------------------------------------------------------------------- | +| `getFilteredRowModel()` | `filteredRowModel: createFilteredRowModel()` after column filtering | +| `getSortedRowModel()` | `sortedRowModel: createSortedRowModel()` after row sorting | +| `getPaginationRowModel()` | `paginatedRowModel: createPaginatedRowModel()` after pagination | +| `getExpandedRowModel()` | `expandedRowModel: createExpandedRowModel()` after expanding | +| `getGroupedRowModel()` | `groupedRowModel: createGroupedRowModel()` after grouping | +| `getFacetedRowModel()` | `facetedRowModel: createFacetedRowModel()` after faceting | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues: createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues: createFacetedUniqueValues()` | + +Factories take no arguments. Register `filterFns`, `sortFns`, and `aggregationFns` as sibling feature slots. + +## Svelte State Migration + +- Reactive option inputs must remain live: use getters for rune values such as `data` and controlled state slices. +- `table.getState().sorting` becomes `table.state.sorting`, `table.store.state.sorting`, or the narrow `table.atoms.sorting.get()`. +- `table.state` contains all registered state by default. Pass a second-argument selector to `createTable` only to narrow its reactive surface. +- `subscribeTable(table.atoms.pagination, selector?)` exposes `.current` for fine-grained template subscriptions. +- For Svelte-owned controlled slices, use `createTableState` and matching `onSortingChange`, `onPaginationChange`, and other per-slice callbacks. +- For shared ownership, provide atoms created by `@tanstack/svelte-store` through `atoms`. Never provide both `atoms.pagination` and `state.pagination`. +- Subscribe to `table.store` to observe every state change. Do not port the removed top-level `onStateChange`. +- Treat `table.baseAtoms` as internal writable state; prefer feature APIs or external atoms. + +## Rendering and Composition + +| v8 | v9 | +| ---------------------------------------- | -------------------------------------------------------------------------------- | +| `flexRender(...)` / `` | ``, ``, or `` | +| Component returned directly | `renderComponent(Component, props)` | +| Svelte snippet content | `renderSnippet(snippet, props)` | +| Repeated raw options | `tableOptions(...)` composition | +| Repeated table conventions | `createTableHook({ features, ... })` and its pre-bound helpers | + +`createTableHook` returns a feature-bound table creator and column helper; use it for application-wide conventions, not as a required migration step. + +## Complete Shared Breaking-Change Map + +### Instance methods + +Row, cell, column, header, and related object methods now live on shared prototypes and use `this`. Call `row.getValue(...)`, `cell.getContext()`, `column.getCanSort()`, and `header.getContext()` on their instances. Do not destructure them or pass them as bare callbacks. They are not own enumerable properties, so object spread, `Object.keys`, and JSON serialization do not preserve them. Table methods are not affected. + +### Logical column pinning + +There are no `left`/`right` aliases in beta.38. + +| old | new | +| -------------------------------------------------------------- | ------------------------------------------------------------- | +| `columnPinning.left` / `.right` | `.start` / `.end` | +| `column.pin('left' \| 'right')` | `column.pin('start' \| 'end')` | +| `getIsPinned() === 'left' \| 'right'` | `'start' \| 'end'` | +| `row.getLeftVisibleCells()` / `getRightVisibleCells()` | `getStartVisibleCells()` / `getEndVisibleCells()` | +| `getLeftHeaderGroups()` / `getRightHeaderGroups()` | `getStartHeaderGroups()` / `getEndHeaderGroups()` | +| `getLeftFooterGroups()` / `getRightFooterGroups()` | `getStartFooterGroups()` / `getEndFooterGroups()` | +| `getLeftFlatHeaders()` / `getRightFlatHeaders()` | `getStartFlatHeaders()` / `getEndFlatHeaders()` | +| `getLeftLeafHeaders()` / `getRightLeafHeaders()` | `getStartLeafHeaders()` / `getEndLeafHeaders()` | +| `getLeftLeafColumns()` / `getRightLeafColumns()` | `getStartLeafColumns()` / `getEndLeafColumns()` | +| `getLeftVisibleLeafColumns()` / `getRightVisibleLeafColumns()` | `getStartVisibleLeafColumns()` / `getEndVisibleLeafColumns()` | +| `getLeftTotalSize()` / `getRightTotalSize()` | `getStartTotalSize()` / `getEndTotalSize()` | +| `column.getStart('left')` | `column.getStart('start')` | +| `column.getAfter('right')` | `column.getAfter('end')` | +| `column.getIndex('left' \| 'right')` | `column.getIndex('start' \| 'end')` | + +This is logical region naming, not automatic DOM direction handling. Prefer CSS `inset-inline-start`/`inset-inline-end`. `columnResizeDirection` is unchanged. + +### Feature and state splits + +- `enablePinning` splits into `enableColumnPinning` and `enableRowPinning`. +- Interactive resizing requires both `columnSizingFeature` and `columnResizingFeature`; fixed widths need only sizing. +- `columnSizingInfo` becomes `columnResizing`. +- `setColumnSizingInfo()` becomes `setColumnResizing()`. +- `onColumnSizingInfoChange` becomes `onColumnResizingChange`. + +### Sorting, rows, and selection + +| v8 | v9 | +| ------------------------------ | ----------------------------- | +| `sortingFn` | `sortFn` | +| `sortingFns` | `sortFns` | +| `getSortingFn()` | `getSortFn()` | +| `getAutoSortingFn()` | `getAutoSortFn()` | +| `SortingFn` / `SortingFns` | `SortFn` / `SortFns` | +| `row._getAllCellsByColumnId()` | `row.getAllCellsByColumnId()` | + +All other `_`-prefixed internal APIs are removed, including `_getPinnedRows`, `_getFacetedRowModel`, `_getFacetedMinMaxValues`, and `_getFacetedUniqueValues`; do not seek replacements unless a public API is documented. + +`getIsSomeRowsSelected()` and `getIsSomePageRowsSelected()` now mean at least one, including all. For an indeterminate checkbox, combine “some” with `!getIsAllRowsSelected()` or `!getIsAllPageRowsSelected()`. + +## TypeScript Migration + +- Core types now take `TFeatures` first: `ColumnDef`, `Column`, `Row`, `Table`. +- Replace `createColumnHelper()` with `createColumnHelper()`; wrap arrays in `columnHelper.columns([...])` for inference. +- With `stockFeatures`, use `StockFeatures` as the feature type. +- `TableMeta` and `ColumnMeta` declaration merging still works only after adding `TFeatures` first. Prefer per-table `tableMeta`/`columnMeta: metaHelper<...>()` slots. +- Replace global `FilterFns`, `SortFns`, `AggregationFns`, and `FilterMeta` augmentation with `filterFns`, `sortFns`, `aggregationFns`, and `filterMeta: metaHelper<...>()` slots. Registered keys become valid string references. +- Prefer explicit object row types; `RowData` is restricted to records or arrays. + +## Common Migration Failures + +### CRITICAL: Running v9 on Svelte 3/4 + +Upgrade to Svelte 5 first. Writable-store-era table setup is not a supported v9 adapter contract. + +### HIGH: Moving the feature but not its row model + +Register both the feature and its `create*RowModel()` slot. Leaving `get*RowModel` on table options silently leaves the v9 processing pipeline incomplete. + +### HIGH: Snapshotting a rune value + +Use `get data() { return data }`; a one-time `data` snapshot does not remain reactive. + +### HIGH: Destructuring instance methods + +Keep calls bound to row/cell/column/header instances; shallow copies do not contain prototype methods. + +## Final Checklist + +- [ ] Svelte is version 5+; old writable-store patterns are removed. +- [ ] `createSvelteTable` is replaced by `createTable`. +- [ ] Explicit features, row models, and function registries are in `tableFeatures`. +- [ ] `getCoreRowModel` and the separate `rowModels` shape are removed. +- [ ] Reactive inputs and controlled slices use getters/runes; state reads use v9 surfaces. +- [ ] `onStateChange` is replaced; atom/state ownership does not overlap. +- [ ] Rendering uses `FlexRender`, `renderComponent`, or `renderSnippet`. +- [ ] Prototype method calls, pinning, sizing/resizing, sorting, row, and selection semantics are audited. +- [ ] Helpers, types, meta, registries, and `RowData` use the v9 generic/slot shapes. +- [ ] Temporary `stockFeatures` usage has an explicit removal plan. + +## API Discovery + +Verify the installed target in `node_modules/@tanstack/svelte-table/src/index.ts` and its adapter sources. Verify feature slots and exact beta APIs in `node_modules/@tanstack/table-core/src`; do not reconstruct v9 APIs from v8 memory. diff --git a/packages/svelte-table/skills/svelte/client-to-server/SKILL.md b/packages/svelte-table/skills/svelte/client-to-server/SKILL.md deleted file mode 100644 index 97496982c9..0000000000 --- a/packages/svelte-table/skills/svelte/client-to-server/SKILL.md +++ /dev/null @@ -1,236 +0,0 @@ ---- -name: svelte/client-to-server -description: > - Convert a client-side Svelte table to server-side (manual) modes. Toggle `manualPagination`, - `manualSorting`, `manualFiltering`, `manualGrouping`, `manualExpanding` for whatever the server - owns, drop the matching row-model factory slots from `tableFeatures` and any `features` you no - longer need, supply `rowCount` for the pager, then drive the request from `table.atoms.pagination` - / `table.atoms.sorting` / etc. (or external atoms you own) using rune-aware getters - (`get data()`, `get rowCount()`) so the table re-syncs in `$effect.pre`. Svelte 5+ only. -type: lifecycle -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - state-management - - pagination - - filtering - - sorting - - svelte/table-state -sources: - - TanStack/table:examples/svelte/basic-external-atoms/ - - TanStack/table:examples/svelte/basic-external-state/ - - TanStack/table:examples/svelte/with-tanstack-query/ - - TanStack/table:docs/framework/svelte/guide/table-state.md ---- - -# Client → Server (Svelte) - -You have a working client-side table. The dataset is too big to ship to the browser, or it -lives behind an API. You want sorting / filtering / pagination to run on the server while the -table still feels the same in the UI. - -## Mental model - -Each "manual mode" flag tells the table: **don't run this stage of the pipeline; trust the data -you receive.** You can mix modes freely — manual pagination + client-side sorting on the -already-paged window is perfectly valid for medium datasets. - -| Flag | Meaning | What you must provide | -| ------------------ | -------------------------------------------- | ----------------------------------------------------------- | -| `manualPagination` | Server owns slicing; do not paginate locally | `rowCount` (or `pageCount`) | -| `manualSorting` | Server owns ordering | Sort the server query by `sorting` state | -| `manualFiltering` | Server owns row filtering | Filter the server query by `columnFilters` / `globalFilter` | -| `manualGrouping` | Server returns already-grouped rows | Pre-shaped data | -| `manualExpanding` | Server resolves sub-rows | Server-provided sub-row tree | - -When a stage is manual, you can drop its row-model factory slot from `tableFeatures`. `manualPagination: true` does not need `paginatedRowModel: createPaginatedRowModel()` in the features object. - -## Step 1 — Identify what's moving server-side - -For a typical "search and paginate against a database" screen: - -- Pagination → server -- Filtering (column filter inputs + a global search box) → server -- Sorting → server (usually, since a partial page can't be sorted client-side meaningfully) -- Selection / visibility / column ordering → still client - -So the table keeps `rowSelectionFeature` etc., drops `columnFilteringFeature` / -`rowPaginationFeature` / `rowSortingFeature` _row models_ but keeps the _features_ so the -state slices and UI APIs still exist. - -> Subtle point: keep the **feature** even if you drop the row model. The feature is what gives -> you `column.getCanSort()`, `table.setPageIndex()`, `column.setFilterValue()` — all the -> control-surface APIs. Dropping it kills the UI. - -## Step 2 — Own the relevant state with external atoms - -External atoms make state portable: the data layer (a fetch / query / store) can read the -same atoms the table writes to. Use `@tanstack/svelte-store`: - -```ts -import { createAtom, useSelector } from '@tanstack/svelte-store' -import type { - ColumnFiltersState, - PaginationState, - SortingState, -} from '@tanstack/svelte-table' - -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) -const sortingAtom = createAtom([]) -const filtersAtom = createAtom([]) - -// For Svelte markup that should react to changes: -const pagination = useSelector(paginationAtom) -const sorting = useSelector(sortingAtom) -const filters = useSelector(filtersAtom) -``` - -## Step 3 — Configure the table - -```svelte - -``` - -`rowCount` is what makes `table.getPageCount()` / `table.getCanNextPage()` correct under -manual pagination. Without it the pager has no idea how many pages exist. - -## Step 4 — Drive the fetch from those atoms - -Wire whatever data layer you use (TanStack Query, a raw `fetch`, SvelteKit `load`, etc.) to -read the atoms. With TanStack Query: - -```ts -import { createQuery, keepPreviousData } from '@tanstack/svelte-query' - -const dataQuery = createQuery<{ rows: Array; rowCount: number }>( - () => ({ - queryKey: ['people', pagination.current, sorting.current, filters.current], - queryFn: () => - fetch('/api/people', { - method: 'POST', - body: JSON.stringify({ - pageIndex: pagination.current.pageIndex, - pageSize: pagination.current.pageSize, - sorting: sorting.current, - filters: filters.current, - }), - }).then((r) => r.json()), - placeholderData: keepPreviousData, - }), -) -``` - -`placeholderData: keepPreviousData` is what kills the "rows blank for one tick on every -page change" flash. - -## Step 5 — Reset behavior - -When the user changes a filter, you usually want to jump back to page 0. The table does this -automatically when client-side filtering owns the data, but with manual mode the data layer -controls it. Simplest fix: explicitly reset. - -```ts -$effect(() => { - // re-runs whenever filters.current identity changes - filters.current - table.setPageIndex(0) -}) -``` - -Or wrap your filter `onChange` handlers to also call `table.setPageIndex(0)`. - -## Step 6 — A note on global filtering - -If you also support `globalFilterFeature`, debounce the input. `column.setFilterValue` and -`table.setGlobalFilter` fire per keystroke; without debouncing you fire one request per typed -character. See the `compose-with-tanstack-pacer` skill for the pattern. - -## Hybrid example — manual pagination only - -Sometimes you only paginate server-side and let the page-sized window sort/filter on the -client. - -```ts -const table = createTable({ - features: tableFeatures({ - columnFilteringFeature, - rowPaginationFeature, - rowSortingFeature, - filteredRowModel: createFilteredRowModel(), // client filters the page - sortedRowModel: createSortedRowModel(), // client sorts the page - filterFns, - sortFns, - }), - columns, - get data() { - return query.data?.rows ?? [] - }, - get rowCount() { - return query.data?.rowCount - }, - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -Only the manual flag for the stage you're moving server-side. - -## Common failure modes - -- **Forgot `rowCount`.** `table.getPageCount()` returns `-1`, the pager looks broken. -- **Dropped the feature, not just the row model.** Lost `column.getCanSort()` and friends. - Keep the feature when you still need its UI APIs; only drop the row-model factory. -- **Both `state.pagination` and `atoms.pagination`.** Atoms silently win; the `on*Change` - callback never fires. -- **Re-creating atoms inside reactive blocks.** Atoms must be stable across renders. Declare - them at module / component-init scope, not inside `$derived` or `$effect`. -- **Forgetting to reset page on filter change.** Stay on page 12 of a now-2-page result set. -- **Plain `data: query.data?.rows`.** No getter, no reactivity. Use `get data()`. -- **Reimplementing pagination math.** `table.setPageIndex / nextPage / previousPage / -firstPage / lastPage / setPageSize / getCanNextPage / getCanPreviousPage / getPageCount` - already exist and respect manual mode. - -## Related skills - -- `tanstack-table/svelte/compose-with-tanstack-query` — the same flow with a Query data layer. -- `tanstack-table/svelte/compose-with-tanstack-pacer` — debouncing filter inputs. -- `tanstack-table/svelte/compose-with-tanstack-store` — atom interop and per-slice subscription. -- `tanstack-table/core/pagination` / `filtering` / `sorting` — feature deep dives. diff --git a/packages/svelte-table/skills/svelte/compose-with-tanstack-form/SKILL.md b/packages/svelte-table/skills/svelte/compose-with-tanstack-form/SKILL.md deleted file mode 100644 index 99af47a417..0000000000 --- a/packages/svelte-table/skills/svelte/compose-with-tanstack-form/SKILL.md +++ /dev/null @@ -1,294 +0,0 @@ ---- -name: svelte/compose-with-tanstack-form -description: > - Editable cells in `@tanstack/svelte-table` powered by `@tanstack/svelte-form`. The table is the - layout primitive; the form owns the state. Use `createFormHook` to register reusable field - components (`TextField`, `NumberField`, `SelectField`), then in each column's `cell` renderer - return `renderComponent(MyFieldCell, { form, rowIndex, fieldName })` and inside that cell call - `form.Field` (or an `AppField`) with `name="data[${rowIndex}].${fieldName}"`. Drive the table's - `data` from `form.state.values.data`. Svelte 5+ only. -type: composition -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - row-selection - - column-definitions -sources: - - TanStack/table:examples/svelte/with-tanstack-form/ - - TanStack/table:docs/framework/svelte/svelte-table.md ---- - -# Compose with TanStack Form (Svelte) - -Editable tables are a classic source of state-management chaos. With v9 + TanStack Form, the -division of labor is crisp: - -- **Form** owns the editable values (per-row, per-field). -- **Table** owns the layout (columns, filtering, pagination of the same form data). -- **Cells** are just field renderers — they read and write through Form's field APIs. - -## Install - -```bash -pnpm add @tanstack/svelte-form @tanstack/svelte-table -``` - -## Set up a field-component-rich Form hook - -Define a `createAppForm` once with the reusable field components. This is the form-side -equivalent of `createTableHook`. - -```ts -// hooks/form.ts -import { createFormHook, createFormHookContexts } from '@tanstack/svelte-form' -import TextField from '../components/TextField.svelte' -import NumberField from '../components/NumberField.svelte' -import SelectField from '../components/SelectField.svelte' -import SubmitButton from '../components/SubmitButton.svelte' -import FormStateIndicator from '../components/FormStateIndicator.svelte' - -export const { fieldContext, formContext } = createFormHookContexts() - -export const { useAppForm: createAppForm } = createFormHook({ - fieldComponents: { TextField, NumberField, SelectField }, - formComponents: { SubmitButton, FormStateIndicator }, - fieldContext, - formContext, -}) -``` - -## Reusable field cell components - -Each cell type is a small Svelte component that knows which row + field it edits and uses -`form.Field`. The shape is the same across types — `TextFieldCell`, `NumberFieldCell`, -`SelectFieldCell`. - -```svelte - - - - - {#snippet children(field)} - field.handleChange((e.target as HTMLInputElement).value)} - onblur={field.handleBlur} - /> - {#if field.state.meta.errors?.length} - {field.state.meta.errors.join(', ')} - {/if} - {/snippet} - -``` - -## Wire the table to form state - -```svelte - - -
{ - e.preventDefault() - void form.handleSubmit() - }} -> - - {#snippet children()} - - - {/snippet} - - - - - - - {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - - {#each headerGroup.headers as header (header.id)} - - {/each} - - {/each} - - - {#each table.getRowModel().rows as row (row.id)} - - {#each row.getAllCells() as cell (cell.id)} - - {/each} - - {/each} - -
-
-``` - -## Why `row.index` and not `row.id`? - -Form indexes its arrays positionally. `row.index` is the position inside the **current** row -model (after filter + sort + paging). If you want a positional address into `form.state.values.data`, -you usually want the **original** index — pass `row.original` somewhere that exposes it, or -store an `id` field and look it up. - -For the common case where the table renders the array in its natural order (no sort, no -filter that reorders), `row.index` matches the form-array position. - -## Add Row / Remove Row - -Use the form's array helpers; the table re-renders because its `data` getter points at -`form.state.values.data`. - -```ts -form.pushFieldValue('data', newPerson) -form.removeFieldValue('data', rowIndex) -form.replaceFieldValue('data', rowIndex, updatedPerson) -``` - -## Pairing with selection - -Add `rowSelectionFeature` to enable row checkboxes, then a "delete selected" button can use -`table.getSelectedRowModel()` to collect rows and `form.removeFieldValue` to remove them. - -```ts -const selectedRows = table.getSelectedRowModel().rows -const indexesDesc = selectedRows.map((r) => r.index).sort((a, b) => b - a) -for (const i of indexesDesc) { - form.removeFieldValue('data', i) -} -table.resetRowSelection() -``` - -Remove in descending order so earlier removals don't shift later indexes. - -## Pairing with virtualization - -You can virtualize the rows even with editable cells — but be aware that **virtualized rows -unmount when scrolled out of view**, taking their inline form fields with them. If a cell has -unsaved local-only state, you'll lose it. Use Form fields (which live on the form's state) -and you're fine — the field state survives the unmount. - -## Common failure modes - -- **`renderComponent` from React docs.** Use the Svelte adapter's `renderComponent` from - `@tanstack/svelte-table`. The signature is the same shape but the runtime is different. -- **`form` not reactive in cells.** Pass `form` as a prop; don't reach for it via context - unless you set up `formContext`. -- **Wrong field name.** `data[${row.index}].firstName` — string template, not a plain join. -- **Reordering / filtering breaks `row.index`.** As above. Either keep a stable id and resolve - back to the form-array index, or accept that `row.index` only addresses the visible window. -- **Editing inside virtualized rows without form state.** Field values lost on scroll. -- **Reimplementing form state with `$state` per cell.** Defeats the whole point — Form already - owns this state and runs validation. - -## Related skills - -- `tanstack-table/core/row-selection` — checkbox column patterns. -- `tanstack-table/core/column-definitions` — accessor / display columns. -- `tanstack-table/svelte/table-state` — `getRowModel()` and reactivity. diff --git a/packages/svelte-table/skills/svelte/compose-with-tanstack-pacer/SKILL.md b/packages/svelte-table/skills/svelte/compose-with-tanstack-pacer/SKILL.md deleted file mode 100644 index 13bb4dc010..0000000000 --- a/packages/svelte-table/skills/svelte/compose-with-tanstack-pacer/SKILL.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -name: svelte/compose-with-tanstack-pacer -description: > - Use `@tanstack/svelte-pacer` to debounce / throttle high-frequency writes that drive a - `@tanstack/svelte-table` v9 instance — column filter inputs and column resize state are the - two hot paths. Import `createDebouncer` (or `createThrottler`) from - `@tanstack/svelte-pacer/debouncer`, wrap the call site that hits `column.setFilterValue`, - `table.setGlobalFilter`, or commits a resize, and call `.maybeExecute(value)` on each event. - Svelte 5+ only — pacer instances live at component-init scope. -type: composition -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - filtering - - column-layout -sources: - - TanStack/table:examples/svelte/with-tanstack-form/ - - TanStack/table:docs/framework/svelte/guide/table-state.md ---- - -# Compose with TanStack Pacer (Svelte) - -Two places in a v9 table take state writes at event-loop rate: **filter inputs** (one -keystroke = one `setFilterValue`) and **column resizing** (one pointermove = one -`columnSizing` write). Without rate-limiting they either flood the network (server-side -filtering) or burn CPU on tens of thousands of re-renders per drag. - -`@tanstack/svelte-pacer` gives you `createDebouncer`, `createThrottler`, and friends. Wrap -the call site and you're done. - -## Install - -```bash -pnpm add @tanstack/svelte-pacer -``` - -## Debounce a column filter input - -Client-side debouncing reduces re-render churn. Server-side debouncing also kills request -storms. - -```svelte - - - -``` - -Why the `localValue`? So the input stays snappy (controlled by `$state`) while the table only -sees the debounced commit. - -## Debounce a global filter - -Same shape, calling `table.setGlobalFilter`: - -```svelte - - - { - search = (e.target as HTMLInputElement).value - debouncedSetGlobalFilter.maybeExecute(search) - }} -/> -``` - -## Throttle column resizing - -`columnResizingFeature` (which requires `columnSizingFeature` to also be registered in `tableFeatures`) writes to `columnSizing` continuously. v9 supports `columnResizeMode: -'onChange' | 'onEnd'` — the default is `'onChange'` (commit per-frame). For very heavy tables, -either: - -1. Set `columnResizeMode: 'onEnd'` so the commit only happens at pointerup. -2. Or, keep `'onChange'` for the visual handle but throttle the side effects you fire off it. - -Throttle a side effect: - -```ts -import { createThrottler } from '@tanstack/svelte-pacer/throttler' - -const throttledSaveSizing = createThrottler( - (sizing: ColumnSizingState) => persistColumnSizingToStorage(sizing), - { wait: 100 }, -) - -$effect(() => { - const sizing = table.atoms.columnSizing.get() - throttledSaveSizing.maybeExecute(sizing) -}) -``` - -## Patterns to avoid - -### Don't debounce inside a `$effect` - -```ts -// WRONG — new debouncer instance every effect re-run -$effect(() => { - const d = createDebouncer((v) => column.setFilterValue(v), { wait: 200 }) - d.maybeExecute(value) -}) -``` - -Pacer instances must be stable. Declare them once at component init scope. - -### Don't debounce things that should be immediate - -Selection toggles, page changes, sort clicks — these are user-driven discrete events. Don't -debounce them. The user expects instant feedback. - -### Don't double-commit - -```ts -// WRONG — local state syncs immediately AND the debounced commit fires -oninput={(e) => { - column.setFilterValue(e.currentTarget.value) - debouncedSetFilter.maybeExecute(e.currentTarget.value) -}} -``` - -Pick one. If you want a snappy input, store the input value in local `$state` and only call -the debounced commit. - -## Coordinating with TanStack Query - -If your filter triggers a server query, debouncing the filter handler is necessary but not -sufficient — `placeholderData: keepPreviousData` is what keeps the UI from flashing. See the -`compose-with-tanstack-query` skill. - -## Common failure modes - -- **Recreating pacer instances inside `$effect` / `$derived`.** Each re-run produces a fresh - debouncer; nothing is ever delayed. -- **Hand-rolled `setTimeout` debounce.** Loses leading-edge / trailing-edge guarantees; harder - to cancel on unmount. Use pacer. -- **Debouncing a value that drives layout.** Causes user-visible lag where there shouldn't - be. Keep the input controlled by local state and only debounce the table-write call. -- **Forgetting to call `.maybeExecute`.** Constructing a debouncer doesn't enqueue anything; - you have to call `.maybeExecute(value)` per event. -- **Reimplementing pacer with `$effect` timers.** Don't. - -## Related skills - -- `tanstack-table/core/filtering` — column / global filter mechanics. -- `tanstack-table/core/column-layout` — column resizing modes (`onChange` vs `onEnd`). -- `tanstack-table/svelte/compose-with-tanstack-query` — pairing pacer with server queries. -- `tanstack-table/svelte/production-readiness` — where pacer fits in the perf checklist. diff --git a/packages/svelte-table/skills/svelte/compose-with-tanstack-query/SKILL.md b/packages/svelte-table/skills/svelte/compose-with-tanstack-query/SKILL.md deleted file mode 100644 index 00c8d9129c..0000000000 --- a/packages/svelte-table/skills/svelte/compose-with-tanstack-query/SKILL.md +++ /dev/null @@ -1,296 +0,0 @@ ---- -name: svelte/compose-with-tanstack-query -description: > - Server-side / async data flow with `@tanstack/svelte-query` and `@tanstack/svelte-table`. - Key the `createQuery` on the table state that drives the request (pagination + sort + - filters), pass `placeholderData: keepPreviousData` to avoid a "0 rows flash" between pages, - set `manualPagination` (and optionally `manualSorting` / `manualFiltering`), supply - `rowCount`, and feed the query result through reactive getters (`get data()`, - `get rowCount()`). Own driver state with `$state` or `@tanstack/svelte-store` atoms. - Svelte 5+ only. -type: composition -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - svelte/client-to-server - - pagination - - state-management -sources: - - TanStack/table:examples/svelte/with-tanstack-query/ - - TanStack/table:docs/framework/svelte/guide/table-state.md ---- - -# Compose with TanStack Query (Svelte) - -`@tanstack/svelte-query` and `@tanstack/svelte-table` complement each other naturally: - -- **Query** owns server data — fetching, caching, retries, placeholder data. -- **Table** owns view state — pagination, sort, filters, selection. - -The integration is short and predictable: drive the query key from the view state, manual-mode -the affected pipeline stages, pipe the result back through reactive getters. - -## The pattern in 30 seconds - -```svelte - -``` - -Three things to notice: - -1. `queryKey` includes the driver state (`pagination`). Query re-fetches when the page or page - size changes. -2. `placeholderData: keepPreviousData` keeps the previous page visible while the next page - loads. Without it, `dataQuery.data?.rows` is `undefined` for one tick on every page change - and the table flashes empty. -3. `manualPagination: true` tells the table the data is already paged. Without `rowCount` the - pager has no idea how many pages exist. - -## Driver-state ownership choices - -You can drive the query from either: - -- **Component `$state` + `state` + `on[State]Change`** (shown above) — simplest, mirrors - what most v8 codebases look like after migration. -- **External `@tanstack/svelte-store` atoms + `atoms`** — preferable when the same state - drives multiple components (a toolbar, a sidebar, a URL syncer). - -```ts -import { createAtom, useSelector } from '@tanstack/svelte-store' - -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) -const pagination = useSelector(paginationAtom) - -const dataQuery = createQuery(() => ({ - queryKey: ['people', pagination.current], - queryFn: () => fetchPeople(pagination.current), - placeholderData: keepPreviousData, -})) - -const table = createTable({ - features, - columns, - get data() { - return dataQuery.data?.rows ?? [] - }, - get rowCount() { - return dataQuery.data?.rowCount - }, - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -`table.setPageIndex(2)` writes through `paginationAtom`, which invalidates `queryKey`, which -fetches page 3. - -## Adding sort and filters - -```ts -import type { ColumnFiltersState, SortingState } from '@tanstack/svelte-table' - -let sorting: SortingState = $state([]) -let filters: ColumnFiltersState = $state([]) -let pagination: PaginationState = $state({ pageIndex: 0, pageSize: 10 }) - -const dataQuery = createQuery(() => ({ - queryKey: ['people', pagination, sorting, filters], - queryFn: () => fetchPeople({ pagination, sorting, filters }), - placeholderData: keepPreviousData, -})) - -const table = createTable({ - features: tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - }), - columns, - get data() { - return dataQuery.data?.rows ?? [] - }, - get rowCount() { - return dataQuery.data?.rowCount - }, - state: { - get pagination() { - return pagination - }, - get sorting() { - return sorting - }, - get columnFilters() { - return filters - }, - }, - onPaginationChange: (u) => - (pagination = typeof u === 'function' ? u(pagination) : u), - onSortingChange: (u) => (sorting = typeof u === 'function' ? u(sorting) : u), - onColumnFiltersChange: (u) => - (filters = typeof u === 'function' ? u(filters) : u), - manualPagination: true, - manualSorting: true, - manualFiltering: true, -}) -``` - -## Reset page on filter / sort change - -Otherwise a user filters from "all 5000 people" to "5 named Alice" and stays on page 12. - -```ts -$effect(() => { - // re-run on identity change - filters - sorting - table.setPageIndex(0) -}) -``` - -## Debounce keystroke-driven filters - -Without debouncing, a search input fires one request per character. - -```ts -import { createDebouncer } from '@tanstack/svelte-pacer/debouncer' - -const debouncedSetGlobalFilter = createDebouncer( - (value: string) => table.setGlobalFilter(value), - { wait: 250 }, -) -``` - -```svelte - debouncedSetGlobalFilter.maybeExecute(e.currentTarget.value)} -/> -``` - -See the `compose-with-tanstack-pacer` skill for the full pacer pattern. - -## Loading and empty states - -`createQuery` exposes `isFetching`, `isPending`, `isError`, `data`. Use them around the -table, not inside the row loop. - -```svelte -{#if dataQuery.isPending} -
Loading…
-{:else if dataQuery.isError} -
Failed: {dataQuery.error.message}
-{:else} - ...
-{/if} - -{#if dataQuery.isFetching} - Refreshing… -{/if} -``` - -`isFetching` is helpful for the "loading next page" indicator while -`placeholderData: keepPreviousData` still shows the old rows. - -## Optimistic updates (when you also mutate) - -```ts -import { createMutation, useQueryClient } from '@tanstack/svelte-query' - -const queryClient = useQueryClient() - -const updatePerson = createMutation(() => ({ - mutationFn: (input: Partial) => - fetch(`/api/people/${input.id}`, { - method: 'PATCH', - body: JSON.stringify(input), - }), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['people'] }), -})) -``` - -After the mutation succeeds, `invalidateQueries` re-fetches; `placeholderData` keeps the old -rows visible during the refresh. - -## SvelteKit `load` integration (a sketch) - -If your table is on a SvelteKit page, `+page.ts` can hydrate the query cache with the first -page so SSR renders rows immediately. Subsequent pages still go through `createQuery`. - -```ts -// +page.ts -export const load = async ({ fetch }) => { - const initial = await fetchPeople({ pageIndex: 0, pageSize: 10 }, fetch) - return { initial } -} -``` - -```svelte - -``` - -## Common failure modes - -- **Forgot `rowCount`.** Pager shows zero pages. -- **No `placeholderData: keepPreviousData`.** Empty-table flash on every page change. -- **Forgot `manualPagination: true`.** Table tries to paginate the already-paged window. - `getPageCount()` returns 1. -- **Driver state in `queryKey` is stale.** Always pass the current value, not a captured one. -- **No reset on filter change.** Stays on dead pages. -- **Plain `data: dataQuery.data?.rows`.** No reactivity — must be a getter. -- **Re-creating `createQuery` inside `$effect`.** It's a one-time call; create it at component init. -- **Reimplementing pagination math against `query.data` instead of calling - `table.nextPage()`.** Don't. - -## Related skills - -- `tanstack-table/svelte/client-to-server` — base server-side pattern (without Query). -- `tanstack-table/svelte/compose-with-tanstack-store` — atom-based driver state. -- `tanstack-table/svelte/compose-with-tanstack-pacer` — debounce / throttle high-frequency inputs. -- `tanstack-table/core/pagination` — manual mode semantics. diff --git a/packages/svelte-table/skills/svelte/compose-with-tanstack-store/SKILL.md b/packages/svelte-table/skills/svelte/compose-with-tanstack-store/SKILL.md deleted file mode 100644 index 2026684370..0000000000 --- a/packages/svelte-table/skills/svelte/compose-with-tanstack-store/SKILL.md +++ /dev/null @@ -1,270 +0,0 @@ ---- -name: svelte/compose-with-tanstack-store -description: > - TanStack Table v9 is built on TanStack Store. Each state slice (sorting, pagination, - rowSelection, columnFilters, ...) is a separate atom. In Svelte, `@tanstack/svelte-store` - exposes `createAtom`, `useSelector`, `shallow`. Read `table.atoms.` per slice, - `table.store` flat, or `table.state` for the selector projection. Subscribe with - `subscribeTable(atom, selector?)` (returns `.current`). Own a slice externally with - `createAtom` + `atoms: { sorting: sortingAtom }`. Svelte 5+ only — `$state` / `$derived.by` / - `$effect.pre` reactivity. -type: composition -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - state-management -sources: - - TanStack/table:docs/framework/svelte/guide/table-state.md - - TanStack/table:packages/svelte-table/src/reactivity.svelte.ts - - TanStack/table:packages/svelte-table/src/subscribe.ts - - TanStack/table:examples/svelte/basic-external-atoms/ ---- - -# Compose with TanStack Store (Svelte) - -`@tanstack/svelte-store` is the reactive primitive under `@tanstack/svelte-table` v9. The -table doesn't merely _use_ Store — its entire reactivity model is built from Store atoms with -rune backings. - -## Mental model — three read surfaces - -A registered v9 table exposes: - -| Surface | Shape | When to use | -| --------------------- | --------------------------- | ------------------------------------------ | -| `table.atoms.` | `ReadonlyAtom` | Per-slice subscription / `.get()` snapshot | -| `table.store` | `ReadonlyStore` | Flat snapshot across registered slices | -| `table.state` | `TSelected` (from selector) | The selector projection (Svelte-only) | - -Plus the writable internals: - -- `table.baseAtoms.` — writable atom for state the table owns. - -If a slice is supplied externally via `atoms`, `table.atoms.` reads from your atom and -`table.baseAtoms.` is unused for that slice. - -## The Svelte bindings (what `svelteReactivity()` actually does) - -The Svelte adapter ships `svelteReactivity()` and installs it as `coreReactivityFeature`. It -maps Store primitives to runes: - -- Readonly atoms → `$derived.by(fn)` -- Writable atoms → `$state(initialValue)` -- Subscriptions → `$effect.root` + `$effect` -- Batch → `flushSync` - -This is why simple atom reads inside `.svelte` components (templates, `$derived`, `$effect`) -participate in reactivity automatically. There is no React-style `useStore` requirement. - -## Pattern 1 — Read a slice without subscribing - -For event handlers, async work, exports, anything outside of reactive markup. Cheap, no -subscription setup. - -```ts -import type { SortingState } from '@tanstack/svelte-table' - -function logSort() { - const sorting: SortingState = table.atoms.sorting.get() - console.log(sorting) -} -``` - -`table.state` is the full snapshot equivalent. - -## Pattern 2 — Reactive selector via `createTable` - -The second argument to `createTable` is a TanStack Store selector. The result is exposed on -`table.state`. The default selector is `(state) => state`. - -```svelte - - -Page {table.state.pageIndex + 1} -``` - -The narrower the selector, the less your markup re-renders. - -## Pattern 3 — Per-block subscription with `subscribeTable` - -`subscribeTable(source, selector?)` is the dedicated per-component subscription. It uses -`shallow` compare and exposes a `.current` accessor. - -```svelte - - -Page {pagination.current.pageIndex + 1} ({pageSize.current} per page) -``` - -Inside per-row components, `subscribeTable(table.atoms.rowSelection, (s) => !!s[row.id])` keeps -that row's checkbox reactive without subscribing to the entire selection map. - -## Pattern 4 — Own a slice externally with `createAtom` - -When the app should own a slice — share across components, sync with URL, persist to storage — -create a stable atom and hand it to the table via `atoms`. - -```ts -import { createAtom, useSelector } from '@tanstack/svelte-store' -import { - createTable, - rowPaginationFeature, - rowSortingFeature, - tableFeatures, - type PaginationState, - type SortingState, -} from '@tanstack/svelte-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, -}) - -const sortingAtom = createAtom([]) -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) - -// Optional: a Svelte-reactive view onto each atom for use in markup. -const sorting = useSelector(sortingAtom) -const pagination = useSelector(paginationAtom) - -const table = createTable({ - features, - columns, - get data() { - return data - }, - atoms: { - sorting: sortingAtom, - pagination: paginationAtom, - }, -}) - -// table.setPageIndex(2) writes through paginationAtom. -// paginationAtom.set(...) updates table.atoms.pagination immediately. -``` - -Atom precedence: external `atoms.` wins over external `state.` which writes -into the internal `baseAtoms.`. **Never combine them on the same slice.** - -## Pattern 5 — Cross-component / cross-module state - -Because atoms are first-class subscribable values, you can read them outside the table component. - -```ts -// stores/table-state.ts -import { createAtom } from '@tanstack/svelte-store' -import type { RowSelectionState } from '@tanstack/svelte-table' - -export const rowSelectionAtom = createAtom({}) -``` - -```svelte - - - - -``` - -```svelte - - -``` - -## Pattern 6 — `useSelector` with custom equality - -`useSelector(source, selector, { compare })` lets you switch comparison strategies — useful -for object selectors so you don't re-fire on every reference change. - -```ts -import { shallow, useSelector } from '@tanstack/svelte-store' - -const filterValues = useSelector( - table.atoms.columnFilters, - (filters) => Object.fromEntries(filters.map((f) => [f.id, f.value])), - { compare: shallow }, -) -``` - -`subscribeTable` already uses `shallow` by default, so prefer it for table sources unless you -need a custom compare. - -## Pattern 7 — Direct base-atom writes (last resort) - -When a slice is internally owned and you really need to write outside a feature API: - -```ts -table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 })) -``` - -Do not do this for externally-owned slices — write to your external atom instead. The base -atom is dormant in that case and your write will be silently ignored next sync. - -## Common failure modes - -- **Reading a slice that wasn't registered.** `table.atoms.rowSelection` is `undefined` if - `rowSelectionFeature` isn't in `features`. TS will catch it if you used `tableFeatures()`. -- **Creating atoms inside reactive blocks.** Atoms must be stable. Module scope or top-level - component scope, never inside `$derived` / `$effect`. -- **`useSelector` without `.current`.** `selection.pageIndex` is wrong — `selection.current.pageIndex`. -- **Mixing `atoms.X` and `state.X`.** Atom wins, callback never fires. -- **`tableState` as a plain object.** No reactivity. Use `subscribeTable`, `useSelector`, or - the `createTable` selector. -- **Reimplementing `useSelector` with `$effect`.** Built-in is more efficient and uses - shallow compare. - -## Related skills - -- `tanstack-table/svelte/table-state` — full reactivity model and selector patterns. -- `tanstack-table/core/state-management` — atom precedence rules. -- `tanstack-table/svelte/client-to-server` — atoms as the data-driver for server queries. -- `tanstack-table/svelte/production-readiness` — selector / subscription tuning. diff --git a/packages/svelte-table/skills/svelte/compose-with-tanstack-virtual/SKILL.md b/packages/svelte-table/skills/svelte/compose-with-tanstack-virtual/SKILL.md deleted file mode 100644 index 954015dbdf..0000000000 --- a/packages/svelte-table/skills/svelte/compose-with-tanstack-virtual/SKILL.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -name: svelte/compose-with-tanstack-virtual -description: > - `@tanstack/svelte-table` does not include virtualization — pair it with - `@tanstack/svelte-virtual`. Use `createVirtualizer({ count, estimateSize, getScrollElement, - ... })`, feed `table.getRowModel().rows.length` as `count`, render only - `$rowVirtualizer.getVirtualItems()`, position rows with `transform: translateY(...)` and a - container of `getTotalSize()`. Use `measureElement` actions for dynamic row heights. Svelte 5+ - only — `$state` for refs, `$effect` to sync count. -type: composition -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - svelte/table-state - - row-expanding -sources: - - TanStack/table:docs/guide/virtualization.md - - TanStack/table:examples/svelte/virtualized-rows/ - - TanStack/table:examples/svelte/virtualized-columns/ - - TanStack/table:examples/svelte/virtualized-infinite-scrolling/ ---- - -# Compose with TanStack Virtual (Svelte) - -TanStack Table is **not** a virtualizer. For lists / grids past a few thousand rows (or with -heavy per-row markup), pair it with `@tanstack/svelte-virtual`. - -## Install - -```bash -pnpm add @tanstack/svelte-virtual -``` - -## Core mental model - -- TanStack Table gives you `rows: row[]` (already filtered / sorted / paged / grouped). -- TanStack Virtual takes `count` (the length) and returns `virtualItems` (the slice currently - in view). -- You render only those virtual items, absolutely positioned, inside a container sized to - `getTotalSize()` pixels. - -`createVirtualizer` returns a Svelte store. Read its current value with `$rowVirtualizer` or -`get(rowVirtualizer)` (from `svelte/store`). - -## Basic row virtualization - -```svelte - - -
- - - {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - - {#each headerGroup.headers as header (header.id)} - - {/each} - - {/each} - - - {#each $rowVirtualizer.getVirtualItems() as virtualRow (virtualRow.index)} - {@const row = rows[virtualRow.index]} - - {#each row.getAllCells() as cell (cell.id)} - - {/each} - - {/each} - -
- -
- -
-
-``` - -Why `display: grid` / `flex` instead of native table layout? Because the rows are absolutely -positioned, the browser's native table layout algorithm can't size columns from non-flowing -rows. CSS layout takes over. - -## Dynamic row heights (`measureElement`) - -For variable row heights (multi-line cells, expanding rows), measure rendered nodes with the -virtualizer's `measureElement` API. - -```svelte - - -... -``` - -`data-index` is required — the virtualizer uses it to map a measured element back to its -virtual item. - -> Firefox measures table-border rows incorrectly. The above guards against measuring there and -> falls back to the estimate. - -## Column virtualization - -`createVirtualizer` with `horizontal: true` against `table.getVisibleLeafColumns()`. Same -pattern — only render `getVirtualItems()` cells per row, position with `translateX`. - -```ts -const columnVirtualizer = createVirtualizer({ - get count() { - return visibleColumns.length - }, - estimateSize: (index) => visibleColumns[index].getSize(), - getScrollElement: () => tableContainerRef ?? null, - horizontal: true, - overscan: 3, -}) -``` - -For combined row + column virtualization, render the row virtualizer's items, and inside each -row render the column virtualizer's items. See `examples/svelte/virtualized-columns/`. - -## Infinite scroll (load more on near-bottom) - -Subscribe to the virtualizer's `getVirtualItems()` and check the last one's index against your -total available count. - -```ts -import { createInfiniteQuery } from '@tanstack/svelte-query' - -const infiniteQuery = createInfiniteQuery(() => ({ - queryKey: ['people-infinite'], - queryFn: ({ pageParam }) => fetchPeople({ cursor: pageParam, pageSize: 50 }), - initialPageParam: undefined as string | undefined, - getNextPageParam: (last) => last.nextCursor, -})) - -const flatData = $derived( - infiniteQuery.data?.pages.flatMap((p) => p.rows) ?? [], -) - -const table = createTable({ - features: tableFeatures({}), - columns, - get data() { - return flatData - }, -}) - -const rows = $derived(table.getRowModel().rows) - -const rowVirtualizer = createVirtualizer({ - get count() { - return rows.length - }, - estimateSize: () => 33, - getScrollElement: () => tableContainerRef ?? null, - overscan: 10, -}) - -$effect(() => { - const items = $rowVirtualizer.getVirtualItems() - const last = items[items.length - 1] - if ( - last && - last.index >= rows.length - 1 && - infiniteQuery.hasNextPage && - !infiniteQuery.isFetchingNextPage - ) { - infiniteQuery.fetchNextPage() - } -}) -``` - -## Interaction with row expanding - -If `rowExpandingFeature` is registered, `table.getRowModel().rows` already flattens expanded -sub-rows into a single sequential list. The virtualizer just sees a longer list — no special -handling needed. - -For variable row heights driven by expand state, you'll want `measureElement` so the -container resizes when a row expands. - -## Pagination vs. virtualization - -Pick one. Virtualization is for "render all rows but render only the visible window". -Pagination is for "the user navigates discrete pages". Combining them usually means you don't -need either — drop pagination and let the virtualizer handle the rendering window. - -## Common failure modes - -- **Forgot to push `count` updates.** `svelte-virtual` does not auto-track `get count()` — - use `$effect` + `setOptions({ count })`. -- **Native table layout with virtualized rows.** Columns collapse because absolutely - positioned rows don't contribute to layout. Use `display: grid` / `flex`. -- **No `data-index` on ``.** `measureElement` can't map back to virtual items. -- **No `transform: translateY`.** Rows render at `top: 0` and stack visually. -- **Missing container `height`.** No overflow, no scroll, no virtualization. -- **Calling `get(rowVirtualizer).getVirtualItems()` in template.** Wrong access pattern; - use `$rowVirtualizer.getVirtualItems()` (store auto-subscribe) or be sure to - `import { get } from 'svelte/store'`. -- **Reimplementing windowing manually.** Don't. - -## Related skills - -- `tanstack-table/svelte/table-state` — `getRowModel()` and the reactivity model. -- `tanstack-table/core/row-expanding` — flattening sub-rows for virtualization. -- `tanstack-table/svelte/compose-with-tanstack-query` — infinite-scroll data source. diff --git a/packages/svelte-table/skills/svelte/getting-started/SKILL.md b/packages/svelte-table/skills/svelte/getting-started/SKILL.md deleted file mode 100644 index 73539cc89d..0000000000 --- a/packages/svelte-table/skills/svelte/getting-started/SKILL.md +++ /dev/null @@ -1,338 +0,0 @@ ---- -name: svelte/getting-started -description: > - End-to-end first-table journey for `@tanstack/svelte-table@9` on Svelte 5. Install the adapter, - declare `features` with `tableFeatures()` (including row-model factories and `*Fns` registries as - feature slots), build a typed column helper with both `TFeatures` and `TData` generics, instantiate - the table with `createTable(options)` using `$state` data and `get data()` reactive option getters, - and render with `FlexRender`. Svelte 5+ only — Svelte 3/4 must use v8. -type: lifecycle -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - setup - - column-definitions - - state-management - - svelte/table-state -sources: - - TanStack/table:docs/installation.md - - TanStack/table:docs/framework/svelte/svelte-table.md - - TanStack/table:examples/svelte/basic-create-table/ - - TanStack/table:examples/svelte/basic-app-table/ - - TanStack/table:examples/svelte/basic-snippets/ - - TanStack/table:packages/svelte-table/src/index.ts ---- - -# Getting Started — Svelte - -A first working `@tanstack/svelte-table` v9 table from a blank Svelte 5 project. Read this -end-to-end before searching the docs — the v9 shape diverges enough from v8 (and from your -muscle memory) that skimming will produce broken code. - -## CRITICAL: Svelte version - -**`@tanstack/svelte-table@9` requires Svelte 5 or newer.** The adapter is built on runes -(`$state`, `$derived.by`, `$effect.pre`). If your project is on Svelte 3 or 4, do **one** of: - -- Upgrade the project to Svelte 5, then install v9. -- Stay on `@tanstack/svelte-table@8` for that project. - -There is no shim, no `/legacy` export, and no support path that runs v9 on Svelte 4. - -## 1. Install - -```bash -pnpm add @tanstack/svelte-table -# optional: external atoms / fine-grained selectors -pnpm add @tanstack/svelte-store -``` - -You do **not** install `@tanstack/table-core` separately — the Svelte adapter re-exports -everything you need (column helpers, feature objects, row-model factories, types). - -## 2. Define `features` - -v9 is explicit. You opt in to every feature and every row model. The core row model is -included by default, so the minimum viable table is: - -```ts -const features = tableFeatures({}) -``` - -For anything beyond a flat table, register the features you'll use along with the matching -row-model factories and `*Fns` registries — all as slots inside `tableFeatures`: - -```ts -import { - columnFilteringFeature, - createFilteredRowModel, - createPaginatedRowModel, - createSortedRowModel, - filterFns, - rowPaginationFeature, - rowSortingFeature, - sortFns, - tableFeatures, -} from '@tanstack/svelte-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - columnFilteringFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - sortFns, - filterFns, -}) -``` - -**Skipping a feature** in `features` means its state slice does not exist on `table.atoms`, -its options on `createTable` do nothing, and its derived APIs (`table.setSorting`, -`column.getCanSort`) are not on the instance. - -## 3. Type your data and define columns - -```ts -type Person = { - firstName: string - lastName: string - age: number - visits: number - status: 'relationship' | 'complicated' | 'single' - progress: number -} -``` - -Both `ColumnDef` and the column helper take the two generics ``: - -```ts -import { createColumnHelper, type ColumnDef } from '@tanstack/svelte-table' - -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - header: 'First Name', - cell: (info) => info.getValue(), - }), - columnHelper.accessor((row) => row.lastName, { - id: 'lastName', - header: () => 'Last Name', - cell: (info) => info.getValue(), - }), - columnHelper.accessor('age', { header: 'Age' }), - columnHelper.accessor('visits', { header: 'Visits' }), - columnHelper.accessor('status', { header: 'Status' }), - columnHelper.accessor('progress', { header: 'Profile Progress' }), -]) -``` - -Or use raw `ColumnDef` arrays if you don't want the helper: - -```ts -const columns: Array> = [ - { - accessorKey: 'firstName', - header: 'First Name', - cell: (info) => info.getValue(), - }, -] -``` - -## 4. Create the table - -Use Svelte 5 `$state` for the data and pass it through a **reactive getter** so the table -re-evaluates `data` when the rune changes. The same pattern applies for any other reactive -option (`columns`, `rowCount`, `state.*`). - -```svelte - -``` - -`createTable` syncs options inside `$effect.pre`, so external `$state` updates flow into the -table **before** the DOM reads `getRowModel()` — no stale-frame bugs. - -## 5. Render with `FlexRender` - -`FlexRender` handles plain strings, function renderers, component renderers -(`renderComponent`), and snippet renderers (`renderSnippet`). - -```svelte - - - - - - - {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - - {#each headerGroup.headers as header (header.id)} - - {/each} - - {/each} - - - {#each table.getRowModel().rows as row (row.id)} - - {#each row.getAllCells() as cell (cell.id)} - - {/each} - - {/each} - -
- {#if !header.isPlaceholder} - - {/if} -
-``` - -**Key the `{#each}` blocks on stable ids.** Without keys, Svelte recreates nodes and loses -focus, scroll, and any per-row component state. - -## 6. Adding a feature — pagination - -```svelte - - -
- - Page {table.atoms.pagination.get().pageIndex + 1} of {table.getPageCount()} - -
-``` - -To make pagination reactive in the controls, either pass a selector to `createTable` or use -`subscribeTable`: - -```ts -import { subscribeTable } from '@tanstack/svelte-table' - -const pagination = subscribeTable(table.atoms.pagination) -// pagination.current.pageIndex is reactive -``` - -## 7. `createTableHook` (when you have more than one table) - -For apps with multiple tables, define `features` (including row-model factories) and shared components once: - -```ts -// hooks/table.ts -import { - createPaginatedRowModel, - createSortedRowModel, - createTableHook, - rowPaginationFeature, - rowSortingFeature, - sortFns, - tableFeatures, -} from '@tanstack/svelte-table' - -export const { createAppTable, createAppColumnHelper } = createTableHook({ - features: tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - sortFns, - }), -}) -``` - -```svelte - -``` - -## Common failure modes - -- **Svelte 3/4.** Adapter will not work. See top of file. -- **`createSvelteTable` / `useSvelteTable` / `getCoreRowModel`** — all v8 names. v9 uses - `createTable` with row-model factories registered as slots in `tableFeatures({ paginatedRowModel: createPaginatedRowModel(), ... })`. -- **Plain `data` instead of `get data()` getter.** Table will not see data updates. Always - pass a reactive getter for state that lives in `$state`. -- **Missing feature in `features`.** `table.setSorting` / `column.getCanSort` won't exist. - TypeScript will tell you; if it doesn't, you're missing the `` - generics on the column helper or `ColumnDef`. -- **Plain object instead of `tableFeatures({...})`.** Loses typed atom keys; you'll get - `unknown` state shapes everywhere. -- **Unkeyed `{#each}` blocks.** Reuse bugs (focus jumps, wrong row selected). -- **Reimplementing built-ins.** If you write a manual sort comparator across rows, you're - re-doing `rowSortingFeature`. Register it instead. - -## Next steps - -- `tanstack-table/svelte/table-state` — reactivity model, selectors, subscribeTable, ownership. -- `tanstack-table/core/filtering` / `pagination` / `sorting` / `row-selection` — feature-by-feature. -- `tanstack-table/svelte/compose-with-tanstack-query` — server-side data. -- `tanstack-table/svelte/compose-with-tanstack-virtual` — large datasets. -- `tanstack-table/svelte/production-readiness` — selector tuning, bundle size. diff --git a/packages/svelte-table/skills/svelte/migrate-v8-to-v9/SKILL.md b/packages/svelte-table/skills/svelte/migrate-v8-to-v9/SKILL.md deleted file mode 100644 index 346d90bfd7..0000000000 --- a/packages/svelte-table/skills/svelte/migrate-v8-to-v9/SKILL.md +++ /dev/null @@ -1,264 +0,0 @@ ---- -name: svelte/migrate-v8-to-v9 -description: > - Mechanical migration from `@tanstack/svelte-table@8` to `@tanstack/svelte-table@9`. v9 in Svelte - is a full rewrite — Svelte 5 runes only (no Svelte 3/4), no `/legacy` adapter (unlike React), - `createSvelteTable` → `createTable`, `getCoreRowModel` / `getSortedRowModel` factories → - row-model factories registered as slots inside `tableFeatures({...})`, `flexRender` helper → - `` component, writable-store `state` → rune-based getters / external atoms, - `onStateChange` → per-slice `on[State]Change` or `atoms`. Plan a feature-by-feature audit, not a - search-and-replace. -type: lifecycle -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - column-definitions -sources: - - TanStack/table:docs/framework/svelte/svelte-table.md - - TanStack/table:docs/framework/svelte/guide/table-state.md - - TanStack/table:packages/svelte-table/src/ - - TanStack/table:examples/svelte/basic-create-table/ - - TanStack/table:examples/svelte/basic-external-atoms/ - - TanStack/table:examples/svelte/basic-external-state/ ---- - -# Migrate v8 → v9 (Svelte) - -## CRITICAL: v9 = Svelte 5 - -**v9 of the Svelte adapter only supports Svelte 5+.** No backport. No shim. No `/legacy` import. -The v9 adapter is built on Svelte 5 runes (`$state`, `$derived.by`, `$effect.pre`). If your app -is still on Svelte 3/4, you have two real options: - -1. Stay on `@tanstack/svelte-table@8`. v8 keeps working with the Svelte 3/4 writable-store API. -2. Migrate the app to Svelte 5 first, then migrate the table. - -There is no third option. Trying to install v9 on a Svelte 4 codebase will error at compile. - -> This makes the Svelte migration heavier than React's. React has a `/legacy` re-export that -> mirrors the v8 surface; Svelte does not. Plan for a real rewrite of every table screen. - -## What changed at the type / API level - -| v8 | v9 | -| -------------------------------------------------- | ------------------------------------------------------------------------------------- | -| `createSvelteTable(options)` | `createTable(options, selector?)` | -| `getCoreRowModel()` | included by default; no factory | -| `getPaginationRowModel()` | `tableFeatures({ paginatedRowModel: createPaginatedRowModel() })` | -| `getSortedRowModel()` | `tableFeatures({ sortedRowModel: createSortedRowModel(), sortFns })` | -| `getFilteredRowModel()` | `tableFeatures({ filteredRowModel: createFilteredRowModel(), filterFns })` | -| `getExpandedRowModel()` | `tableFeatures({ expandedRowModel: createExpandedRowModel() })` | -| `getGroupedRowModel()` | `tableFeatures({ groupedRowModel: createGroupedRowModel(), aggregationFns })` | -| `getFacetedRowModel()` / `MinMax` / `UniqueValues` | facet APIs auto-derived when `*Facet*Feature` registered | -| `flexRender(template, ctx)` helper | `` / `` / `` | -| `ColumnDef` | `ColumnDef` (extra generic) | -| `createColumnHelper()` | `createColumnHelper()` | -| writable store on the table instance | `table.atoms.` + `table.store` + `table.state` | -| `onStateChange` (monolithic) | per-slice `on[State]Change`, or external `atoms` | -| `useSvelteTable` (rare) | gone | - -## What did NOT change - -- The data array is still the source of truth; columns are still defined the same way - shape-wise (`accessorKey` / `accessorFn` / `header` / `cell` / `footer`). -- Filter, sort, aggregation function registries are still `filterFns`, `sortFns`, - `aggregationFns` — but now passed into row-model factories instead of being auto-resolved. -- Feature APIs (`table.nextPage()`, `column.getCanSort()`, `row.toggleSelected()`) keep the - same names. - -## Migration checklist (per file) - -For each Svelte component that uses the table: - -1. **Upgrade Svelte.** Ensure the app is on Svelte 5 and the component compiles in runes mode. -2. **Replace store with rune.** `let data = writable([])` → `let data = $state([])`. Reads - inside markup are no longer `$data` — just `data`. -3. **Import surface.** `import { createSvelteTable, getCoreRowModel, flexRender }` → - `import { createTable, FlexRender, tableFeatures, ... }`. -4. **Add `features`.** Create `const features = tableFeatures({ ... only the features this -table uses ... })`. -5. **Move row-model factories into `tableFeatures`.** Every `get*RowModel: get*RowModel()` becomes a slot inside `tableFeatures({ ..., create*RowModel() })`. Any `*Fns` registry that was passed as a factory argument is now also a named slot in the same `tableFeatures` call. -6. **Generic columns.** Add `` to `ColumnDef<>` and - `createColumnHelper<>()`. TypeScript will tell you when you missed one. -7. **`data` as a getter.** `data: data` → `get data() { return data }`. Same for any other - reactive option (`state.*`, `columns`, `rowCount`). -8. **State.** Pick the new ownership model — see below. -9. **Rendering.** `{flexRender(header.column.columnDef.header, header.getContext())}` → - ``. Same for cells and footers. -10. **Components / snippets in cells.** v8: pass a Svelte component constructor. v9: wrap with - `renderComponent(MyCell, props)` or `renderSnippet(snippet, args)`. - -## State ownership — choose one per slice - -### Was: `writable` store + `onStateChange` - -```svelte - -``` - -### Now: pick one of three - -**Internal (default).** Pass only `initialState` and let the table own it. - -```ts -const table = createTable({ - features, - columns, - get data() { - return data - }, - initialState: { - pagination: { pageIndex: 0, pageSize: 25 }, - }, -}) -``` - -**External `state` + per-slice `on[State]Change`.** Closest to a literal v8 port. - -```svelte - -``` - -**External atoms (preferred for shared state).** Atomic, subscribable from anywhere. - -```ts -import { createAtom, useSelector } from '@tanstack/svelte-store' - -const sortingAtom = createAtom([]) -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) - -const sorting = useSelector(sortingAtom) -const pagination = useSelector(paginationAtom) - -const table = createTable({ - features, - columns, - get data() { - return data - }, - atoms: { - sorting: sortingAtom, - pagination: paginationAtom, - }, -}) -``` - -**Do not combine** `state.pagination` with `atoms.pagination` — atoms always win, `state` is -discarded silently, you'll think your callbacks aren't firing. - -## Rendering migration cheat sheet - -```svelte - - - {#if !header.isPlaceholder} - - {/if} - - - - - {#if !header.isPlaceholder} - - {/if} - -``` - -For cells that render a custom Svelte component: - -```svelte - -{ cell: () => MyCellComponent } - - -import { renderComponent } from '@tanstack/svelte-table' -{ cell: ({ row }) => renderComponent(MyCellComponent, { row }) } -``` - -For inline snippets: - -```svelte - -import { renderSnippet } from '@tanstack/svelte-table' - -{#snippet myCell(row)} - {row.original.firstName} -{/snippet} - -{ cell: ({ row }) => renderSnippet(myCell, row) } -``` - -## After the rewrite — verify - -- `pnpm test:types` (or `svelte-check`) catches missing `` generics, - missing `features` or row-model factory slots, and feature APIs called on tables that didn't - register them. -- `pnpm build` should pass with the new `features` set. Bundle should shrink — only the - features you register are included. -- Click through every table screen. Reset buttons, multi-sort, pagination reset on filter, - expanded-row count under pagination — these are all the spots where v8 muscle memory - reaches for an option (`autoResetPageIndex` etc.) that's still named the same in v9 but - only takes effect if the matching feature is registered. - -## Common failure modes during migration - -- **Trying to skip the Svelte 5 upgrade.** Will not work. -- **Reaching for `useLegacyTable`.** Doesn't exist in `@tanstack/svelte-table`. That's a - React-only escape hatch. -- **Importing from `@tanstack/table-core` directly.** Re-exported by the adapter — go through - the adapter. -- **Plain `data` instead of `get data()` getter.** Largest single source of "but my data - changed" bugs during migration. -- **Plain `features: { rowPaginationFeature }` instead of `tableFeatures({...})`.** Loses - inference; you'll get `any` everywhere. -- **Forgetting feature registration after adding the row-model factory slot.** Adding - `paginatedRowModel: createPaginatedRowModel()` to `tableFeatures` without also including - `rowPaginationFeature` does nothing. -- **Reimplementing v8 helpers** (`flexRender`-style functions, manual store subscriptions, - hand-rolled selectors) instead of using `FlexRender` / `subscribeTable` / atom selectors. - That's the #1 AI tell on this migration. - -## Related skills - -- `tanstack-table/svelte/getting-started` — clean-slate setup. -- `tanstack-table/svelte/table-state` — the v9 state model in depth. -- `tanstack-table/core/state-management` — atom precedence rules. -- `tanstack-table/svelte/production-readiness` — post-migration tuning. diff --git a/packages/svelte-table/skills/svelte/production-readiness/SKILL.md b/packages/svelte-table/skills/svelte/production-readiness/SKILL.md deleted file mode 100644 index bab77d425f..0000000000 --- a/packages/svelte-table/skills/svelte/production-readiness/SKILL.md +++ /dev/null @@ -1,258 +0,0 @@ ---- -name: svelte/production-readiness -description: > - Ship-ready optimizations for `@tanstack/svelte-table@9` on Svelte 5. Tree-shake by registering - ONLY the `features` you use; keep `features`, `columns`, and `data` stable; replace broad - `(state) => state` selectors with narrow projections on `createTable`; reach for - `subscribeTable(atom, selector?)` when only one block of markup should react; lean on rune-aware - atom reads (`table.atoms..get()`) for non-reactive paths; key every `{#each}` block on - stable ids; debounce high-frequency writes with `@tanstack/svelte-pacer`. Svelte 5+ only. -type: lifecycle -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - svelte/table-state -sources: - - TanStack/table:docs/guide/features.md - - TanStack/table:docs/framework/svelte/guide/table-state.md - - TanStack/table:packages/svelte-table/src/createTable.svelte.ts - - TanStack/table:packages/svelte-table/src/subscribe.ts - - TanStack/table:examples/svelte/basic-external-atoms/ - - TanStack/table:examples/svelte/virtualized-rows/ ---- - -# Production Readiness — Svelte - -Once your tables work, this is the checklist for making them fast and small. Most of these are -v9-specific — v8 tables won't have any of these levers. - -## 1. Register only the features you use - -`features` is the bundle gate. Any feature you don't register is tree-shaken out — including -its state slice, its API surface, and its reactive plumbing. - -```ts -// good — minimal table, ~smallest bundle -const features = tableFeatures({}) - -// good — feature-by-feature opt-in -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, -}) - -// bad — kitchen sink, every state slice created even when unused -const features = tableFeatures({ - columnFilteringFeature, - columnGroupingFeature, - columnOrderingFeature, - columnPinningFeature, - columnResizingFeature, - columnSizingFeature, - columnVisibilityFeature, - globalFilteringFeature, - rowExpandingFeature, - rowPaginationFeature, - rowPinningFeature, - rowSelectionFeature, - rowSortingFeature, -}) -``` - -If you find yourself running a table without a feature's UI ever showing, drop the feature. - -## 2. Stable identities for `features`, `columns`, `data` - -`createTable` syncs options in `$effect.pre`. If any of these identities flip every component -run, the table re-syncs more than it needs to. - -- **`features`**: declare at module scope, not inside the component function, and never - inside `$derived` / `$effect`. The `features` object now also carries row-model factories - and `*Fns` registries, so keeping it stable prevents unnecessary re-sync. -- **`columns`**: same — module scope or `$state.frozen` / a non-reactive `const` in the - component. Reactive recompute of columns is rare and almost always a bug. -- **`data`**: pass with a getter (`get data()`) so the reference is stable when the data - doesn't change. If you're reshaping data inside the component, do it once in a `$derived`, - not on every read. - -```svelte - -``` - -## 3. Narrow the `table.state` selector - -The default selector is `(state) => state`. That makes `table.state` re-run any consumer when -**any** slice changes. Pass a focused selector and you only re-render markup that actually -depends on that slice. - -```ts -// good — only re-projects when pagination changes -const table = createTable(options, (state) => ({ - pagination: state.pagination, -})) - -// even better — only what your UI actually reads -const table = createTable(options, (state) => ({ - pageIndex: state.pagination.pageIndex, - pageSize: state.pagination.pageSize, -})) -``` - -If different parts of the UI need different slices, **don't** widen the selector — use -`subscribeTable` instead (next section). - -## 4. Reach for `subscribeTable` for fine-grained reactivity - -`subscribeTable(source, selector?)` is the per-block subscription. It returns an object whose -`.current` re-runs only when the selected value changes (shallow compared). - -```svelte - - - -``` - -Use it inside per-row components — the row block re-renders only on its own selection toggle, -not on every row's toggle. - -## 5. Non-reactive reads where you don't need reactivity - -Inside event handlers, derived calculations, or one-shot logic, read atoms directly. Cheaper -than subscribing. - -```ts -function exportSelected() { - const selection = table.atoms.rowSelection.get() - const selectedIds = Object.keys(selection).filter((id) => selection[id]) - // ... -} -``` - -`table.state` is the same idea for a full snapshot. - -## 6. Key every `{#each}` block on a stable id - -Svelte without keys recreates nodes on reorder. Result: lost input focus, lost scroll, lost -component state. Every TanStack Table loop has a stable id. - -```svelte -{#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - - {#each headerGroup.headers as header (header.id)} - ... - {/each} - -{/each} - -{#each table.getRowModel().rows as row (row.id)} - - {#each row.getVisibleCells() as cell (cell.id)} - ... - {/each} - -{/each} -``` - -## 7. Don't fight `$effect.pre` - -`createTable` is already syncing options in `$effect.pre`. Don't write a second `$effect` -that calls `table.setOptions` — it'll race with the built-in sync and may render with stale -state. - -If you need to react to an option change with a side effect, put the side effect in your own -`$effect`, not the option write. - -## 8. Debounce high-frequency writes - -Two places will hammer table state at keystroke / pointermove rate: - -- **Filter inputs.** Wrap `setFilterValue` calls with a debounced callback. -- **Column resizing.** v9 commits `columnSizing` continuously by default; use the resize-end - commit mode or debounce. - -```ts -import { createDebouncer } from '@tanstack/svelte-pacer/debouncer' - -const debouncedSetFilter = createDebouncer( - (value: string) => column.setFilterValue(value), - { wait: 200 }, -) -``` - -See the `compose-with-tanstack-pacer` skill for full examples. - -## 9. Virtualization for large datasets - -`getRowModel().rows.length > ~1000` and rows are simple? Performance is fine without -virtualization. Above that, or with heavy per-row markup, use `@tanstack/svelte-virtual`. See -the `compose-with-tanstack-virtual` skill. - -## 10. Don't ship debug flags - -`debugTable: true`, `debugRows`, `debugHeaders` all log. Strip them or gate on `import.meta.env.DEV`. - -## 11. Don't reimplement built-ins - -The #1 production-readiness regression we see in audits: somebody hand-rolled the thing the -table already does. If you're writing any of these, register the feature instead. - -- Hand-rolled sort comparator across rows → `rowSortingFeature` + `createSortedRowModel` -- Hand-rolled page math (`rows.slice(start, end)`) → `rowPaginationFeature` + - `createPaginatedRowModel` -- Hand-rolled selection toggle (`selected[row.id] = !selected[row.id]`) → `rowSelectionFeature` -- Hand-rolled column hide map → `columnVisibilityFeature` -- Hand-rolled column resizer → `columnResizingFeature` -- Hand-rolled debounced filter that doesn't update through `setFilterValue` → - `columnFilteringFeature` + pacer - -Each rewrite breaks tree-shaking, breaks the reset APIs, and breaks devtools introspection. - -## Quick smoke test before shipping - -- Bundle: does the table chunk match the features you registered? (`pnpm build` and inspect.) -- DevTools profiler: clicking sort triggers exactly one re-render of the headers and rows, - not every consumer of `table.state`. -- Resize / filter: no jank, no per-keystroke server hits (pacer / debounce). -- Reload: state restored from your atom / URL / storage, no flicker. -- Stress: 100k-row dataset with virtualization stays interactive. - -## Related skills - -- `tanstack-table/svelte/table-state` — selectors, atoms, subscribeTable. -- `tanstack-table/svelte/compose-with-tanstack-pacer` — debounce patterns. -- `tanstack-table/svelte/compose-with-tanstack-virtual` — virtualization. -- `tanstack-table/svelte/compose-with-tanstack-store` — atom ownership patterns. diff --git a/packages/svelte-table/skills/svelte/table-state/SKILL.md b/packages/svelte-table/skills/svelte/table-state/SKILL.md deleted file mode 100644 index e77d36441e..0000000000 --- a/packages/svelte-table/skills/svelte/table-state/SKILL.md +++ /dev/null @@ -1,437 +0,0 @@ ---- -name: svelte/table-state -description: > - Svelte 5 rune-based reactivity for TanStack Table v9. Covers `createTable(options, selector?)`, - the `table.state` selector projection, fine-grained `subscribeTable(atom, selector?)` with `.current`, - reading and writing `table.atoms` / `table.baseAtoms`, the `svelteReactivity()` bridge that backs - readonly atoms with `$derived.by` and writable atoms with `$state`, and the `$effect.pre` option - sync. State ownership: `initialState`, `state` + `on[State]Change`, or external `atoms` from - `@tanstack/svelte-store` (`createAtom`, `useSelector`). Svelte 5+ only — no Svelte 3/4 support. -type: framework -library: tanstack-table -framework: svelte -library_version: '9.0.0-alpha.48' -requires: - - state-management - - setup -sources: - - TanStack/table:docs/framework/svelte/svelte-table.md - - TanStack/table:docs/framework/svelte/guide/table-state.md - - TanStack/table:packages/svelte-table/src/createTable.svelte.ts - - TanStack/table:packages/svelte-table/src/createTableHook.svelte.ts - - TanStack/table:packages/svelte-table/src/reactivity.svelte.ts - - TanStack/table:packages/svelte-table/src/subscribe.ts - - TanStack/table:examples/svelte/basic-create-table/ - - TanStack/table:examples/svelte/basic-external-atoms/ - - TanStack/table:examples/svelte/basic-external-state/ ---- - -# Svelte Table State, `subscribeTable` & `createTableHook` - -> **TanStack Table is a state-management coordinator for table state.** Understanding how state -> flows through atoms, runes, and selectors is foundational to everything else you do with the -> Svelte adapter. - -## Critical: Svelte 5+ only - -`@tanstack/svelte-table@9` requires **Svelte 5 or newer**. The adapter is built on runes -(`$state`, `$derived.by`, `$effect.pre`). For Svelte 3/4 projects, stay on -`@tanstack/svelte-table@8` — there is no v9 path that supports the legacy stores API. - -## How v9 state is wired in Svelte - -A table instance has three (and a half) state surfaces: - -- `table.baseAtoms.` — writable atoms created from the resolved initial state. -- `table.atoms.` — readonly derived atoms, exposed per registered feature. -- `table.store` — readonly flat TanStack Store, a derived view of all registered atoms. -- `table.state` — the value returned by the optional selector passed as the second argument to - `createTable`. **Svelte-only surface.** - -The Svelte adapter installs `svelteReactivity()` as the `coreReactivityFeature`: - -| Core concept | Svelte binding | -| ------------- | --------------- | -| readonly atom | `$derived.by()` | -| writable atom | `$state` | -| subscription | rune `$effect` | -| option sync | `$effect.pre` | -| batch | `flushSync` | - -`createTable` reads reactive option getters inside `$effect.pre` so the table sees fresh data, -columns, and controlled state **before** the DOM renders — `getRowModel()` is never a frame behind. - -## Feature-based state — registered features only - -State slices only exist for the features registered in `features`. Reading -`table.atoms.rowSelection` without `rowSelectionFeature` is a TypeScript error and a runtime -`undefined`. **This is the most common v9 mistake.** - -```ts -import { - createTable, - rowPaginationFeature, - rowSortingFeature, - tableFeatures, - createPaginatedRowModel, - createSortedRowModel, - sortFns, -} from '@tanstack/svelte-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -const table = createTable({ - features, - columns, - get data() { - return data - }, -}) - -table.atoms.pagination.get() // ok -table.atoms.sorting.get() // ok -// table.atoms.rowSelection // TypeScript error -``` - -## Reading state — pick the right tool for the job - -### Current value, no reactivity - -Read the atom directly. Cheapest path; only reactive when called inside a rune-tracked context. - -```ts -const sorting = table.atoms.sorting.get() -const pagination = table.atoms.pagination.get() -const flat = table.state -``` - -### Reactive read inside markup — `table.state` selector - -Pass a TanStack Store selector as the second argument to `createTable`. The selected value is -exposed as `table.state`. The default selector returns the full registered state. - -```svelte - - - - Page {table.state.pagination.pageIndex + 1} of {table.getPageCount()} - -``` - -### Fine-grained — `subscribeTable` - -`subscribeTable(source, selector?)` wraps a `useSelector` with `shallow` compare and returns an -object whose `.current` is the selected value. Use it when only one block of markup should -re-render on a state change. - -```ts -import { subscribeTable } from '@tanstack/svelte-table' - -const pageIndex = subscribeTable(table.atoms.pagination, (p) => p.pageIndex) -``` - -```svelte -Page {pageIndex.current + 1} -``` - -## Setting state — APIs first, atoms last - -Use the feature APIs. They handle updaters, external-atom routing, and validation: - -```ts -table.nextPage() -table.previousPage() -table.setPageIndex(0) -table.setPageSize(25) -table.setSorting([{ id: 'age', desc: true }]) -column.toggleVisibility() -row.toggleSelected() -``` - -Direct base-atom writes are a last resort: - -```ts -table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 })) -``` - -When a slice is owned by an external atom (passed through `atoms`), write to the external atom — -`table.atoms.` will read from it, not from `baseAtoms`. - -## State ownership — three patterns - -### 1. Initial state only - -The default: set starting values, let the table own the rest. `initialState` also drives -`resetSorting()`, `resetPagination()`, etc. - -```ts -const table = createTable({ - features, - columns, - get data() { - return data - }, - initialState: { - sorting: [{ id: 'age', desc: true }], - pagination: { pageIndex: 0, pageSize: 25 }, - }, -}) -``` - -### 2. External atoms (recommended for shared state) - -When the app should own a slice, create a stable atom with `createAtom`, pass it through `atoms`, -and subscribe with `useSelector` or `subscribeTable`. The table writes through the external atom -on `setPageIndex`, `setSorting`, etc. - -```ts -import { createAtom, useSelector } from '@tanstack/svelte-store' -import { - createTable, - rowPaginationFeature, - rowSortingFeature, - tableFeatures, - type PaginationState, - type SortingState, -} from '@tanstack/svelte-table' - -const features = tableFeatures({ - rowPaginationFeature, - rowSortingFeature, -}) - -const sortingAtom = createAtom([]) -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) - -const sorting = useSelector(sortingAtom) -const pagination = useSelector(paginationAtom) - -const table = createTable({ - features, - columns, - get data() { - return data - }, - atoms: { - sorting: sortingAtom, - pagination: paginationAtom, - }, -}) -``` - -When you use `atoms` for a slice, **do not** pair it with the matching `on[State]Change` callback. - -### 3. External `state` + `on[State]Change` (migration / simple cases) - -Classic pattern, still supported. Use Svelte 5 `$state` and getter properties so the table sees -updates. - -```svelte - -``` - -> The v8-style monolithic `onStateChange` is gone in v9. Use per-slice `on[State]Change` or, better, -> external atoms. - -### Precedence — do not mix sources - -External `atoms` win over external `state`. External `state` syncs into the internal base atom. -For any given slice, pick **one** source of truth. Don't pass `initialState.pagination` and -`atoms.pagination` and `state.pagination` together. - -## Rendering — `FlexRender` - -`FlexRender` handles `header`, `cell`, and `footer` definitions whether they're plain strings, -Svelte components wrapped with `renderComponent`, or snippets wrapped with `renderSnippet`. - -```svelte - - - - {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - - {#each headerGroup.headers as header (header.id)} - - {#if !header.isPlaceholder} - - {/if} - - {/each} - - {/each} - - - {#each table.getRowModel().rows as row (row.id)} - - {#each row.getVisibleCells() as cell (cell.id)} - - {/each} - - {/each} - -``` - -Always key `{#each}` blocks on stable ids (`headerGroup.id`, `header.id`, `row.id`, `cell.id`). - -## `createTableHook` — app-wide composition - -Create one configured hook per app: shared `features` (including row-model factories), defaults, and pre-bound component registries. - -```ts -import { - createPaginatedRowModel, - createSortedRowModel, - createTableHook, - rowPaginationFeature, - rowSortingFeature, - sortFns, - tableFeatures, -} from '@tanstack/svelte-table' -import TextCell from './cells/TextCell.svelte' -import SortIndicator from './headers/SortIndicator.svelte' - -export const { - createAppTable, - createAppColumnHelper, - useTableContext, - useCellContext, - useHeaderContext, -} = createTableHook({ - features: tableFeatures({ - rowPaginationFeature, - rowSortingFeature, - paginatedRowModel: createPaginatedRowModel(), - sortedRowModel: createSortedRowModel(), - sortFns, - }), - cellComponents: { TextCell }, - headerComponents: { SortIndicator }, -}) -``` - -In components: - -```svelte - - - - {#snippet children()} - - - {#each table.getHeaderGroups() as group (group.id)} - - {#each group.headers as header (header.id)} - - {#snippet children(h)} - - {/snippet} - - {/each} - - {/each} - -
- {/snippet} -
-``` - -Inside custom `cellComponents` / `headerComponents` / `tableComponents`, use `useCellContext()` / -`useHeaderContext()` / `useTableContext()` instead of prop-drilling. - -## Common failure modes - -- **Svelte 4 code with v9 adapter.** Will not run. `$state` / `$derived.by` are Svelte 5 syntax. -- **`createSvelteTable` import.** v8 name. v9 uses `createTable`. There is no `useSvelteTable`, - `getCoreRowModel`, `getSortedRowModel`, etc. — those are v8 names too. -- **Forgetting `tableFeatures()`.** `features` must come from `tableFeatures({...})` for type - inference; passing a raw object loses the typed state slice keys. -- **Forgetting feature registration.** Calling `table.setSorting(...)` without - `rowSortingFeature` in `features` is a runtime no-op (the API method won't exist). -- **Mixing ownership.** `atoms.pagination` + `state.pagination` + `initialState.pagination` is - ambiguous; the table will not "merge" them the way you expect. -- **Reactive getters dropped.** If you pass `data` as a plain value instead of a getter, the - table won't re-render when `data` changes. Always use `get data() { return data }`. -- **Reimplementing built-ins.** If you're hand-rolling sorting comparators, pagination math, or - selection toggles, you're skipping `rowSortingFeature` / `rowPaginationFeature` / - `rowSelectionFeature` and their reset / state APIs. Register the feature instead. - -## Related skills - -- `tanstack-table/core/state-management` — atom model, slice precedence, base vs derived atoms. -- `tanstack-table/svelte/getting-started` — end-to-end first table. -- `tanstack-table/svelte/compose-with-tanstack-store` — direct atom interop. -- `tanstack-table/svelte/production-readiness` — selector / subscription tuning. diff --git a/packages/svelte-table/skills/table-state/SKILL.md b/packages/svelte-table/skills/table-state/SKILL.md new file mode 100644 index 0000000000..c98a384abe --- /dev/null +++ b/packages/svelte-table/skills/table-state/SKILL.md @@ -0,0 +1,205 @@ +--- +name: table-state +description: > + Use Svelte 5 rune-backed table.atoms/store and selected table.state, reactive option getters, controlled $state slices, value-or-updater callbacks, external atoms, and auto-reset behavior without snapshot mismatches. +metadata: + type: framework + library: '@tanstack/svelte-table' + framework: svelte + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - getting-started +sources: + - 'TanStack/table:docs/framework/svelte/guide/table-state.md' + - 'TanStack/table:docs/framework/svelte/guide/pagination.md' + - 'TanStack/table:examples/svelte/basic-external-state' + - 'TanStack/table:packages/svelte-table/src/createTable.svelte.ts' +--- + +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. +- `table.state` is only the result selected by the second `createTable` argument. + +Svelte 5 backs these surfaces with runes and synchronizes reactive options before DOM updates. Only registered features create state and types. If pagination is missing, register `rowPaginationFeature`; do not add a cast or an 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. Select only render state that the component needs. + +```svelte + + + +``` + +## Core Patterns + +### Control a slice with value-or-updater semantics + +```ts +import type { PaginationState, Updater } from '@tanstack/svelte-table' + +let pagination = $state({ pageIndex: 0, pageSize: 20 }) +const updatePagination = (next: Updater) => { + pagination = typeof next === 'function' ? next(pagination) : next +} +``` + +Pass `get state() { return { pagination } }` and `onPaginationChange: updatePagination` to `createTable`. + +### Subscribe narrowly outside selected table.state + +```ts +import { subscribeTable } from '@tanstack/svelte-table' + +const pageIndex = subscribeTable( + table.atoms.pagination, + (value) => value.pageIndex, +) +``` + +Read `pageIndex.current` in rune-tracked Svelte code. Use feature APIs for writes; `baseAtoms` is a low-level escape hatch. + +## 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. +- Prefer a stable external atom in `atoms` for state shared with Query, routing, or another component. Do not also add its change callback. +- Use a `$state` value exposed through a `state` getter plus the matching callback for simple controlled state. Always resolve value-or-updater semantics. + +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 all state changes must be observed. + +## 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.` owns it. + +```ts +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` for the complete registered state. + +## Common Mistakes + +### HIGH Controlling without writing back + +Wrong: + +```ts +const options = { state: { pagination }, onPaginationChange: console.log } +``` + +Correct: + +```ts +const options = { + get state() { + 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 Reading snapshots outside tracking + +Wrong: + +```ts +const pageIndex = table.store.state.pagination.pageIndex +``` + +Correct: + +```ts +const pageIndex = subscribeTable( + table.atoms.pagination, + (value) => value.pageIndex, +) +``` + +`store.state` is a current snapshot; it does not create a future Svelte update outside a tracked scope. + +Source: `packages/svelte-table/src/createTable.svelte.ts` + +### MEDIUM Declaring one slice in two owners + +Wrong: + +```ts +const options = { initialState: { pagination: start }, state: { pagination } } +``` + +Correct: + +```ts +const options = { + get state() { + 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: + +```ts +table.setPageIndex(4) +data = filteredData +``` + +Correct: + +```ts +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/src/createTable.svelte.ts`, `createTableState.svelte.ts`, and `subscribe.ts`; inspect registered state slices in the matching core feature source. diff --git a/packages/svelte-table/skills/with-tanstack-query/SKILL.md b/packages/svelte-table/skills/with-tanstack-query/SKILL.md new file mode 100644 index 0000000000..26b0e947f5 --- /dev/null +++ b/packages/svelte-table/skills/with-tanstack-query/SKILL.md @@ -0,0 +1,147 @@ +--- +name: with-tanstack-query +description: > + Compose Svelte Query with Svelte Table manual filtering, sorting, and pagination using reactive query inputs, query-result data getters, server counts, and a single source of server-data truth. +metadata: + type: composition + library: '@tanstack/svelte-table' + framework: svelte + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#client-vs-server' + - getting-started + - table-state +sources: + - 'TanStack/table:examples/svelte/with-tanstack-query' + - 'TanStack/table:docs/framework/svelte/guide/pagination.md' +--- + +This skill builds on `@tanstack/table-core#client-vs-server`, `getting-started`, and `table-state`. Decide which row-processing stages the server owns before composing Query. + +## Setup + +```ts +import { createQuery, keepPreviousData } from '@tanstack/svelte-query' +import { + createTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/svelte-table' + +const features = tableFeatures({ rowPaginationFeature }) +let pagination = $state({ pageIndex: 0, pageSize: 20 }) +const defaultData: Array<{ name: string }> = [] +const dataQuery = createQuery<{ + rows: Array<{ name: string }> + rowCount: number +}>(() => ({ + queryKey: ['people', pagination.pageIndex, pagination.pageSize], + queryFn: () => + fetch( + `/api/people?page=${pagination.pageIndex}&size=${pagination.pageSize}`, + ).then((r) => r.json()), + placeholderData: keepPreviousData, +})) +const table = createTable({ + features, + columns, + get data() { + return dataQuery.data?.rows ?? defaultData + }, + get rowCount() { + return dataQuery.data?.rowCount ?? 0 + }, + manualPagination: true, + get state() { + return { pagination } + }, + onPaginationChange: (next) => { + pagination = typeof next === 'function' ? next(pagination) : next + }, +}) +``` + +## Core Patterns + +### Put every server-owned stage in the query key + +If sorting or filtering is manual too, control those slices and include their serializable values in `queryKey`. Return data already processed in that same order. + +### Keep Query as server-data owner + +Expose `dataQuery.data` through Table getters. Copy it into `$state` only when the application explicitly owns an editable draft and defines cache synchronization. + +## Common Mistakes + +### HIGH Building a non-reactive query + +Wrong: + +```ts +const query = createQuery({ + queryKey: ['people', pagination.pageIndex], + queryFn, +}) +``` + +Correct: + +```ts +const query = createQuery(() => ({ + queryKey: ['people', pagination.pageIndex], + queryFn, +})) +``` + +The options function lets Svelte Query track the rune read and refetch on page changes. + +Source: `examples/svelte/with-tanstack-query/src/App.svelte` + +### HIGH Expecting manual mode to fetch + +Wrong: + +```ts +const options = { manualPagination: true } +``` + +Correct: + +```ts +const options = { + manualPagination: true, + get data() { + return dataQuery.data?.rows ?? defaultData + }, +} +``` + +Manual mode only bypasses Table pagination; Query or application code performs the request. Hoist `defaultData` instead of creating a new `[]` from a repeatedly evaluated getter. + +Source: `docs/framework/svelte/guide/pagination.md` + +### HIGH Omitting total counts + +Wrong: + +```ts +const options = { manualPagination: true, data: pageRows } +``` + +Correct: + +```ts +const options = { + manualPagination: true, + data: pageRows, + rowCount: response.rowCount, +} +``` + +Table cannot derive navigation limits from one server page; provide `rowCount` or `pageCount`. + +Source: `docs/framework/svelte/guide/pagination.md` + +## API Discovery + +Inspect `node_modules/@tanstack/svelte-table/src/index.ts` for adapter APIs and installed `@tanstack/svelte-query/src` for the exact Query version. Table manual-stage options live in the matching core feature source. diff --git a/packages/svelte-table/skills/with-tanstack-virtual/SKILL.md b/packages/svelte-table/skills/with-tanstack-virtual/SKILL.md new file mode 100644 index 0000000000..09f9edbdbf --- /dev/null +++ b/packages/svelte-table/skills/with-tanstack-virtual/SKILL.md @@ -0,0 +1,150 @@ +--- +name: with-tanstack-virtual +description: > + Virtualize Svelte Table final row or column models with reactive counts and scroll targets, stable keys, dynamic measurement, absolute transforms, sticky regions, grid/flex sizing, and infinite data. +metadata: + type: composition + library: '@tanstack/svelte-table' + framework: svelte + library_version: '9.0.0-beta.38' +requires: + - '@tanstack/table-core#core' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/svelte/guide/virtualization.md' + - 'TanStack/table:examples/svelte/virtualized-rows' + - 'TanStack/table:examples/svelte/virtualized-columns' + - 'TanStack/table:examples/svelte/virtualized-infinite-scrolling' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Virtual is a rendering layer over Table’s final model, never a `tableFeatures` plugin. + +## Setup + +```svelte + + +
+
+ {#each $rowVirtualizer.getVirtualItems() as item (item.key)} +
+ {rows[item.index].id} +
+ {/each} +
+
+``` + +## Core Patterns + +### Virtualize visible models + +Use `table.getRowModel().rows` for rows and `table.getVisibleLeafColumns()` for columns. Recompute counts when filtering, sorting, expansion, or visibility changes. + +### Make CSS geometry agree with measurement + +Use one scroll container, a total-size spacer, positioned items, and either fixed estimates or `measureElement`. For semantic tables with dynamic rows, follow the maintained grid/flex examples rather than assuming native table layout will honor transforms. + +### Fetch before the virtual end + +In infinite scrolling, compare the last virtual item with fetched row count, then request the next Query page only when more server rows exist and no fetch is active. + +## Common Mistakes + +### HIGH Virtualizing raw data + +Wrong: + +```ts +const rows = data +``` + +Correct: + +```ts +const rows = $derived(table.getRowModel().rows) +``` + +Raw data ignores Table filtering, sorting, grouping, expansion, and pagination decisions. + +Source: `examples/svelte/virtualized-rows/src/App.svelte` + +### HIGH Expecting getter options to stay reactive + +Wrong: + +```ts +const virtualizer = createVirtualizer({ + get count() { + return rows.length + }, + getScrollElement, +}) +``` + +Correct: + +```ts +const virtualizer = createVirtualizer({ count: rows.length, getScrollElement }) +$effect(() => { + get(virtualizer).setOptions({ + count: rows.length, + getScrollElement, + }) +}) +``` + +`createVirtualizer` returns a Svelte store, and its adapter does not track getter options. Push rune-derived counts and the bound scroll element with `$effect` and `get(store).setOptions(...)`. + +Source: `examples/svelte/virtualized-rows/src/App.svelte` + +### HIGH Omitting the geometry contract + +Wrong: + +```svelte +{#each rowVirtualizer.getVirtualItems() as item}
+ {rows[item.index].id} +
{/each} +``` + +Correct: + +```svelte +
+
+
+``` + +Virtual supplies ranges and measurements, not spacer height, transforms, sticky regions, or column widths. In markup, call virtualizer methods through the store auto-subscription (`$rowVirtualizer`); use `get(rowVirtualizer)` in script code. + +Source: `docs/framework/svelte/guide/virtualization.md` + +## API Discovery + +Inspect installed `@tanstack/svelte-table/src` for Table APIs and `@tanstack/svelte-virtual/src` for the exact virtualizer options. Use the maintained Svelte examples for layout combinations. diff --git a/packages/table-core/skills/api-not-found/SKILL.md b/packages/table-core/skills/api-not-found/SKILL.md new file mode 100644 index 0000000000..083c289388 --- /dev/null +++ b/packages/table-core/skills/api-not-found/SKILL.md @@ -0,0 +1,113 @@ +--- +name: api-not-found +description: > + Diagnose missing TanStack Table v9 exports, options, state slices, and instance methods. Load before inventing an API when code sees a type error, undefined feature method, absent object key, adapter mismatch, or v8-shaped example. +metadata: + type: sub-skill + library: '@tanstack/table-core' + library_version: '9.0.0-beta.38' +requires: ['core', 'table-features'] +sources: + - 'TanStack/table:packages/table-core/src/index.ts' + - 'TanStack/table:packages/table-core/src/types/TableFeatures.ts' + - 'TanStack/table:docs/framework/react/guide/migrating.md' +--- + +This skill builds on `core` and `table-features`. Treat the installed package—not remembered docs—as exact API truth. + +## Setup + +```ts +import { readFile } from 'node:fs/promises' + +const packageJson = JSON.parse( + await readFile('node_modules/@tanstack/table-core/package.json', 'utf8'), +) +const entrypoint = await readFile( + 'node_modules/@tanstack/table-core/src/index.ts', + 'utf8', +) +console.log(packageJson.version, entrypoint.includes('rowSortingFeature')) +``` + +## Core Patterns + +### Trace an export to source + +```sh +rg "export .*rowSortingFeature|rowSortingFeature" node_modules/@tanstack/table-core/src +``` + +For an adapter API, start at `node_modules/@tanstack/-table/src/index.ts`. + +### Check feature gating before replacement + +```ts +const features = tableFeatures({ rowSortingFeature }) +``` + +If the export exists but the instance API does not, inspect `TableFeatures.ts` and the table's registry. + +## Common Mistakes + +### [CRITICAL] Substituting a remembered v8 API + +Wrong: + +```ts +const options = { getSortedRowModel: getSortedRowModel() } +``` + +Correct: + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +``` + +V9 moved row-model factories to named feature slots and renamed `get*` factories to `create*`. + +Source: `docs/framework/react/guide/migrating.md#row-model-factories` + +### [HIGH] Mistaking omitted feature for removal + +Wrong: + +```ts +const features = tableFeatures({}) +``` + +Correct: + +```ts +const features = tableFeatures({ columnVisibilityFeature }) +``` + +Optional APIs and state are typed and installed from the concrete feature registry. + +Source: `packages/table-core/src/types/TableFeatures.ts` + +### [HIGH] Discovering methods through own keys + +Wrong: + +```ts +const methods = Object.keys(row).filter((key) => key.startsWith('get')) +``` + +Correct: + +```ts +const value = row.getValue('name') +const prototype = Object.getPrototypeOf(row) +``` + +V9 shares many instance methods through prototypes, so spread, serialization, and `Object.keys` omit them. + +Source: `docs/framework/react/guide/migrating.md#instance-methods-must-be-called-on-their-instance` + +## API Discovery + +Use this order: installed `package.json` version, installed adapter `src/index.ts`, installed core `src/index.ts`, then the exported implementation or feature directory. If `src` is absent, inspect installed declarations for that exact version. diff --git a/packages/table-core/skills/client-vs-server/SKILL.md b/packages/table-core/skills/client-vs-server/SKILL.md new file mode 100644 index 0000000000..fc336a1062 --- /dev/null +++ b/packages/table-core/skills/client-vs-server/SKILL.md @@ -0,0 +1,164 @@ +--- +name: client-vs-server +description: > + Choose client or server ownership for filtering, grouping, sorting, expanding, and pagination in TanStack Table v9. Load for manual* flags, mixed pipelines, server counts, or deciding which dataset each row-model stage receives. +metadata: + type: sub-skill + library: '@tanstack/table-core' + library_version: '9.0.0-beta.38' +requires: ['core', 'table-features'] +sources: + - 'TanStack/table:docs/guide/row-models.md' + - 'TanStack/table:packages/table-core/src/core/row-models/coreRowModelsFeature.utils.ts' + - 'TanStack/table:examples/react/with-tanstack-query' +--- + +This skill builds on `core` and `table-features`. Read them first for the model pipeline and plugin slots. + +## Setup + +```ts +import { + rowPaginationFeature, + rowSortingFeature, + tableFeatures, +} from '@tanstack/table-core' + +export const features = tableFeatures({ + rowSortingFeature, + rowPaginationFeature, +}) +export const serverOptions = { + manualSorting: true, + manualPagination: true, + rowCount: 12_450, +} as const +``` + +## Core Patterns + +### Client owns the complete pipeline + +```ts +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}) +``` + +Pass the full client dataset so each stage can process all rows. + +### Server owns processing + +```ts +const options = { + data: serverPage.rows, + manualFiltering: true, + manualSorting: true, + manualPagination: true, + rowCount: serverPage.total, +} +``` + +Put controlled filter, sorting, and pagination state into the request/query key. + +## Common Mistakes + +### [CRITICAL] Treating manual flags as requests + +Wrong: + +```ts +const options = { manualSorting: true, data: unsortedRows } +``` + +Correct: + +```ts +const sortedPage = await fetchPage({ sorting }) +const options = { manualSorting: true, data: sortedPage.rows } +``` + +`manualSorting` only bypasses client sorting; it never calls a backend. + +Source: `packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts` + +### [HIGH] Expecting manual pagination to slice + +Wrong: + +```ts +const options = { data: allRows, manualPagination: true } +``` + +Correct: + +```ts +const page = await fetchPage({ pageIndex, pageSize }) +const options = { + data: page.rows, + manualPagination: true, + rowCount: page.rowCount, +} +``` + +Manual pagination assumes `data` already represents the intended page. Pass the stable page result directly. If the application deliberately performs custom local slicing instead, derive it with the framework's memo/computed primitive and keep the reference stable until its actual inputs change. + +Source: `docs/framework/react/guide/pagination.md#manual-server-side-pagination` + +### [HIGH] Sorting only the loaded page accidentally + +Wrong: + +```ts +const options = { + data: serverPage.rows, + manualPagination: true, + manualSorting: false, +} +``` + +Correct: + +```ts +const options = { + data: serverPage.rows, + manualPagination: true, + manualSorting: true, +} +``` + +Client sorting can see only loaded rows, so it cannot produce database-wide order. + +Source: `docs/guide/row-models.md` + +### [HIGH] Recreating processed data or columns inline + +Wrong: + +```ts +const options = { + data: allRows.filter(matchesFilters).slice(pageStart, pageEnd), + columns: makeColumns(), +} +``` + +Correct: + +```ts +const processedRows = pageResult.rows +const columns = sharedColumns +const options = { data: processedRows, columns } +``` + +`data` and `columns` are model inputs. Keep both references stable between meaningful changes with module constants, state, or the adapter's memo/computed primitive; otherwise Table repeatedly invalidates row and column models and some adapters can enter render loops. “Manual” describes processing ownership, not permission to derive new arrays inside table options. + +Source: `docs/guide/data.md` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/core/row-models/coreRowModelsFeature.utils.ts` for pipeline order and each feature's `.types.ts` for its `manual*` contract. diff --git a/packages/table-core/skills/column-definitions/SKILL.md b/packages/table-core/skills/column-definitions/SKILL.md deleted file mode 100644 index ace0d43450..0000000000 --- a/packages/table-core/skills/column-definitions/SKILL.md +++ /dev/null @@ -1,330 +0,0 @@ ---- -name: column-definitions -description: > - Define TanStack Table v9 columns with `createColumnHelper()`. - Covers `columnHelper.accessor` (key + function forms), `columnHelper.display`, - `columnHelper.group`, `columnHelper.columns`, the `ColumnDef`/`AccessorKeyColumnDef`/ - `AccessorFnColumnDef`/`DisplayColumnDef`/`GroupColumnDef` types, `accessorKey` with - `DeepKeys`, `accessorFn`, the `header`/`cell`/`footer`/`aggregatedCell` renderers, - required `id` rules, and `getRowId` for stable row identity. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -sources: - - TanStack/table:docs/guide/column-defs.md - - TanStack/table:docs/guide/columns.md - - TanStack/table:packages/table-core/src/helpers/columnHelper.ts - - TanStack/table:packages/table-core/src/core/columns/constructColumn.ts - - TanStack/table:examples/react/basic-use-table/src/main.tsx ---- - -## Setup - -`createColumnHelper` takes TWO generics in v9: the features type (so accessor keys, sort/filter strings, etc. are typed against your registered features) and the row data type. - -```ts -import { - createColumnHelper, - tableFeatures, - rowSortingFeature, -} from '@tanstack/table-core' - -type Person = { - id: string - firstName: string - lastName: string - age: number - visits: number -} - -const features = tableFeatures({ rowSortingFeature }) - -// TFeatures FIRST, TData SECOND -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - // accessorKey — deep keys via DeepKeys (dot paths) are supported - columnHelper.accessor('firstName', { header: 'First Name' }), - columnHelper.accessor('lastName', { header: 'Last Name' }), - columnHelper.accessor('age', { header: 'Age' }), - - // accessorFn — needs an explicit `id` - columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, { - id: 'fullName', - header: 'Full Name', - cell: (info) => info.getValue(), - }), - - // display column — no value extraction, just rendering - columnHelper.display({ - id: 'actions', - header: 'Actions', - cell: ({ row }) => `Edit ${row.original.id}`, - }), - - // group column — wraps child columns under a shared header - columnHelper.group({ - id: 'stats', - header: 'Stats', - columns: [columnHelper.accessor('visits', { header: 'Visits' })], - }), -]) -``` - -## Core Patterns - -### Stable row identity with `getRowId` - -```ts -const table = useTable({ - features, - columns, - data, - getRowId: (row) => row.id, // ← stable from row's own data -}) -``` - -Without `getRowId`, `row.id` defaults to the row's array index. Row-keyed state (selection, expansion, pinning) then attaches to whatever happens to be at that index after a sort/filter/refetch. - -### Accessor key with deep path - -```ts -type User = { name: { first: string; last: string } } - -const columnHelper = createColumnHelper() - -columnHelper.accessor('name.first', { header: 'First' }) -columnHelper.accessor('name.last', { header: 'Last' }) -``` - -For nested objects with non-optional intermediate keys, the dotted `accessorKey` form works and infers the right value type. Switch to `accessorFn` when intermediates are optional (see Common Mistakes below). - -### Header / cell / footer renderers - -```ts -columnHelper.accessor('age', { - header: () => 'Age', - cell: (info) => info.getValue(), - footer: (info) => `${info.table.getRowModel().rows.length} rows`, -}) -``` - -Renderers accept string, JSX (in framework adapters), or function forms. Render via `flexRender(def, ctx)` or `` so all three forms work uniformly. - -### `columnHelper.columns([...])` for module-scope stability - -```ts -// Outside any component / hook — stable reference forever -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('lastName', { header: 'Last' }), -]) -``` - -`columnHelper.columns` returns the array as-is but preserves the precise tuple types. Hoist to module scope or wrap in `useMemo` — the table compares `columns` by reference. - -## Common Mistakes - -### [CRITICAL] Passing only `TData` to `createColumnHelper` - -Wrong: - -```ts -// v8 signature — TData ends up in the TFeatures slot -const columnHelper = createColumnHelper() -``` - -Correct: - -```ts -const features = tableFeatures({ rowSortingFeature }) -const columnHelper = createColumnHelper() -``` - -v9 changed the generic order: ``. The compiler error is noisy because `Person` lands in the `TFeatures` slot and breaks every column type that follows. - -Source: packages/table-core/src/helpers/columnHelper.ts; docs/framework/react/guide/migrating.md - -### [HIGH] Accessor function returns an object or array - -Wrong: - -```ts -// returns an object — built-in alphanumeric sort and includesString filter break -columnHelper.accessor((row) => row.name, { - id: 'name', - cell: (info) => `${info.getValue().first} ${info.getValue().last}`, -}) -``` - -Correct: - -```ts -// accessor returns a primitive; cell can still format it -columnHelper.accessor((row) => `${row.name.first} ${row.name.last}`, { - id: 'fullName', - cell: (info) => info.getValue(), -}) -``` - -The accessed value drives sorting, filtering, faceting, and grouping. Built-in `sortFn`/`filterFn`/`aggregationFn` expect a primitive `string` / `number` / `Date`. Return a primitive — or supply a matching custom function. - -Source: docs/guide/column-defs.md - -### [CRITICAL] Omitting `id` on an `accessorFn` column - -Wrong: - -```tsx -// accessorFn + JSX header => no id can be derived -columnHelper.accessor((row) => row.lastName, { - header: () => Last Name, - cell: (info) => info.getValue(), -}) -``` - -Correct: - -```tsx -columnHelper.accessor((row) => row.lastName, { - id: 'lastName', // required when there's no string accessorKey or string header - header: () => Last Name, - cell: (info) => info.getValue(), -}) -``` - -The constructor throws "coreColumnsFeature require an id when using an accessorFn" in development. The same applies to non-string `header` values without a fallback `id`. - -Source: packages/table-core/src/core/columns/constructColumn.ts - -### [CRITICAL] Defining `columns` inside the component without `useMemo` - -Wrong: - -```tsx -function MyTable() { - // new array reference every render → infinite render loop - const columns = [ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('lastName', { header: 'Last' }), - ] - const table = useTable({ features, columns, data }) -} -``` - -Correct: - -```tsx -function MyTable() { - const columns = React.useMemo( - () => - columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('lastName', { header: 'Last' }), - ]), - [], - ) - const table = useTable({ features, columns, data }) -} -``` - -TanStack Table compares `columns` and `data` by reference. The #1 FAQ entry across versions. - -Source: docs/faq.md; examples/react/basic-subscribe/src/main.tsx - -### [HIGH] Using array-index row IDs with mutating data - -Wrong: - -```ts -// no getRowId — rowSelection survives data updates but maps to wrong rows -const table = useTable({ - features, - columns, - data, - enableRowSelection: true, -}) -``` - -Correct: - -```ts -const table = useTable({ - features, - columns, - data, - getRowId: (row) => row.id, - enableRowSelection: true, -}) -``` - -When `data` reorders, filters, or items are removed/refetched, row-keyed state (selection, expansion, pinning) attaches to the wrong row. - -Source: docs/guide/rows.md; packages/table-core/src/core/rows/coreRowsFeature.utils.ts - -### [MEDIUM] `accessorKey` with optional path strips `undefined` from `getValue` type - -Wrong: - -```ts -// amount inferred as `number` even though salary is optional -columnHelper.accessor('user.salary.amount', { - cell: (info) => { - const amount = info.getValue() // type: number (WRONG) - return amount.toFixed(2) // crashes when salary is undefined - }, -}) -``` - -Correct: - -```ts -columnHelper.accessor((row) => row.user.salary?.amount, { - id: 'salary', - cell: (info) => { - const amount = info.getValue() // type: number | undefined - return amount?.toFixed(2) ?? '-' - }, -}) -``` - -The `DeepValue` type doesn't propagate `undefined` through optional intermediates. Use `accessorFn` when any segment is optional — the type follows the expression. - -Source: https://github.com/TanStack/table/issues/6238 - -### [MEDIUM] `columnHelper.accessor` nested inside `columnHelper.group` loses `getValue` inference - -Wrong: - -```ts -// info.getValue() inferred as unknown -columnHelper.group({ - id: 'name', - columns: [ - columnHelper.accessor('firstName', { - cell: (info) => info.getValue(), // unknown - }), - ], -}) -``` - -Correct: - -```ts -// Hoist accessor definitions out of the group -const firstNameCol = columnHelper.accessor('firstName', { - cell: (info) => info.getValue(), // string -}) - -columnHelper.group({ id: 'name', columns: [firstNameCol] }) -``` - -The group helper's overloads don't thread `TData` through correctly when accessors are defined inline. - -Source: https://github.com/TanStack/table/issues/5860 - -## See also - -- `tanstack-table/setup` — how `features` (with row model factory slots) threads through `useTable` -- `tanstack-table/customizing-feature-behavior` — per-column `sortFn`/`filterFn`/`aggregationFn` -- `tanstack-table/row-selection` — why `getRowId` is essentially mandatory diff --git a/packages/table-core/skills/column-faceting/SKILL.md b/packages/table-core/skills/column-faceting/SKILL.md new file mode 100644 index 0000000000..bba184ea02 --- /dev/null +++ b/packages/table-core/skills/column-faceting/SKILL.md @@ -0,0 +1,91 @@ +--- +name: column-faceting +description: > + Build faceted filter UIs with columnFacetingFeature, facetedRowModel, facetedUniqueValues, and facetedMinMaxValues. Load for facet counts, numeric ranges, own-filter exclusion, or server-page facet completeness. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features', 'column-filtering'] +sources: + - 'TanStack/table:docs/framework/react/guide/column-faceting.md' + - 'TanStack/table:packages/table-core/src/features/column-faceting' + - 'TanStack/table:examples/react/filters-faceted' +--- + +This skill builds on `core`, `table-features`, and `column-filtering`. Faceting derives filter choices; it does not render controls. + +## Setup + +```ts +import { + columnFacetingFeature, + columnFilteringFeature, + createFacetedMinMaxValues, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + filterFns, + tableFeatures, +} from '@tanstack/table-core' + +export const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns, + columnFacetingFeature, + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), +}) +``` + +## Core Patterns + +```ts +const counts = table.getColumn('status')?.getFacetedUniqueValues() ?? new Map() +const range = table.getColumn('age')?.getFacetedMinMaxValues() +``` + +Use unique values for discrete controls and min/max only for numeric ranges. +The filtered model makes facets respond to the table's other active filters. +The built-in `filterFns` registry supports named stock filters; a column may +instead receive a filter function directly without registering it. + +## Common Mistakes + +### [HIGH] Registering APIs without model slots + +Wrong: `tableFeatures({ columnFilteringFeature, columnFacetingFeature })` + +Correct: `tableFeatures({ columnFilteringFeature, columnFacetingFeature, facetedRowModel: createFacetedRowModel(), facetedUniqueValues: createFacetedUniqueValues() })` + +Each faceting getter needs its matching factory slot. + +Source: `packages/table-core/src/features/column-faceting/columnFacetingFeature.ts` + +### [MEDIUM] Expecting facet to apply itself + +Wrong: `column.getFacetedUniqueValues().get(activeValue) === 0` + +Correct: `column.getFacetedUniqueValues().get(activeValue) ?? 0` + +A column facet intentionally excludes that column's own filter while applying other filters. + +Source: `docs/framework/react/guide/column-faceting.md` + +### [HIGH] Treating page facets as global + +Wrong: `const globalCounts = column.getFacetedUniqueValues()` + +Correct: `const globalCounts = await fetchFacetCounts(activeFilters)` + +With server pagination, client faceting sees only loaded data. + +Source: `docs/framework/react/guide/column-faceting.md#custom-server-side-faceting` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/features/column-faceting/` for exact getters and factory return types. diff --git a/packages/table-core/skills/column-filtering/SKILL.md b/packages/table-core/skills/column-filtering/SKILL.md new file mode 100644 index 0000000000..6a314b00f9 --- /dev/null +++ b/packages/table-core/skills/column-filtering/SKILL.md @@ -0,0 +1,82 @@ +--- +name: column-filtering +description: > + Filter columns with columnFilteringFeature, filteredRowModel, filterFns, filterMeta, nested-row direction, and manualFiltering. Load for accessor compatibility, controlled filter updaters, fuzzy metadata, or client/server ownership. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features', 'client-vs-server'] +sources: + - 'TanStack/table:docs/framework/react/guide/column-filtering.md' + - 'TanStack/table:packages/table-core/src/features/column-filtering' + - 'TanStack/table:examples/react/filters' +--- + +This skill builds on `core`, `table-features`, and `client-vs-server`. Filtering state, row processing, and filter UI are separate concerns. + +## Setup + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + filterFns, + tableFeatures, +} from '@tanstack/table-core' + +export const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns, +}) +``` + +## Core Patterns + +```ts +const options = { + filterFromLeafRows: true, + maxLeafRowFilterDepth: 2, +} +``` + +Use leaf-first filtering only when a parent should survive because a descendant matches. + +## Common Mistakes + +### [HIGH] Combining manual and client filtering + +Wrong: `const options = { manualFiltering: true }` + +Correct: `const options = { manualFiltering: false }` + +Manual mode returns the pre-filtered model; send filter state to the server instead if it is `true`. + +Source: `packages/table-core/src/features/column-filtering/columnFilteringFeature.types.ts` + +### [HIGH] Filtering renderer output + +Wrong: `helper.accessor(row => ({ label: row.status }), { filterFn: 'includesString' })` + +Correct: `helper.accessor('status', { filterFn: 'includesString' })` + +Built-in string and numeric filters expect comparable accessor values, not objects or UI nodes. + +Source: `docs/framework/react/guide/column-filtering.md#filterfns` + +### [HIGH] Ignoring updater-function callbacks + +Wrong: `onColumnFiltersChange: value => { columnFilters = value as ColumnFiltersState }` + +Correct: `onColumnFiltersChange: updater => { columnFilters = functionalUpdate(updater, columnFilters) }` + +Controlled callbacks receive either a value or a function of previous state. + +Source: `packages/table-core/src/features/column-filtering/columnFilteringFeature.types.ts` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/features/column-filtering/` and `src/fns/filterFns.ts` for exact signatures and auto-remove behavior. diff --git a/packages/table-core/skills/column-layout/SKILL.md b/packages/table-core/skills/column-layout/SKILL.md deleted file mode 100644 index 91c3c6e964..0000000000 --- a/packages/table-core/skills/column-layout/SKILL.md +++ /dev/null @@ -1,326 +0,0 @@ ---- -name: column-layout -description: > - The five UI-state-only column features in TanStack Table v9 that shape how - columns render — visibility, ordering, pinning, sizing, resizing. None - require a row model. Covers `columnVisibilityFeature` (getVisibleLeafColumns, - row.getVisibleCells), `columnOrderingFeature` (columnOrder string[], column.getIndex), - `columnPinningFeature` (left/right ColumnPinningState, column.pin, column.getStart / - getAfter, split-table getLeft*/getCenter*/getRight* APIs, sticky-CSS pattern; - `enableColumnPinning` table-level option distinct from per-column `enablePinning`), - `columnSizingFeature` (defaultColumnSizing, column.getSize, table.getTotalSize), - `columnResizingFeature` (columnResizeMode 'onEnd'/'onChange', columnResizeDirection - 'ltr'/'rtl', header.getResizeHandler for mouse + touch, CSS-variable - performant resize pattern). Pipeline: Column Pinning → columnOrder → Grouping. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management -sources: - - TanStack/table:docs/guide/column-visibility.md - - TanStack/table:docs/guide/column-ordering.md - - TanStack/table:docs/guide/column-pinning.md - - TanStack/table:docs/guide/column-sizing.md - - TanStack/table:docs/guide/column-resizing.md - - TanStack/table:examples/react/column-visibility/src/main.tsx - - TanStack/table:examples/react/column-resizing/src/main.tsx - - TanStack/table:examples/react/column-resizing-performant/src/main.tsx - - TanStack/table:examples/react/column-pinning-split/src/main.tsx - - TanStack/table:examples/react/column-pinning-sticky/src/main.tsx - - TanStack/table:examples/react/column-dnd/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management`. Read it first for the atom model — these are UI-state-only features (no row model). - -## Setup - -All five features are opt-in via `tableFeatures({...})`. The reorder pipeline is fixed: **(1) Column Pinning splits into left/center/right → (2) `columnOrder` reorders the center → (3) Grouping (`groupedColumnMode: 'reorder' | 'remove'`) may move grouped columns to the front.** - -```ts -import { - tableFeatures, - columnVisibilityFeature, - columnOrderingFeature, - columnPinningFeature, - columnSizingFeature, - columnResizingFeature, - constructTable, -} from '@tanstack/table-core' - -const features = tableFeatures({ - columnVisibilityFeature, - columnOrderingFeature, - columnPinningFeature, - columnSizingFeature, - columnResizingFeature, // explicit — formerly part of v8 ColumnSizing -}) - -const table = constructTable({ - features, - columns, - data, - initialState: { - columnVisibility: {}, - columnOrder: [], - columnPinning: { left: [], right: [] }, - columnSizing: {}, - }, -}) -``` - -## Subsystems - -| Feature | State slice | Key APIs | -| ------------------------- | ---------------------- | ---------------------------------------------------- | -| `columnVisibilityFeature` | `columnVisibility` | `column.toggleVisibility()`, `row.getVisibleCells()` | -| `columnOrderingFeature` | `columnOrder` | `table.setColumnOrder()`, `column.getIndex()` | -| `columnPinningFeature` | `columnPinning` (l/r) | `column.pin()`, `column.getStart()`, `getAfter()` | -| `columnSizingFeature` | `columnSizing` | `column.getSize()`, `table.getTotalSize()` | -| `columnResizingFeature` | (transient drag state) | `header.getResizeHandler()`, `columnResizeMode` | - -Full API surface, render strategies, and additional MEDIUM-priority failure modes (reorder-pinned-via-columnOrder, react-dnd/react-beautiful-dnd avoidance, touch-resize handler) in [subsystems.md](references/subsystems.md). - -## Core Patterns - -### Performant `'onChange'` resize (React) - -```tsx -// From examples/react/column-resizing-performant/src/main.tsx -const columnSizeVars = React.useMemo(() => { - const headers = table.getFlatHeaders() - const colSizes: { [key: string]: number } = {} - for (const header of headers) { - colSizes[`--header-${header.id}-size`] = header.getSize() - colSizes[`--col-${header.column.id}-size`] = header.column.getSize() - } - return colSizes -}, [table.state.columnResizing, table.state.columnSizing]) - -
- {table.store.state.columnResizing.isResizingColumn - ? - : } -
- -// Body cells use the CSS variable (no per-cell getSize() call) -
- {cell.renderValue()} -
- -export const MemoizedTableBody = React.memo( - TableBody, - (prev, next) => prev.table.options.data === next.table.options.data, -) -``` - -## Common Mistakes - -### [HIGH] Rendering body cells with `row.getAllCells()` while visibility is registered - -Wrong: - -```tsx -// Toggling visibility has no effect on rendered cells -{ - table.getAllLeafColumns().map((column) => ...) -} -{ - row.getAllCells().map((cell) => ( - - - - )) -} -``` - -Correct: - -```tsx -// Header groups already respect visibility; use them for headers. -// For body cells, swap getAllCells → getVisibleCells. - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder ? null : } - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -``` - -The `getAll*` accessors do NOT consult `columnVisibility` state. Only `Visible` variants and header-group APIs filter by visibility. - -Source: docs/guide/column-visibility.md; examples/react/column-visibility/src/main.tsx - -### [HIGH] `columnResizeMode: 'onChange'` + `column.getSize()` per cell + un-memoized body - -Wrong: - -```tsx -const table = useTable({ - features: tableFeatures({ columnSizingFeature, columnResizingFeature }), - columnResizeMode: 'onChange', -}) -{cell.renderValue()} -``` - -Correct: - -```tsx -// See "Performant 'onChange' resize" above — CSS variables + memoized body -const columnSizeVars = React.useMemo(() => { - /* … */ -}, [table.state.columnResizing, table.state.columnSizing]) - -{ - table.store.state.columnResizing.isResizingColumn ? ( - - ) : ( - - ) -} - -;
-``` - -`'onChange'` commits a new `columnSizing` map on every pointer move. Per-cell `getSize()` blows the 16ms frame budget. The CSS-variable pattern caches widths once per resize batch. - -Source: docs/guide/column-resizing.md; examples/react/column-resizing-performant/src/main.tsx - -### [HIGH] Using v8 `enablePinning` at the table level - -Wrong: - -```ts -// v8 syntax — no longer disables pinning at table level in v9 -const table = useTable({ - features: tableFeatures({ columnPinningFeature }), - enablePinning: false, // ignored -}) -``` - -Correct: - -```ts -// v9 split: two distinct table-level options -const table = useTable({ - features: tableFeatures({ columnPinningFeature, rowPinningFeature }), - enableColumnPinning: false, - enableRowPinning: false, -}) - -// Per-column opt-out is still spelled `enablePinning`: -columnHelper.accessor('id', { - enablePinning: false, // this column can't be pinned -}) -``` - -v9 split `enablePinning` into `enableColumnPinning` and `enableRowPinning`. The bare name now refers ONLY to per-column opt-out. - -Source: packages/table-core/src/features/column-pinning/columnPinningFeature.types.ts - -### [HIGH] Defining `columns` inline (infinite loop once a layout feature commits state) - -Wrong: - -```tsx -function App() { - const columns = [ - columnHelper.accessor('firstName', { - /* … */ - }), - columnHelper.accessor('lastName', { - /* … */ - }), - ] - const table = useTable({ - features: tableFeatures({ columnPinningFeature, columnResizingFeature }), - columns, - data, - }) -} -``` - -Correct: - -```tsx -const defaultColumns = columnHelper.columns([ - columnHelper.accessor('firstName', { - /* … */ - }), - columnHelper.accessor('lastName', { - /* … */ - }), -]) -function App() { - const [columns] = React.useState(() => [...defaultColumns]) - const table = useTable({ features, columns, data }) -} - -// or: useMemo -const columns = React.useMemo( - () => - columnHelper.columns([ - /* … */ - ]), - [], -) -``` - -Layout features commit state on every interaction. An inline `columns` array gets a new identity each render → table rebuild → another render. FAQ Pitfall 1. - -Source: docs/faq.md; examples/react/column-pinning-split/src/main.tsx; examples/react/column-dnd/src/main.tsx - -### [CRITICAL] Reimplementing visibility / pinning / resize logic manually - -Wrong: - -```ts -// Hand-rolled hide/show with a separate set -const [hidden, setHidden] = useState(new Set()) -const visibleColumns = useMemo( - () => columns.filter((c) => !hidden.has(c.id)), - [columns, hidden], -) -``` - -Correct: - -```ts -const table = useTable({ - features: tableFeatures({ columnVisibilityFeature }), - columns, - data, -}) -column.toggleVisibility() -column.getIsVisible() -table.getVisibleLeafColumns() -``` - -Source: maintainer interview (Phase 4, 2026-05-17) - -## See also - -- `tanstack-table/state-management` — `state.columnVisibility` / `columnOrder` / `columnPinning` / `columnSizing` slices -- `tanstack-table/row-pinning` — analogous pinning for rows (different render pipeline) -- `tanstack-table/grouping` — `groupedColumnMode` interacts with `columnOrder` - -## References - -- [subsystems.md](references/subsystems.md) — full API surface per UI-state subsystem (visibility, ordering, pinning, sizing, resizing) plus MEDIUM-priority failure modes: reorder-pinned-via-`columnOrder`, react-dnd / react-beautiful-dnd avoidance, touch-resize handler diff --git a/packages/table-core/skills/column-layout/references/subsystems.md b/packages/table-core/skills/column-layout/references/subsystems.md deleted file mode 100644 index 7e14d93a1d..0000000000 --- a/packages/table-core/skills/column-layout/references/subsystems.md +++ /dev/null @@ -1,220 +0,0 @@ -# Column-layout subsystems — full API surface - -Detailed reference for the five UI-state-only column features extracted from `SKILL.md`. The SKILL keeps a 2-line summary table linking here; this file documents each subsystem in detail. - -## Visibility — `columnVisibilityFeature` - -State: `columnVisibility: Record` — missing or `true` means visible. - -```tsx -// Visibility toggle panel -{ - table.getAllLeafColumns().map((column) => ( - - )) -} - -// Body — use Visible variants, NOT getAllLeafColumns / getAllCells -; - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - - - ))} - - ))} - -``` - -## Ordering — `columnOrderingFeature` - -State: `columnOrder: string[]` of leaf column ids. Empty means definition order. **Scoped to UNPINNED columns** when pinning is active — pinned columns are sequenced inside `columnPinning.left/right`. - -```ts -table.setColumnOrder(['firstName', 'lastName', 'age']) -column.getIndex('center') // ← position -column.getIsFirstColumn() -column.getIsLastColumn() -``` - -For drag-and-drop with `@dnd-kit/core`, see the "Common Mistakes" entry on dnd libraries in the SKILL — `DndContext` must wrap from OUTSIDE the ``. - -## Pinning — `columnPinningFeature` - -State: `columnPinning: { left: string[]; right: string[] }`. Two render strategies: - -```tsx -// Strategy A — split tables - - {table.getLeftHeaderGroups().map(/* … */)} - -// + getCenterHeaderGroups / getRightHeaderGroups -// + row.getLeftVisibleCells / getCenterVisibleCells / getRightVisibleCells - -// Strategy B — single table + sticky CSS - - -// Toggle a pin programmatically -column.pin('left') // or 'right' | false -``` - -## Sizing — `columnSizingFeature` - -State: `columnSizing: Record` (pixels). Defaults via `defaultColumnSizing` ({ size: 150, minSize: 20, maxSize: Number.MAX_SAFE_INTEGER }) or `tableOptions.defaultColumn` globally. - -```ts -columnHelper.accessor('firstName', { - size: 200, - minSize: 80, - maxSize: 400, -}) - -// Reads -column.getSize() // committed size (clamped) -header.getSize() // same, for groups sums children -table.getTotalSize() -table.getCenterTotalSize() -column.resetSize() // drop the override -``` - -## Resizing — `columnResizingFeature` - -```tsx -// Wire BOTH onMouseDown AND onTouchStart on the resize handle -
header.column.resetSize()} - onMouseDown={header.getResizeHandler()} - onTouchStart={header.getResizeHandler()} - className={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`} -/> - -// Modes: -// columnResizeMode: 'onEnd' (default) — commit on drag release; safer for big React tables -// columnResizeMode: 'onChange' — commit live; needs the perf pattern in SKILL -// columnResizeDirection: 'ltr' (default) | 'rtl' -``` - -## Additional MEDIUM-priority failure modes - -### Trying to reorder pinned columns via `columnOrder` - -Wrong: - -```ts -// Won't move 'actions' relative to 'firstName' while it's pinned right -const [columnPinning] = useState({ - left: ['select'], - right: ['actions'], -}) -table.setColumnOrder(['actions', 'select', 'firstName', 'lastName']) -``` - -Correct: - -```ts -// Reorder the pinning state itself -table.setColumnPinning((old) => ({ - left: ['select'], - right: ['summary', 'actions'], // 'summary' renders before 'actions' -})) - -// columnOrder works normally for the unpinned center region -table.setColumnOrder(['firstName', 'lastName']) -``` - -After the pipeline's pinning split, the left/right partitions read directly from `state.columnPinning.left/right`. `columnOrder` only affects the center. - -Source: docs/guide/column-ordering.md; packages/table-core/src/features/column-pinning/columnPinningFeature.utils.ts - -### Using `react-dnd` / `react-beautiful-dnd` for column reorder in React 18+ - -Wrong: - -```tsx -// react-dnd in React 18 Strict Mode — flicker and stale drags -import { DndProvider } from 'react-dnd' -import { HTML5Backend } from 'react-dnd-html5-backend' - -// or nesting DndContext inside
- ... -
-;
- - ... - -
-``` - -Correct: - -```tsx -// @dnd-kit + wrap from OUTSIDE the table (DndContext renders divs) - - - - {table.getHeaderGroups().map((hg) => ( - - - {hg.headers.map((h) => ( - - ))} - - - ))} - -
-
-``` - -`react-dnd` has Strict Mode incompatibilities; `react-beautiful-dnd` is in maintenance. dnd-kit is the v9-recommended stack. - -Source: examples/react/column-dnd/src/main.tsx - -### Wiring `header.getResizeHandler()` to only `onMouseDown` - -Wrong: - -```tsx -// Desktop only — mobile users can't resize -
-``` - -Correct: - -```tsx -
header.column.resetSize()} - onMouseDown={header.getResizeHandler()} - onTouchStart={header.getResizeHandler()} - className={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`} -/> -``` - -`header_getResizeHandler` branches internally on `isTouchStartEvent`. The same handler must be installed on both DOM events. - -Source: docs/guide/column-resizing.md; examples/react/column-resizing/src/main.tsx diff --git a/packages/table-core/skills/column-ordering/SKILL.md b/packages/table-core/skills/column-ordering/SKILL.md new file mode 100644 index 0000000000..90e8b1d178 --- /dev/null +++ b/packages/table-core/skills/column-ordering/SKILL.md @@ -0,0 +1,75 @@ +--- +name: column-ordering +description: > + Control TanStack Table v9 leaf columnOrder with stable IDs while accounting for pinning regions, visibility, and groupedColumnMode precedence. Load for drag-and-drop columns or rendered order that differs from state. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features'] +sources: + - 'TanStack/table:docs/framework/react/guide/column-ordering.md' + - 'TanStack/table:packages/table-core/src/features/column-ordering' + - 'TanStack/table:examples/react/column-dnd' +--- + +This skill builds on `core` and `table-features`. `columnOrder` orders unpinned leaf IDs; other plugins can still determine the final visual regions. + +## Setup + +```ts +import { columnOrderingFeature, tableFeatures } from '@tanstack/table-core' + +export const features = tableFeatures({ columnOrderingFeature }) +export const initialState = { columnOrder: ['name', 'age', 'actions'] } +``` + +## Core Patterns + +```ts +const next = columnOrder.filter((id) => id !== activeId) +next.splice(next.indexOf(overId), 0, activeId) +table.setColumnOrder(next) +``` + +Use column IDs as drag identities and replace the array. + +## Common Mistakes + +### [HIGH] Assuming state is final order + +Wrong: `renderIds(columnOrder)` + +Correct: `renderColumns(table.getAllLeafColumns())` + +Pinning regions and grouping mode apply after base ordering. If +`columnVisibilityFeature` is also registered, render +`table.getVisibleLeafColumns()` to apply visibility too. + +Source: `docs/framework/react/guide/column-ordering.md#what-affects-column-order` + +### [HIGH] Using labels as identities + +Wrong: `const activeId = column.columnDef.header as string` + +Correct: `const activeId = column.id` + +Headers can collide or change; ordering state stores stable leaf column IDs. + +Source: `packages/table-core/src/features/column-ordering/columnOrderingFeature.types.ts` + +### [HIGH] Mutating controlled state in place + +Wrong: `columnOrder.splice(0, 1); table.setColumnOrder(columnOrder)` + +Correct: `table.setColumnOrder(columnOrder.slice(1))` + +Reactive owners commonly require a new array reference. + +Source: `examples/react/column-dnd/src/main.tsx` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/features/column-ordering/`; combine with pinning/visibility skills when those plugins are registered. diff --git a/packages/table-core/skills/column-pinning/SKILL.md b/packages/table-core/skills/column-pinning/SKILL.md new file mode 100644 index 0000000000..075039ba12 --- /dev/null +++ b/packages/table-core/skills/column-pinning/SKILL.md @@ -0,0 +1,89 @@ +--- +name: column-pinning +description: > + Pin columns into logical start, center, and end regions with columnPinningFeature and renderer-owned sticky CSS. Load for RTL offsets, z-index, backgrounds, overflow, widths, gaps, or overlaps. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features', 'column-sizing'] +sources: + - 'TanStack/table:docs/framework/react/guide/column-pinning.md' + - 'TanStack/table:packages/table-core/src/features/column-pinning' + - 'TanStack/table:examples/react/column-pinning-sticky' +--- + +This skill builds on `core`, `table-features`, and `column-sizing`. Pinning partitions models; CSS creates the sticky visual result. + +## Setup + +```ts +import { + columnPinningFeature, + columnSizingFeature, + tableFeatures, +} from '@tanstack/table-core' + +export const features = tableFeatures({ + columnSizingFeature, + columnPinningFeature, +}) +export const initialState = { + columnPinning: { start: ['name'], end: ['actions'] }, +} +``` + +## Core Patterns + +```ts +const style = (column: Column) => ({ + position: column.getIsPinned() ? 'sticky' : 'relative', + insetInlineStart: + column.getIsPinned() === 'start' + ? `${column.getStart('start')}px` + : undefined, + insetInlineEnd: + column.getIsPinned() === 'end' ? `${column.getAfter('end')}px` : undefined, + width: `${column.getSize()}px`, + zIndex: column.getIsPinned() ? 1 : 0, + background: 'Canvas', +}) +``` + +## Common Mistakes + +### [HIGH] Using physical v8 regions + +Wrong: `column.pin('left')` + +Correct: `column.pin('start')` + +V9 uses logical `start` and `end`, including state and collection APIs. + +Source: `docs/framework/react/guide/migrating.md#column-pinning` + +### [HIGH] Expecting sticky CSS automatically + +Wrong: `column.pin('start')` + +Correct: `Object.assign(cell.style, style(column))` + +The feature only computes regions and offsets; the renderer owns positioning, backgrounds, overflow, and stacking. + +Source: `examples/react/column-pinning-sticky/src/main.tsx` + +### [HIGH] Diverging rendered and model widths + +Wrong: `cell.style.width = 'auto'` + +Correct: `cell.style.width = `${column.getSize()}px`` + +Sticky offsets use numeric sizes, so mismatched DOM widths create gaps or overlaps. + +Source: `docs/framework/react/guide/column-pinning.md#useful-column-pinning-apis` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/features/column-pinning/`; use CSS logical properties for direction-aware rendering. diff --git a/packages/table-core/skills/column-resizing/SKILL.md b/packages/table-core/skills/column-resizing/SKILL.md new file mode 100644 index 0000000000..5e036fcb2a --- /dev/null +++ b/packages/table-core/skills/column-resizing/SKILL.md @@ -0,0 +1,91 @@ +--- +name: column-resizing +description: > + Wire columnResizingFeature, header.getResizeHandler, resize mode and direction, pointer or touch events, and performant CSS-variable updates. Load when resize state changes but widths do not, or large tables resize slowly. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features', 'column-sizing'] +sources: + - 'TanStack/table:docs/framework/react/guide/column-resizing.md' + - 'TanStack/table:packages/table-core/src/features/column-resizing' + - 'TanStack/table:examples/react/column-resizing-performant' +--- + +This skill builds on `core`, `table-features`, and `column-sizing`. Resizing supplies gesture APIs and state; the renderer supplies handles and widths. + +## Setup + +```ts +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, +} from '@tanstack/table-core' + +export const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) +export const options = { + columnResizeMode: 'onChange' as const, + columnResizeDirection: 'ltr' as const, +} +``` + +## Core Patterns + +```ts +const handler = header.getResizeHandler() +handle.addEventListener('mousedown', handler) +handle.addEventListener('touchstart', handler) +``` + +For large grids, compute all visible sizes once per update and expose them as CSS variables. + +## Common Mistakes + +### [CRITICAL] Omitting sizing prerequisite + +Wrong: `tableFeatures({ columnResizingFeature })` + +Correct: `tableFeatures({ columnSizingFeature, columnResizingFeature })` + +Resizing modifies the sizing state and requires `columnSizingFeature`. + +Source: `packages/table-core/src/types/TableFeatures.ts#FeatureSlotPrereqs` + +### [HIGH] Rendering an inert handle + +Wrong: `handle.addEventListener('pointerdown', header.getResizeHandler())` + +Correct: + +```ts +const handler = header.getResizeHandler() +handle.addEventListener('mousedown', handler) +handle.addEventListener('touchstart', handler) +``` + +The shipped handler distinguishes `touchstart` from the mouse path. A single +`pointerdown` listener does not provide working touch resizing; wire both mouse +and touch start events. + +Source: `docs/framework/react/guide/column-resizing.md#connect-column-resizing-apis-to-ui` + +### [MEDIUM] Reading sizes in every cell + +Wrong: `cells.forEach(cell => cell.style.width = `${cell.column.getSize()}px`)` + +Correct: `root.style.setProperty(`--col-${header.id}`, `${header.getSize()}px`)` + +Caching sizes or CSS variables avoids repeated work during `onChange` resizing. + +Source: `examples/react/column-resizing-performant/src/main.tsx` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/features/column-resizing/` and the sizing feature directory for the state it updates. diff --git a/packages/table-core/skills/column-sizing/SKILL.md b/packages/table-core/skills/column-sizing/SKILL.md new file mode 100644 index 0000000000..3f2bee170b --- /dev/null +++ b/packages/table-core/skills/column-sizing/SKILL.md @@ -0,0 +1,72 @@ +--- +name: column-sizing +description: > + Use columnSizingFeature numeric size, minSize, maxSize, getSize, getStart, getAfter, and total-size APIs in table, grid, or flex CSS. Load for auto or percentage misconceptions and sizing/pinning layout mismatch. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features'] +sources: + - 'TanStack/table:docs/framework/react/guide/column-sizing.md' + - 'TanStack/table:packages/table-core/src/features/column-sizing' + - 'TanStack/table:examples/react/column-sizing' +--- + +This skill builds on `core` and `table-features`. Sizing is numeric state; translating it to layout is a renderer decision. + +## Setup + +```ts +import { columnSizingFeature, tableFeatures } from '@tanstack/table-core' + +export const features = tableFeatures({ columnSizingFeature }) +export const defaultColumn = { size: 180, minSize: 80, maxSize: 480 } +``` + +## Core Patterns + +```ts +cell.style.width = `${cell.column.getSize()}px` +tableElement.style.width = `${table.getTotalSize()}px` +``` + +Apply the same model sizes consistently to headers, cells, and pinned offsets. + +## Common Mistakes + +### [HIGH] Expecting state to style DOM + +Wrong: `const options = { defaultColumn: { size: 180 } }` + +Correct: `cell.style.width = `${cell.column.getSize()}px`` + +Numeric state does not apply CSS to headless markup. + +Source: `docs/framework/react/guide/column-sizing.md#column-size-apis` + +### [HIGH] Passing CSS sizes into numeric APIs + +Wrong: `const column = { size: '25%' }` + +Correct: `const column = { size: 240 }` + +Table sizing state is numeric; percentages and auto layout belong in renderer CSS. + +Source: `packages/table-core/src/features/column-sizing/columnSizingFeature.types.ts` + +### [HIGH] Letting content override offsets + +Wrong: `cell.style.minWidth = 'max-content'` + +Correct: `cell.style.width = `${column.getSize()}px`; cell.style.overflow = 'hidden'` + +If rendered width differs from the model, totals and pinning offsets no longer match geometry. + +Source: `examples/react/column-sizing/src/main.tsx` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/features/column-sizing/` for defaults and region-aware offset signatures. diff --git a/packages/table-core/skills/column-visibility/SKILL.md b/packages/table-core/skills/column-visibility/SKILL.md new file mode 100644 index 0000000000..6771b87a2a --- /dev/null +++ b/packages/table-core/skills/column-visibility/SKILL.md @@ -0,0 +1,75 @@ +--- +name: column-visibility +description: > + Hide columns with columnVisibilityFeature while rendering visibility-aware header, column, and cell collections. Load when hidden columns remain in the DOM, false-versus-absent state is confused, or enableHiding is misunderstood. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features'] +sources: + - 'TanStack/table:docs/framework/react/guide/column-visibility.md' + - 'TanStack/table:packages/table-core/src/features/column-visibility' + - 'TanStack/table:examples/react/column-visibility' +--- + +This skill builds on `core` and `table-features`. Visibility state changes visibility-aware models; it never removes the column definition. + +## Setup + +```ts +import { columnVisibilityFeature, tableFeatures } from '@tanstack/table-core' + +export const features = tableFeatures({ columnVisibilityFeature }) +export const initialState = { columnVisibility: { internalId: false } } +``` + +## Core Patterns + +```ts +const headers = table.getHeaderGroups() +const cells = row.getVisibleCells() +const toggles = table + .getAllLeafColumns() + .filter((column) => column.getCanHide()) +``` + +Use all-column APIs for controls and visible APIs for rendered table content. + +## Common Mistakes + +### [HIGH] Rendering hidden cells anyway + +Wrong: `row.getAllCells().map(renderCell)` + +Correct: `row.getVisibleCells().map(renderCell)` + +All-cell APIs intentionally include hidden columns. + +Source: `docs/framework/react/guide/column-visibility.md#column-visibility-aware-table-apis` + +### [HIGH] Treating absence as hidden + +Wrong: `const hidden = !columnVisibility[column.id]` + +Correct: `const hidden = columnVisibility[column.id] === false` + +Only explicit `false` hides a column; an absent entry is visible. + +Source: `packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts` + +### [MEDIUM] Treating enableHiding as visibility + +Wrong: `helper.accessor('id', { enableHiding: false })` + +Correct: `const initialState = { columnVisibility: { id: false } }` + +`enableHiding` controls whether hiding is allowed; visibility belongs in table state. + +Source: `packages/table-core/src/features/column-visibility/columnVisibilityFeature.types.ts` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/features/column-visibility/` for visibility-aware table, row, and column APIs. diff --git a/packages/table-core/skills/core/SKILL.md b/packages/table-core/skills/core/SKILL.md new file mode 100644 index 0000000000..07cad06ef5 --- /dev/null +++ b/packages/table-core/skills/core/SKILL.md @@ -0,0 +1,140 @@ +--- +name: core +description: > + Use TanStack Table v9 as a headless data-grid state and row-processing engine. Load for first-table architecture, stable data and columns, semantic rendering, framework adapter choice, or deciding what Table owns versus the renderer. +metadata: + type: core + library: '@tanstack/table-core' + library_version: '9.0.0-beta.38' +sources: + - 'TanStack/table:docs/overview.md' + - 'TanStack/table:docs/guide/tables.md' + - 'TanStack/table:docs/guide/data.md' + - 'TanStack/table:packages/table-core/src/index.ts' +--- + +# TanStack Table Core + +TanStack Table creates a table instance, state, and row models. It does not render a component, choose a component library, apply CSS, or supply interaction accessibility. Use a framework adapter in UI code; use `constructTable` only for framework-neutral integrations. + +## Setup + + + +```ts +import { + constructTable, + createColumnHelper, + tableFeatures, +} from '@tanstack/table-core' +import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings' + +type Person = { id: string; name: string } +const features = tableFeatures({ + coreReactivityFeature: storeReactivityBindings(), +}) +const helper = createColumnHelper() +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) +const data: Person[] = [{ id: '1', name: 'Ada' }] +const table = constructTable({ + features, + columns, + data, + getRowId: (row) => row.id, +}) + +for (const row of table.getRowModel().rows) { + console.log(row.getAllCells().map((cell) => cell.getValue())) +} +``` + +## Core Patterns + +### Start with core, add only behavior used + +```ts +const features = tableFeatures({ + coreReactivityFeature: storeReactivityBindings(), +}) +``` + +The core row model is automatic; filtering, sorting, pagination, and other optional behavior require their feature plugins. + +### Keep model inputs stable + +```ts +const data: Person[] = [{ id: '1', name: 'Ada' }] +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) +``` + +Define static inputs once and preserve query/store references when data has not changed. + +## Common Mistakes + +### [HIGH] Expecting Table to render a grid + +Wrong: + +```ts +document.body.append(table as unknown as Node) +``` + +Correct: + +```ts +const names = table + .getRowModel() + .rows.map((row) => row.getValue('name')) +document.body.textContent = names.join(', ') +``` + +The table instance is a model; markup, CSS, semantics, and accessibility are renderer responsibilities. + +Source: `docs/overview.md` + +### [HIGH] Recreating model inputs repeatedly + +Wrong: + +```ts +const options = () => ({ + data: source.map((item) => item), + columns: helper.columns([]), +}) +``` + +Correct: + +```ts +const data = source.map((item) => item) +const columns = helper.columns([]) +const options = () => ({ data, columns }) +``` + +New references invalidate memoized row and column work and can create adapter render loops. + +Source: `docs/guide/data.md` + +### [HIGH] Detaching prototype-bound methods + +Wrong: + +```ts +const { getValue } = table.getRowModel().rows[0]! +getValue('name') +``` + +Correct: + +```ts +const row = table.getRowModel().rows[0]! +row.getValue('name') +``` + +V9 row, cell, column, and header methods use their instance as `this`. + +Source: `docs/framework/react/guide/migrating.md#instance-methods-must-be-called-on-their-instance` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/index.ts`, then follow the exported implementation. For UI creation and rendering, inspect `node_modules/@tanstack/-table/src/index.ts` and load that adapter's getting-started skill. diff --git a/packages/table-core/skills/custom-features/SKILL.md b/packages/table-core/skills/custom-features/SKILL.md new file mode 100644 index 0000000000..be069c6f89 --- /dev/null +++ b/packages/table-core/skills/custom-features/SKILL.md @@ -0,0 +1,207 @@ +--- +name: custom-features +description: > + Author a TanStack Table v9 feature plugin across every FeatureMap and API installation surface: state, options, column definitions, table, column, row, cell, header, row-model functions/caches, defaults, prototypes, and instance data. Load only for reusable behavior not covered by built-ins, meta, or option composition. +metadata: + type: sub-skill + library: '@tanstack/table-core' + library_version: '9.0.0-beta.38' +requires: ['core', 'table-features', 'typescript'] +sources: + - 'TanStack/table:docs/framework/react/guide/custom-features.md' + - 'TanStack/table:packages/table-core/src/types' + - 'TanStack/table:packages/table-core/src/types/TableFeatures.ts' + - 'TanStack/table:packages/table-core/src/utils.ts' + - 'TanStack/table:packages/table-core/src/features' + - 'TanStack/table:examples/react/custom-plugin' +--- + +This skill builds on `core`, `table-features`, and `typescript`. Prefer a built-in feature or typed `tableMeta`/`columnMeta` for application callbacks; create a feature only for reusable state or behavior that must augment Table objects. + +## Extension Surface + +Declaration-merge `Plugins` to register the feature key, then merge only the maps backed by runtime behavior: + +| Feature map | Contribution | +| ------------------------------------------------ | ----------------------------------------- | +| `TableState_FeatureMap` | State slices in options, atoms, and store | +| `TableOptions_FeatureMap` | Table options and change callbacks | +| `Table_FeatureMap` | Public table APIs | +| `ColumnDef_FeatureMap` | Column-definition options | +| `Column_FeatureMap` | Public column APIs/data | +| `Row_FeatureMap` | Public row APIs/data | +| `Cell_FeatureMap` | Public cell APIs | +| `Header_FeatureMap` | Public header APIs | +| `RowModelFns_FeatureMap` | Advanced `table._rowModelFns` registries | +| `CachedRowModels_FeatureMap` | Advanced `table._rowModels` cache getters | + +`assignTableAPIs` installs singleton table methods. Use `assignPrototypeAPIs` inside `assignColumnPrototype`, `assignRowPrototype`, `assignCellPrototype`, or `assignHeaderPrototype` for shared object methods. Use `initColumnInstanceData` and `initRowInstanceData` only for per-instance fields. + +## Complete Example + +This single example shows every ordinary FeatureMap and API installation path. It names the two advanced row-model maps without enabling them because density does not own the row-model pipeline. + + + +```ts +import { + assignPrototypeAPIs, + assignTableAPIs, + functionalUpdate, + makeStateUpdater, + tableFeatures, + type CellData, + type OnChangeFn, + type RowData, + type TableFeature, + type TableFeatures, + type Updater, +} from '@tanstack/table-core' + +type Density = 'sm' | 'md' +interface DensityState { + density: Density +} +interface DensityOptions { + onDensityChange?: OnChangeFn +} +interface DensityColumnDef { + enableDensity?: boolean +} +interface DensityAPI { + getDensity: () => Density +} +interface DensityTable extends DensityAPI { + setDensity: (updater: Updater) => void +} +interface DensityColumn extends DensityAPI { + densityAtCreation?: Density +} +interface DensityRow extends DensityAPI { + densityAtCreation?: Density +} +type DensityCell = DensityAPI +type DensityHeader = DensityAPI + +declare module '@tanstack/table-core' { + interface Plugins { + densityFeature: TableFeature + } + interface TableState_FeatureMap { + densityFeature: DensityState + } + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityFeature: DensityOptions + } + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityFeature: DensityTable + } + interface ColumnDef_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + > { + densityFeature: DensityColumnDef + } + interface Column_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityFeature: DensityColumn + } + interface Row_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityFeature: DensityRow + } + interface Cell_FeatureMap { + densityFeature: DensityCell + } + interface Header_FeatureMap { + densityFeature: DensityHeader + } + + // If this feature actually owned a row-model stage, it would also merge + // RowModelFns_FeatureMap and CachedRowModels_FeatureMap here, then populate + // table._rowModelFns/table._rowModels with matching runtime/cache wiring. +} + +const readDensity = (table: unknown): Density => + ( + table as { + atoms: { density: { get: () => Density } } + } + ).atoms.density.get() + +export const densityFeature: TableFeature = { + getInitialState: (state) => ({ density: 'md', ...state }), + getDefaultTableOptions: (table) => ({ + onDensityChange: makeStateUpdater('density', table), + }), + constructTableAPIs: (table) => { + assignTableAPIs('densityFeature', table, { + table_getDensity: { fn: () => readDensity(table) }, + table_setDensity: { + fn: (updater: Updater) => + (table.options as DensityOptions).onDensityChange?.((old) => + functionalUpdate(updater, old), + ), + }, + }) + }, + assignColumnPrototype: (prototype, table) => { + assignPrototypeAPIs('densityFeature', prototype, table, { + column_getDensity: { fn: (column) => readDensity(column.table) }, + }) + }, + assignRowPrototype: (prototype, table) => { + assignPrototypeAPIs('densityFeature', prototype, table, { + row_getDensity: { fn: (row) => readDensity(row.table) }, + }) + }, + assignCellPrototype: (prototype, table) => { + assignPrototypeAPIs('densityFeature', prototype, table, { + cell_getDensity: { fn: (cell) => readDensity(cell.table) }, + }) + }, + assignHeaderPrototype: (prototype, table) => { + assignPrototypeAPIs('densityFeature', prototype, table, { + header_getDensity: { fn: (header) => readDensity(header.table) }, + }) + }, + initColumnInstanceData: (column) => { + ;(column as unknown as DensityColumn).densityAtCreation = readDensity( + column.table, + ) + }, + initRowInstanceData: (row) => { + ;(row as unknown as DensityRow).densityAtCreation = readDensity(row.table) + }, +} + +export const features = tableFeatures({ densityFeature }) +``` + +`getDefaultColumnDef` is also available for overridable column defaults; add it when the feature owns a real column-definition default. Do not add lifecycle hooks merely to fill the surface. + +## Guardrails + +- Keep the feature object and `tableFeatures({ densityFeature })` result stable. +- Match every declaration-merged API with runtime installation. Types alone do nothing. +- Preserve incoming state in `getInitialState`; put user state after defaults. +- Method keys use `table_`, `column_`, `row_`, `cell_`, or `header_`; the prefix is removed on installation. +- Table API `fn` receives declared arguments. Prototype API `fn` receives the current object first. +- Add `memoDeps` only for a genuinely derived method. Prototype methods are shared and must not close over per-object mutable data. +- There are no `assignColumnAPIs`, `assignRowAPIs`, `assignCellAPIs`, or `assignHeaderAPIs`; use `assignPrototypeAPIs` in the matching hook. +- Do not mutate constructed instances ad hoc or use a feature for renderer-only callbacks that belong in meta. + +## API Discovery + +Inspect exported `*_FeatureMap` interfaces under `node_modules/@tanstack/table-core/src/types/`, `TableFeature` in `types/TableFeatures.ts`, and `assignTableAPIs`/`assignPrototypeAPIs` in `utils.ts`. Copy lifecycle shapes—not domain behavior—from the nearest stock feature under `src/features/`. diff --git a/packages/table-core/skills/customizing-feature-behavior/SKILL.md b/packages/table-core/skills/customizing-feature-behavior/SKILL.md deleted file mode 100644 index b1ed2f6f1e..0000000000 --- a/packages/table-core/skills/customizing-feature-behavior/SKILL.md +++ /dev/null @@ -1,423 +0,0 @@ ---- -name: customizing-feature-behavior -description: > - Override per-column `sortFn`, `filterFn`, `aggregationFn` and table-level - `globalFilterFn` in TanStack Table v9. Covers built-in `filterFns` / `sortFns` / - `aggregationFns` registries (registered as slots on `tableFeatures({...})`), - authoring custom functions with the `FilterFn` / `SortFn` / `AggregationFn` - signatures, chaining filter→sort via the `addMeta` callback + - `row.columnFiltersMeta`, `resolveFilterValue`, `autoRemove`, `invertSorting`, - `sortUndefined` ('first'|'last'|-1|1), and `sortDescFirst`. Distinguishes - `aggregationFn` (produces value) from `aggregatedCell` (renders value). -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management -sources: - - TanStack/table:docs/guide/sorting.md - - TanStack/table:docs/guide/column-filtering.md - - TanStack/table:docs/guide/fuzzy-filtering.md - - TanStack/table:packages/table-core/src/fns/filterFns.ts - - TanStack/table:packages/table-core/src/fns/sortFns.ts - - TanStack/table:packages/table-core/src/fns/aggregationFns.ts - - TanStack/table:examples/react/filters-fuzzy/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management`. Read it first for how feature plugins drive state slices. - -## Setup - -v9 customization happens in three places: - -1. **Built-in function registries** — `filterFns`, `sortFns`, `aggregationFns` — registered as named slots on `tableFeatures({...})` so unused fns tree-shake away. -2. **Per-column overrides** — `columnDef.filterFn`, `columnDef.sortFn`, `columnDef.aggregationFn` (string name OR inline function). -3. **Table-level overrides** — `tableOptions.globalFilterFn`. - -```ts -import { - tableFeatures, - rowSortingFeature, - columnFilteringFeature, - globalFilteringFeature, - columnGroupingFeature, - rowExpandingFeature, - createFilteredRowModel, - createSortedRowModel, - createGroupedRowModel, - createExpandedRowModel, - filterFns, - sortFns, - aggregationFns, - metaHelper, - createColumnHelper, -} from '@tanstack/table-core' -import type { - FilterFn, - SortFn, - AggregationFn, - TableFeatures, -} from '@tanstack/table-core' -import { - rankItem, - compareItems, - type RankingInfo, -} from '@tanstack/match-sorter-utils' - -type Person = { - id: string - firstName: string - lastName: string - revenue: number - status: 'single' | 'complicated' | 'relationship' -} - -// 1. Describe meta shape for the fuzzy filter. -interface FuzzyFilterMeta { - itemRank?: RankingInfo -} - -// 2. Extend TableFeatures so FilterFn / SortFn can reference filterMeta types. -type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } - -const fuzzyFilter: FilterFn = ( - row, - columnId, - value, - addMeta, -) => { - const itemRank = rankItem(row.getValue(columnId), value) - addMeta?.({ itemRank }) - return itemRank.passed -} - -// 3. Register everything on features — fn registries are named slots. -const features = tableFeatures({ - rowSortingFeature, - columnFilteringFeature, - globalFilteringFeature, - columnGroupingFeature, - rowExpandingFeature, - filteredRowModel: createFilteredRowModel(), - sortedRowModel: createSortedRowModel(), - groupedRowModel: createGroupedRowModel(), - expandedRowModel: createExpandedRowModel(), - filterFns: { ...filterFns, fuzzy: fuzzyFilter }, // keep built-ins + add custom - sortFns, - aggregationFns, - filterMeta: metaHelper(), -}) - -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - filterFn: 'fuzzy', // ← string ref typechecks because 'fuzzy' is in features.filterFns - sortFn: 'alphanumeric', - }), - columnHelper.accessor('revenue', { - aggregationFn: 'sum', - aggregatedCell: (info) => `$${info.getValue().toLocaleString()}`, - }), -]) - -const table = constructTable({ - features, - columns, - data, - globalFilterFn: 'fuzzy', -}) -``` - -## Core Patterns - -### Pick a built-in `sortFn` by name + direction control - -```ts -columnHelper.accessor('lastName', { - sortFn: 'alphanumeric', - sortDescFirst: false, - sortUndefined: 'last', // ABSOLUTE: always at end regardless of asc/desc -}) -``` - -Layered direction controls: - -- `sortDescFirst: true/false` — first click sorts descending -- `sortUndefined: 'first' | 'last' | -1 | 1 | false` — string forms are absolute; numeric flips with `desc` -- `invertSorting: true` — for "lower-is-better" scales (rank 1 above rank 2 even when descending) - -### Filter → sort handoff via `addMeta` - -```ts -const fuzzyFilter: FilterFn = ( - row, - columnId, - value, - addMeta, -) => { - const itemRank = rankItem(row.getValue(columnId), value) - addMeta?.({ itemRank }) - return itemRank.passed -} - -// Custom sortFn reads the meta the filter stashed -const fuzzySort: SortFn = (rowA, rowB, columnId) => { - let dir = 0 - if (rowA.columnFiltersMeta[columnId]) { - dir = compareItems( - rowA.columnFiltersMeta[columnId].itemRank!, - rowB.columnFiltersMeta[columnId].itemRank!, - ) - } - return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir -} - -columnHelper.accessor('fullName', { filterFn: 'fuzzy', sortFn: fuzzySort }) -``` - -`row.columnFiltersMeta` is keyed by the column id that produced the meta (or `'__global__'` for the global filter). The sortFn MUST look up the same column id its filterFn used. - -### Custom `aggregationFn` for grouping - -```ts -import type { AggregationFn } from '@tanstack/table-core' - -// Signature: (columnId, leafRows, childRows) → aggregated value -// leafRows = all descendant non-grouped rows -// childRows = immediate children (may be sub-aggregates at deeper levels) -const weightedMean: AggregationFn = ( - columnId, - leafRows, -) => { - let totalWeight = 0 - let weightedSum = 0 - leafRows.forEach((row) => { - const v = row.getValue(columnId) - const w = row.original.revenue - weightedSum += v * w - totalWeight += w - }) - return totalWeight === 0 ? 0 : weightedSum / totalWeight -} - -const customFeatures = tableFeatures({ - columnGroupingFeature, - rowExpandingFeature, - groupedRowModel: createGroupedRowModel(), - expandedRowModel: createExpandedRowModel(), - aggregationFns: { ...aggregationFns, weightedMean }, -}) - -const table = constructTable({ - features: customFeatures, - columns: columnHelper.columns([ - columnHelper.accessor('revenue', { - aggregationFn: 'weightedMean', - aggregatedCell: (info) => `$${info.getValue().toFixed(2)}`, - }), - ]), - data, -}) -``` - -## Common Mistakes - -### [CRITICAL] Referencing a custom `filterFn` by string without registering it - -Wrong: - -```ts -// "fuzzy" string never registered in features.filterFns -const features = tableFeatures({ - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, // ❌ built-ins only — no fuzzy -}) -const table = useTable({ - features, - columns: [columnHelper.accessor('fullName', { filterFn: 'fuzzy' })], - data, -}) -``` - -Correct: - -```ts -const fuzzyFilter: FilterFn = ( - row, - columnId, - value, - addMeta, -) => { - const itemRank = rankItem(row.getValue(columnId), value) - addMeta?.({ itemRank }) - return itemRank.passed -} - -const features = tableFeatures({ - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns: { ...filterFns, fuzzy: fuzzyFilter }, // ✅ registered - filterMeta: metaHelper(), -}) - -const table = useTable({ - features, - columns: [columnHelper.accessor('fullName', { filterFn: 'fuzzy' })], - data, -}) -``` - -String values are looked up in `features.filterFns`. Unregistered names log `Could not find a valid 'column.filterFn' …` in dev and silently no-op in prod. - -Source: examples/react/filters-fuzzy/src/main.tsx; packages/table-core/src/features/column-filtering/columnFilteringFeature.utils.ts - -### [HIGH] Using v8 `sortingFn` / `sortingFns` names - -Wrong: - -```ts -columnHelper.accessor('age', { - sortingFn: 'alphanumeric', // v8 name — ignored -}) -``` - -Correct: - -```ts -columnHelper.accessor('age', { - sortFn: 'alphanumeric', -}) -``` - -v9 renamed every sorting API: `sortingFn` → `sortFn`, `sortingFns` → `sortFns`, type `SortingFn` → `SortFn`, `column.getSortingFn()` → `column.getSortFn()`. The default `sortFn` is `'auto'`, falling back to `sortFn_basic` if the lookup misses — so wrong names sort wrong instead of erroring. - -Source: docs/framework/react/guide/migrating.md; packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts - -### [HIGH] Custom `sortFn` reads filter meta from a different column id - -Wrong: - -```ts -// filter on 'fullName', sort reads meta from 'firstName' -const fuzzySort: SortFn = (a, b, columnId) => { - const meta = a.columnFiltersMeta['firstName'] // ❌ wrong key - return meta - ? compareItems(meta.itemRank, b.columnFiltersMeta['firstName'].itemRank) - : 0 -} -columnHelper.accessor('fullName', { filterFn: 'fuzzy', sortFn: fuzzySort }) -``` - -Correct: - -```ts -const fuzzySort: SortFn = (rowA, rowB, columnId) => { - let dir = 0 - if (rowA.columnFiltersMeta[columnId]) { - dir = compareItems( - rowA.columnFiltersMeta[columnId].itemRank!, - rowB.columnFiltersMeta[columnId].itemRank!, - ) - } - return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir -} -``` - -`row.columnFiltersMeta` is keyed by the column id that produced it. Always use the `columnId` argument the sortFn receives. - -Source: examples/react/filters-fuzzy/src/main.tsx - -### [MEDIUM] Returning a complex value from the accessor while using a built-in `sortFn` - -Wrong: - -```ts -// accessor returns object; alphanumeric sees "[object Object]" -columnHelper.accessor((row) => row.name, { - id: 'name', - sortFn: 'alphanumeric', -}) -``` - -Correct: - -```ts -// Option A — return a primitive -columnHelper.accessor((row) => `${row.name.first} ${row.name.last}`, { - id: 'fullName', - sortFn: 'alphanumeric', -}) - -// Option B — custom sortFn that knows the shape -columnHelper.accessor((row) => row.name, { - id: 'name', - sortFn: (a, b, id) => { - const av = a.getValue<{ first: string }>(id).first - const bv = b.getValue<{ first: string }>(id).first - return av === bv ? 0 : av > bv ? 1 : -1 - }, -}) -``` - -Built-in sortFns (`alphanumeric`, `text`, `basic`) coerce via comparison operators. Object accessors collapse to `"[object Object]"` and every row ties. - -Source: packages/table-core/src/fns/sortFns.ts - -### [MEDIUM] Confusing `aggregationFn` with `aggregatedCell` - -Wrong: - -```ts -// rendering JSX inside the aggregation function -columnHelper.accessor('revenue', { - aggregationFn: (id, leaves) => ${leaves.reduce((a, r) => a + r.getValue(id), 0)}, -}) -``` - -Correct: - -```ts -columnHelper.accessor('revenue', { - aggregationFn: 'sum', // returns a value - aggregatedCell: (info) => ${info.getValue().toLocaleString()}, // renders it -}) -``` - -`aggregationFn` produces the grouped-row value (signature `(columnId, leafRows, childRows)`). `aggregatedCell` renders it. Don't combine. - -Source: packages/table-core/src/features/column-grouping/columnGroupingFeature.types.ts - -### [CRITICAL] Reimplementing what built-in APIs provide - -Wrong: - -```ts -// Reimplements sorting state manually instead of using the API -const [sorting, setSorting] = useState([]) -const sortedData = useMemo(() => [...data].sort(/* … */), [data, sorting]) -``` - -Correct: - -```ts -const table = useTable({ - features: tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, - }), - columns, - data, -}) -// table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() -``` - -Source: maintainer interview (Phase 4, 2026-05-17) - -## See also - -- `tanstack-table/filtering` — `filterFn` placement, fuzzy filter pattern, faceted UI -- `tanstack-table/sorting` — built-in `sortFns`, multi-sort, `sortUndefined` -- `tanstack-table/grouping` — `aggregationFn` signature details and built-ins diff --git a/packages/table-core/skills/expanding/SKILL.md b/packages/table-core/skills/expanding/SKILL.md new file mode 100644 index 0000000000..c04760d93d --- /dev/null +++ b/packages/table-core/skills/expanding/SKILL.md @@ -0,0 +1,80 @@ +--- +name: expanding +description: > + Expand hierarchical subrows or custom detail panels with rowExpandingFeature, expandedRowModel, getSubRows, getRowCanExpand, manualExpanding, and paginateExpandedRows. Load when expansion state changes but no UI appears. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features', 'client-vs-server'] +sources: + - 'TanStack/table:docs/framework/react/guide/expanding.md' + - 'TanStack/table:packages/table-core/src/features/row-expanding' + - 'TanStack/table:examples/react/expanding' +--- + +This skill builds on `core`, `table-features`, and `client-vs-server`. Expansion controls row-tree inclusion; the renderer owns disclosure controls and detail panels. + +## Setup + +```ts +import { + createExpandedRowModel, + rowExpandingFeature, + tableFeatures, +} from '@tanstack/table-core' + +type Node = { id: string; children?: Node[] } +export const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), +}) +export const options = { getSubRows: (row: Node) => row.children } +``` + +## Core Patterns + +```ts +const detailOptions = { getRowCanExpand: () => true } +const isDetailOpen = row.getIsExpanded() +``` + +For custom detail UI, render an additional row/panel when `isDetailOpen` is true. + +## Common Mistakes + +### [HIGH] Omitting the subrow accessor + +Wrong: `const options = { data: nestedRows }` + +Correct: `const options = { data: nestedRows, getSubRows: (row: Node) => row.children }` + +Nested object structure is not inferred automatically. + +Source: `docs/framework/react/guide/expanding.md#table-rows-as-expanded-data` + +### [HIGH] Expecting detail markup from state + +Wrong: `row.toggleExpanded(true)` + +Correct: `if (row.getIsExpanded()) renderDetail(row.original)` + +Table is headless and cannot render a custom detail panel. + +Source: `docs/framework/react/guide/expanding.md#custom-expanding-ui` + +### [HIGH] Misreading pagination placement + +Wrong: `const options = { paginateExpandedRows: false }; const leafOnlyPages = true` + +Correct: `const options = { paginateExpandedRows: false }; const descendantsStayWithParent = true` + +This option controls whether expansion happens before or after pagination; it does not make page size count only leaf rows. + +Source: `packages/table-core/src/features/row-expanding/rowExpandingFeature.types.ts` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/features/row-expanding/` for expansion state, row APIs, and model placement. diff --git a/packages/table-core/skills/filtering/SKILL.md b/packages/table-core/skills/filtering/SKILL.md deleted file mode 100644 index 8461aedda8..0000000000 --- a/packages/table-core/skills/filtering/SKILL.md +++ /dev/null @@ -1,375 +0,0 @@ ---- -name: filtering -description: > - Filter rows in TanStack Table v9 with the `filteredRowModel` pipeline stage. - Covers `columnFilteringFeature` + `globalFilteringFeature` + `columnFacetingFeature`, - `createFilteredRowModel()` (registered on `features` with `filterFns` slot), `createFacetedRowModel()` / - `createFacetedUniqueValues()` / `createFacetedMinMaxValues()`, fuzzy filtering - with `@tanstack/match-sorter-utils`, the built-in `filterFns` registry, custom - `filterFn` + module augmentation, `state.columnFilters` (Array<{ id, value }>), - `state.globalFilter`, `column.setFilterValue` / `setColumnFilters` / - `setGlobalFilter`, `column.getFacetedUniqueValues` / - `column.getFacetedMinMaxValues`, `manualFiltering`, `filterFromLeafRows`, - `maxLeafRowFilterDepth`, `getColumnCanGlobalFilter`. Five subsystems: - column-filtering, global-filtering, column-faceting, global-faceting, - fuzzy-filtering. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management - - customizing-feature-behavior -sources: - - TanStack/table:docs/guide/column-filtering.md - - TanStack/table:docs/guide/global-filtering.md - - TanStack/table:docs/guide/column-faceting.md - - TanStack/table:docs/guide/global-faceting.md - - TanStack/table:docs/guide/fuzzy-filtering.md - - TanStack/table:examples/react/filters/src/main.tsx - - TanStack/table:examples/react/filters-faceted/src/main.tsx - - TanStack/table:examples/react/filters-fuzzy/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/customizing-feature-behavior`. Read those first for the atom model and `filterFn`/`globalFilterFn` overrides. - -## Setup - -Filtering has five subsystems in v9 — register only the features you need: - -| Subsystem | Feature | Row-model | -| ---------------- | ------------------------ | --------------------------------------------- | -| column-filtering | `columnFilteringFeature` | `createFilteredRowModel()` + `filterFns` slot | -| global-filtering | `globalFilteringFeature` | (same `filteredRowModel`) | -| column-faceting | `columnFacetingFeature` | `createFacetedRowModel()` + helpers | -| global-faceting | `globalFacetingFeature` | global versions of the helpers | -| fuzzy-filtering | (custom filterFn) | `@tanstack/match-sorter-utils` | - -```ts -import { - tableFeatures, - columnFilteringFeature, - globalFilteringFeature, - rowPaginationFeature, - createFilteredRowModel, - createPaginatedRowModel, - filterFns, -} from '@tanstack/table-core' -import type { ColumnFiltersState } from '@tanstack/table-core' - -const features = tableFeatures({ - columnFilteringFeature, - globalFilteringFeature, - rowPaginationFeature, - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - filterFns, -}) - -const table = constructTable({ - features, - columns, - data, - initialState: { - columnFilters: [] satisfies ColumnFiltersState, - globalFilter: '', - }, - globalFilterFn: 'includesString', -}) - -table.setColumnFilters([{ id: 'firstName', value: 'Ada' }]) -table.setGlobalFilter('Lovelace') -``` - -## Core Patterns - -### Text / range / select column filters - -```tsx -// From examples/react/filters/src/main.tsx -function Filter({ column }) { - const firstValue = column - .getFacetedRowModel() - .flatRows[0]?.getValue(column.id) - const columnFilterValue = column.getFilterValue() - - return typeof firstValue === 'number' ? ( -
- - column.setFilterValue((old: [number, number]) => [ - e.target.value, - old?.[1], - ]) - } - placeholder="Min" - /> - - column.setFilterValue((old: [number, number]) => [ - old?.[0], - e.target.value, - ]) - } - placeholder="Max" - /> -
- ) : ( - column.setFilterValue(e.target.value)} - placeholder="Search…" - /> - ) -} -``` - -### Faceted filter UIs and fuzzy global search - -Faceting requires `createFacetedRowModel()` as the base plus `createFacetedUniqueValues()` / `createFacetedMinMaxValues()` — all registered on the `features` object; fuzzy filtering wires a custom `FilterFn` backed by `@tanstack/match-sorter-utils` (`rankItem` / `compareItems`) with the `filterFns` registry slot and `filterMeta` slot for type-safe meta. Full examples in [faceting-and-fuzzy.md](references/faceting-and-fuzzy.md). - -### Server-side filtering - -```tsx -const [columnFilters, setColumnFilters] = useState([]) -const { data } = useQuery({ - queryKey: ['rows', columnFilters], - queryFn: () => - fetch('/api/rows?' + serialize(columnFilters)).then((r) => r.json()), -}) -const table = useTable({ - features: tableFeatures({ columnFilteringFeature }), - // no filteredRowModel registered — server filters - data, - columns, - manualFiltering: true, - state: { columnFilters }, - onColumnFiltersChange: setColumnFilters, -}) -``` - -### Filter tree data and keep matching descendants visible - -```ts -const features = tableFeatures({ - columnFilteringFeature, - rowExpandingFeature, - filteredRowModel: createFilteredRowModel(), - expandedRowModel: createExpandedRowModel(), - filterFns, -}) -const table = constructTable({ - features, - columns, - data, - getSubRows: (r) => r.subRows, - filterFromLeafRows: true, // bottom-up: keep parent if any descendant matches -}) -``` - -## Common Mistakes - -### [HIGH] Forgetting `createFacetedRowModel()` while registering `createFacetedUniqueValues()` / `createFacetedMinMaxValues()` - -Wrong: - -```tsx -const table = useTable({ - features: tableFeatures({ - columnFacetingFeature, - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - // BUG: missing facetedRowModel - facetedUniqueValues: createFacetedUniqueValues(), - facetedMinMaxValues: createFacetedMinMaxValues(), - filterFns, - }), - columns, - data, -}) -``` - -Correct: - -```tsx -const table = useTable({ - features: tableFeatures({ - columnFacetingFeature, - columnFilteringFeature, - rowPaginationFeature, - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - facetedRowModel: createFacetedRowModel(), // REQUIRED base - facetedMinMaxValues: createFacetedMinMaxValues(), - facetedUniqueValues: createFacetedUniqueValues(), - filterFns, - }), - columns, - data, -}) -``` - -Without the base `facetedRowModel`, the unique/minMax helpers fall back to `getPreFilteredRowModel()` — facet values stop excluding the column's own active filter, and a select dropdown collapses to only the currently selected value once the user picks one. - -Source: packages/table-core/src/features/column-faceting/columnFacetingFeature.utils.ts; examples/react/filters-faceted/src/main.tsx - -### [HIGH] Setting `manualFiltering: true` without refetching data - -Wrong: - -```tsx -// manualFiltering bypasses the filteredRowModel — filter UI changes do nothing -const table = useTable({ - features: tableFeatures({ - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, - }), - data, - columns, - manualFiltering: true, - // ...but no useEffect / useQuery key tracking columnFilters -}) -``` - -Correct: - -```tsx -const [columnFilters, setColumnFilters] = useState([]) -const { data } = useQuery({ - queryKey: ['rows', columnFilters], - queryFn: () => - fetch('/api/rows?' + serialize(columnFilters)).then((r) => r.json()), -}) - -const table = useTable({ - features: tableFeatures({ columnFilteringFeature }), - // no filteredRowModel registered for manual mode - data, - columns, - manualFiltering: true, - state: { columnFilters }, - onColumnFiltersChange: setColumnFilters, -}) -``` - -With `manualFiltering: true`, `table_getFilteredRowModel` short-circuits and returns the core rows. Rows are NOT filtered client-side — you must refetch. - -Source: docs/guide/column-filtering.md; packages/table-core/src/core/row-models/coreRowModelsFeature.utils.ts - -### [HIGH] Custom fuzzy filter without merging into `filterFns` - -Wrong: - -```tsx -// drops the built-in registry -const features = tableFeatures({ - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns: { fuzzy: fuzzyFilter }, // BUG: drops built-ins -}) -// Column with filterFn: 'includesString' now warns and never filters -``` - -Correct: - -```tsx -import { filterFns } from '@tanstack/react-table' - -const features = tableFeatures({ - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns: { - ...filterFns, // KEEP built-ins - fuzzy: fuzzyFilter, // ADD custom - }, -}) -``` - -The `filterFns` slot replaces the registry with whatever you provide. Any column using a built-in name like `'includesString'` becomes a no-op if the built-ins are dropped. - -Source: examples/react/filters-fuzzy/src/main.tsx - -### [MEDIUM] Global filter silently skips columns with non-string/non-number values - -Wrong: - -```tsx -// createdAt is a Date object — global filter silently skips it -const columns = [ - columnHelper.accessor('createdAt', { header: 'Created' }), - columnHelper.accessor('name', { header: 'Name' }), -] -// table.setGlobalFilter('2024') will never find Date rows -``` - -Correct: - -```tsx -const table = useTable({ - features: tableFeatures({ - globalFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, - }), - columns, - data, - globalFilterFn: 'includesString', - getColumnCanGlobalFilter: (column) => true, // include every column -}) - -// Or per-column: -columnHelper.accessor('createdAt', { - header: 'Created', - enableGlobalFilter: true, -}) -``` - -`globalFilteringFeature` defaults `getColumnCanGlobalFilter` to a function that returns `typeof value === 'string' || typeof value === 'number'` sampled from the first row. Objects, dates, booleans, undefined all silently fail. - -Source: packages/table-core/src/features/global-filtering/globalFilteringFeature.ts - -### [CRITICAL] Reimplementing what built-in APIs provide - -Wrong: - -```ts -// Hand-rolled filter loop, bypassing the table -const filteredData = useMemo( - () => data.filter(/* …custom matching… */), - [data, query], -) -``` - -Correct: - -```ts -const table = useTable({ - features: tableFeatures({ - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, - }), - columns, - data, -}) -table.setColumnFilters([{ id: 'name', value: 'Ada' }]) -// or: column.setFilterValue('Ada') -``` - -`table.setColumnFilters`, `column.setFilterValue`, `table.setGlobalFilter` honor reset behavior and internal invariants. - -Source: maintainer interview (Phase 4, 2026-05-17) - -## See also - -- `tanstack-table/customizing-feature-behavior` — `filterFn` / `globalFilterFn` authoring, `addMeta` chain -- `tanstack-table/sorting` — fuzzy sort pairing for `match-sorter-utils` -- `tanstack-table/row-expanding` — `filterFromLeafRows` interaction with tree data - -## References - -- [faceting-and-fuzzy.md](references/faceting-and-fuzzy.md) — faceted filter UIs (autocomplete + range slider) with `createFacetedRowModel`/`createFacetedUniqueValues`/`createFacetedMinMaxValues`, fuzzy global search via `@tanstack/match-sorter-utils`, plus MEDIUM-priority failure modes: `state` + `initialState` collision, `filterFromLeafRows` semantics, `'auto'` filter misdetection on null first row diff --git a/packages/table-core/skills/filtering/references/faceting-and-fuzzy.md b/packages/table-core/skills/filtering/references/faceting-and-fuzzy.md deleted file mode 100644 index 2ed0e081a6..0000000000 --- a/packages/table-core/skills/filtering/references/faceting-and-fuzzy.md +++ /dev/null @@ -1,218 +0,0 @@ -# Faceting and fuzzy filtering — extended patterns - -Extended filtering patterns extracted from `SKILL.md`. The SKILL keeps simple column filter, global filter, server-side, and tree-data patterns inline; this file covers faceted UIs and fuzzy filtering with `@tanstack/match-sorter-utils`. - -## Faceted filter UIs (autocomplete + range slider) - -```ts -import { - columnFacetingFeature, - createFacetedRowModel, - createFacetedUniqueValues, - createFacetedMinMaxValues, -} from '@tanstack/table-core' - -const features = tableFeatures({ - columnFacetingFeature, - columnFilteringFeature, - rowPaginationFeature, - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - facetedRowModel: createFacetedRowModel(), // REQUIRED base - facetedMinMaxValues: createFacetedMinMaxValues(), - facetedUniqueValues: createFacetedUniqueValues(), - filterFns, -}) - -const table = constructTable({ - features, - columns, - data, -}) - -// In a Filter component: -const uniqueValues = column.getFacetedUniqueValues() // Map -const [min, max] = column.getFacetedMinMaxValues() ?? [0, 0] -``` - -## Fuzzy global search with match-sorter-utils - -```ts -import { - rankItem, - compareItems, - type RankingInfo, -} from '@tanstack/match-sorter-utils' -import { - tableFeatures, - columnFilteringFeature, - globalFilteringFeature, - rowSortingFeature, - createFilteredRowModel, - createSortedRowModel, - filterFns, - sortFns, - metaHelper, -} from '@tanstack/table-core' -import type { FilterFn, SortFn, TableFeatures } from '@tanstack/table-core' - -// 1. Describe the filterMeta shape for this feature set. -interface FuzzyFilterMeta { - itemRank?: RankingInfo -} - -// 2. Extend TableFeatures so FilterFn / SortFn can read filterMeta types. -type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } - -const fuzzyFilter: FilterFn = ( - row, - columnId, - value, - addMeta, -) => { - const itemRank = rankItem(row.getValue(columnId), value) - addMeta?.({ itemRank }) - return itemRank.passed -} - -const fuzzySort: SortFn = (rowA, rowB, columnId) => { - let dir = 0 - if (rowA.columnFiltersMeta[columnId]) { - dir = compareItems( - rowA.columnFiltersMeta[columnId].itemRank!, - rowB.columnFiltersMeta[columnId].itemRank!, - ) - } - return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir -} - -// 3. Register everything on features — no rowModels option needed. -const features = tableFeatures({ - columnFilteringFeature, - globalFilteringFeature, - rowSortingFeature, - filteredRowModel: createFilteredRowModel(), - sortedRowModel: createSortedRowModel(), - filterFns: { ...filterFns, fuzzy: fuzzyFilter }, - sortFns: { ...sortFns, fuzzy: fuzzySort }, - filterMeta: metaHelper(), -}) - -const table = constructTable({ - features, - columns, - data, - globalFilterFn: 'fuzzy', -}) -``` - -Use string refs in column defs when the fn is registered: - -```ts -columnHelper.accessor('fullName', { - filterFn: 'fuzzy', // string ref — typechecks because 'fuzzy' is in features.filterFns - sortFn: 'fuzzy', -}) -``` - -## Additional MEDIUM-priority failure modes - -### Using `state.columnFilters` AND `initialState.columnFilters` simultaneously - -Wrong: - -```tsx -const [columnFilters, setColumnFilters] = useState([]) -const table = useTable({ - initialState: { columnFilters: [{ id: 'name', value: 'John' }] }, // IGNORED - state: { columnFilters }, // wins, starts empty - onColumnFiltersChange: setColumnFilters, -}) -``` - -Correct: - -```tsx -// Seed the controlled state at useState time, NOT in initialState -const [columnFilters, setColumnFilters] = useState([ - { id: 'name', value: 'John' }, -]) -const table = useTable({ - state: { columnFilters }, - onColumnFiltersChange: setColumnFilters, -}) -``` - -`state` always overrides `initialState`. Seed your controlled state instead. - -Source: docs/guide/column-filtering.md - -### Expecting `filterFromLeafRows` to keep ALL children of a matching parent visible - -Wrong: - -```tsx -// filterFromLeafRows hides children that don't match individually -const table = useTable({ - features: tableFeatures({ - columnFilteringFeature, - rowExpandingFeature, - filteredRowModel: createFilteredRowModel(), - expandedRowModel: createExpandedRowModel(), - filterFns, - }), - columns, - data, - getSubRows: (r) => r.subRows, - filterFromLeafRows: true, - // expectation: parent matches "John" → all children visible - // reality: only children that also match "John" stay visible -}) -``` - -Correct: - -```tsx -// Filter root-only to preserve all sub-rows under a matching parent -const table = useTable({ - features: tableFeatures({ - columnFilteringFeature, - rowExpandingFeature, - filteredRowModel: createFilteredRowModel(), - expandedRowModel: createExpandedRowModel(), - filterFns, - }), - columns, - data, - getSubRows: (r) => r.subRows, - maxLeafRowFilterDepth: 0, -}) -``` - -`filterFromLeafRows: true` is bottom-up. The mutually exclusive top-down default is what preserves descendants under a matching parent. - -Source: packages/table-core/src/features/column-filtering/filterRowsUtils.ts - -### Auto filter type misdetects when first row has `null`/`undefined` - -Wrong: - -```ts -const data = [ - { id: 1, name: null }, // first row - { id: 2, name: 'Alice' }, -] -// Column filter returns 0 results — auto picked wrong filter from null -``` - -Correct: - -```ts -columnHelper.accessor('name', { - filterFn: 'includesString', // explicit, don't rely on auto -}) -``` - -The default `'auto'` `filterFn` infers from the first row's value. If it's null/undefined, type detection fails. - -Source: https://github.com/TanStack/table/issues/4711 diff --git a/packages/table-core/skills/global-filtering/SKILL.md b/packages/table-core/skills/global-filtering/SKILL.md new file mode 100644 index 0000000000..55776340b2 --- /dev/null +++ b/packages/table-core/skills/global-filtering/SKILL.md @@ -0,0 +1,84 @@ +--- +name: global-filtering +description: > + Apply globalFilter across eligible columns with globalFilteringFeature, columnFilteringFeature, filteredRowModel, globalFilterFn, and manual server filtering. Load when columns unexpectedly participate or a global filter changes state without changing rows. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features', 'client-vs-server', 'column-filtering'] +sources: + - 'TanStack/table:docs/framework/react/guide/global-filtering.md' + - 'TanStack/table:packages/table-core/src/features/global-filtering' + - 'TanStack/table:examples/react/filters' +--- + +This skill builds on `core`, `table-features`, `client-vs-server`, and `column-filtering`. Global filtering reuses the column-filtering pipeline. + +## Setup + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + filterFns, + globalFilteringFeature, + tableFeatures, +} from '@tanstack/table-core' + +export const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns, +}) +``` + +## Core Patterns + +```ts +const options = { + globalFilterFn: 'includesString' as const, + getColumnCanGlobalFilter: (column) => column.id !== 'actions', +} +``` + +Declare eligibility when product rules differ from default primitive-value detection. + +## Common Mistakes + +### [CRITICAL] Omitting column-filter prerequisite + +Wrong: `tableFeatures({ globalFilteringFeature })` + +Correct: `tableFeatures({ columnFilteringFeature, globalFilteringFeature, filteredRowModel: createFilteredRowModel() })` + +Global filtering depends on column filtering and needs the filtered model for client processing. + +Source: `packages/table-core/src/types/TableFeatures.ts#FeatureSlotPrereqs` + +### [HIGH] Assuming every column participates + +Wrong: `const options = { globalFilterFn: 'includesString' }` + +Correct: `const options = { getColumnCanGlobalFilter: column => column.id !== 'actions' }` + +Default eligibility uses the first core row and accepts string or number values. + +Source: `packages/table-core/src/features/global-filtering/globalFilteringFeature.ts` + +### [HIGH] Keeping manual filter local only + +Wrong: `table.setGlobalFilter(search); const options = { manualFiltering: true }` + +Correct: `const page = await fetchRows({ search }); const options = { data: page.rows, manualFiltering: true }` + +Manual filtering bypasses the client model, so the value must drive the server request. + +Source: `docs/framework/react/guide/global-filtering.md#manual-server-side-global-filtering` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/features/global-filtering/` plus `src/features/column-filtering/` for shared state and filter functions. diff --git a/packages/table-core/skills/grouping/SKILL.md b/packages/table-core/skills/grouping/SKILL.md index 74fc8b51c1..f931008a14 100644 --- a/packages/table-core/skills/grouping/SKILL.md +++ b/packages/table-core/skills/grouping/SKILL.md @@ -1,443 +1,99 @@ --- name: grouping description: > - Group rows by column values in TanStack Table v9 with the `groupedRowModel` - stage. Covers `columnGroupingFeature` + `createGroupedRowModel()` (registered on `features` with `aggregationFns` slot), - `state.grouping` (GroupingState = Array), `onGroupingChange`, - `columnDef.aggregationFn` ('auto'|name|fn) — distinct signature - `(columnId, leafRows, childRows)` — `columnDef.aggregatedCell`, - `columnDef.getGroupingValue`, `groupedColumnMode` (false|'reorder'|'remove'), - `manualGrouping`, `column.toggleGrouping` / `getCanGroup` / `getIsGrouped`, - `row.getIsGrouped` / `groupingColumnId` / `leafRows`, `cell.getIsGrouped` / - `getIsAggregated` / `getIsPlaceholder`, the built-in `aggregationFns` registry, - and the required `rowExpandingFeature` pairing for drill-down UX. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management - - customizing-feature-behavior + Group and aggregate rows with columnGroupingFeature, groupedRowModel, aggregationFns, groupedColumnMode, and manualGrouping. Load for grouped, placeholder, or aggregated cells and grouping interactions with expansion or pagination. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features', 'client-vs-server'] sources: - - TanStack/table:docs/guide/grouping.md - - TanStack/table:packages/table-core/src/fns/aggregationFns.ts - - TanStack/table:packages/table-core/src/features/column-grouping/createGroupedRowModel.ts - - TanStack/table:examples/react/grouping/src/main.tsx + - 'TanStack/table:docs/framework/react/guide/grouping.md' + - 'TanStack/table:packages/table-core/src/features/column-grouping' + - 'TanStack/table:examples/react/grouping' --- -This skill builds on `tanstack-table/state-management` and `tanstack-table/customizing-feature-behavior`. Read those first for the atom model and `aggregationFn` signature. +This skill builds on `core`, `table-features`, and `client-vs-server`. Grouping creates row structure and aggregate values; the renderer chooses grouped-cell UI. ## Setup -Grouping nearly always pairs with expanding — otherwise grouped rows show aggregates with no way to drill in. - ```ts import { - tableFeatures, + aggregationFns, columnGroupingFeature, - rowExpandingFeature, - rowPaginationFeature, createGroupedRowModel, - createExpandedRowModel, - createPaginatedRowModel, - aggregationFns, - createColumnHelper, - constructTable, + tableFeatures, } from '@tanstack/table-core' -import type { GroupingState } from '@tanstack/table-core' -const features = tableFeatures({ +export const features = tableFeatures({ columnGroupingFeature, - rowExpandingFeature, - rowPaginationFeature, groupedRowModel: createGroupedRowModel(), - expandedRowModel: createExpandedRowModel(), - paginatedRowModel: createPaginatedRowModel(), aggregationFns, }) - -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { - aggregatedCell: () => null, - enableGrouping: false, - }), - columnHelper.accessor('age', { aggregationFn: 'median' }), - columnHelper.accessor('visits', { aggregationFn: 'sum' }), - columnHelper.accessor('status', { aggregationFn: 'count' }), - columnHelper.accessor('progress', { aggregationFn: 'mean' }), -]) - -const table = constructTable({ - features, - columns, - data, - initialState: { grouping: [] satisfies GroupingState }, -}) - -table.setGrouping(['status']) ``` ## Core Patterns -### Per-column group toggle + aggregated cell rendering - -```tsx -// From examples/react/grouping/src/main.tsx -{ - headerGroup.headers.map((header) => ( - - {header.column.getCanGroup() ? ( - - ) : null} - - - )) -} - -// Cell renderer with three branches: grouped, aggregated, normal -{ - row.getVisibleCells().map((cell) => ( - - {cell.getIsGrouped() ? ( - <> - {' '} - ({row.subRows.length}) - - ) : cell.getIsAggregated() ? ( - - ) : cell.getIsPlaceholder() ? null : ( - - )} - - )) -} -``` - -### Custom `aggregationFn` - ```ts -import type { AggregationFn } from '@tanstack/table-core' - -// Signature: (columnId, leafRows, childRows) → aggregated value -// leafRows = ALL descendant non-grouped rows (recursive) -// childRows = immediate children (may themselves be sub-aggregates) -const weightedAverage: AggregationFn = ( - columnId, - leafRows, -) => { - let totalWeight = 0 - let weightedSum = 0 - leafRows.forEach((row) => { - const v = row.getValue(columnId) - const w = row.original.weight - weightedSum += v * w - totalWeight += w - }) - return totalWeight === 0 ? 0 : weightedSum / totalWeight -} - -const customFeatures = tableFeatures({ - columnGroupingFeature, - rowExpandingFeature, - groupedRowModel: createGroupedRowModel(), - expandedRowModel: createExpandedRowModel(), - aggregationFns: { ...aggregationFns, weightedAverage }, -}) - -const table = constructTable({ - features: customFeatures, - columns: columnHelper.columns([ - columnHelper.accessor('progress', { - aggregationFn: 'weightedAverage', - aggregatedCell: (info) => `${info.getValue().toFixed(1)}%`, - }), - ]), - data, -}) +const options = { groupedColumnMode: 'reorder' as const } +const mode = cell.getIsGrouped() + ? 'grouped' + : cell.getIsAggregated() + ? 'aggregated' + : cell.getIsPlaceholder() + ? 'placeholder' + : 'value' ``` -### Override grouping key with `getGroupingValue` - -```ts -columnHelper.accessor('firstName', { - // group by full name, not just firstName - getGroupingValue: (row) => `${row.firstName} ${row.lastName}`, -}) -``` - -### Control grouped-column placement - -```ts -const table = constructTable({ - features, - columns, - data, - groupedColumnMode: 'reorder', // default — grouped columns lead - // groupedColumnMode: 'remove', // hide grouped columns from visible flow - // groupedColumnMode: false, // keep columnOrder intact -}) -``` +Render these cell modes deliberately. ## Common Mistakes -### [HIGH] Adding `columnGroupingFeature` without `rowExpandingFeature` - -Wrong: - -```ts -// grouped rows show aggregates but can't be expanded -const features = tableFeatures({ - columnGroupingFeature, - groupedRowModel: createGroupedRowModel(), - aggregationFns, -}) -const table = useTable({ features, columns, data }) -// row.getToggleExpandedHandler() → TS error or undefined -``` - -Correct: - -```ts -import { - aggregationFns, - columnGroupingFeature, - createExpandedRowModel, - createGroupedRowModel, - rowExpandingFeature, -} from '@tanstack/react-table' - -const features = tableFeatures({ - columnGroupingFeature, - rowExpandingFeature, - groupedRowModel: createGroupedRowModel(), - expandedRowModel: createExpandedRowModel(), - aggregationFns, -}) - -const table = useTable({ features, columns, data }) - -// In cell renderer: -{cell.getIsGrouped() && ( - -)} -``` - -Without `rowExpandingFeature`, `row.getToggleExpandedHandler` doesn't exist — grouped rows stay collapsed forever. - -Source: examples/react/grouping/src/main.tsx - -### [HIGH] Customizing aggregationFns via a v8-style `tableOptions.aggregationFns` option +### [HIGH] Registering processing without grouping -Wrong: +Wrong: `tableFeatures({ groupedRowModel: createGroupedRowModel() })` -```ts -const table = useTable({ - features: tableFeatures({ - columnGroupingFeature, - groupedRowModel: createGroupedRowModel(), - aggregationFns, - }), - columns, data, - // @ts-ignore - this property doesn't exist on v9 TableOptions - aggregationFns: { - myCustom: (id, leaf, child) => /* ... */, - }, -}) -``` - -Correct: - -```ts -import { aggregationFns, createGroupedRowModel } from '@tanstack/react-table' - -const features = tableFeatures({ - columnGroupingFeature, - rowExpandingFeature, - groupedRowModel: createGroupedRowModel(), - expandedRowModel: createExpandedRowModel(), - aggregationFns: { - ...aggregationFns, - myCustomAggregation: (columnId, leafRows, childRows) => { - return /* aggregated value */ - }, - }, -}) - -const table = useTable({ features, columns, data }) - -// Then on a column: -columnHelper.accessor('sales', { aggregationFn: 'myCustomAggregation' }) -``` - -In v9, the aggregation registry is the `aggregationFns` slot on `features`. There is no top-level `tableOptions.aggregationFns`. - -Source: packages/table-core/src/features/column-grouping/createGroupedRowModel.ts - -### [MEDIUM] Confusing the `aggregationFn` signature with filter/sort signatures - -Wrong: - -```ts -// wrong arg names — first arg is columnId, not row -aggregationFn: (rowA, rowB, columnId) => /* ... */ - -// or: averaging via childRows includes already-aggregated sub-group sums -aggregationFn: (id, leaf, child) => child.reduce((a, r) => a + r.getValue(id), 0) / child.length -``` - -Correct: - -```ts -// (columnId, leafRows, childRows) -// leafRows = all descendant non-grouped rows -// childRows = immediate children (may be sub-aggregates) - -// For pure leaf averages, use leafRows: -const aggregationFn_mean: AggregationFn = (columnId, leafRows) => { - let count = 0, - sum = 0 - leafRows.forEach((row) => { - const value = row.getValue(columnId) - if (typeof value === 'number') { - count++ - sum += value - } - }) - return count ? sum / count : undefined -} - -// For nestable sums (reuse sub-aggregates), use childRows: -const aggregationFn_sum: AggregationFn = ( - columnId, - _leafRows, - childRows, -) => { - return childRows.reduce((acc, next) => { - const v = next.getValue(columnId) - return acc + (typeof v === 'number' ? v : 0) - }, 0) -} -``` +Correct: `tableFeatures({ columnGroupingFeature, groupedRowModel: createGroupedRowModel() })` -Built-in `mean`, `median`, `unique`, `uniqueCount`, `count` use `leafRows`. `sum`, `min`, `max`, `extent` use `childRows`. +The grouped slot and aggregation registry require `columnGroupingFeature`. -Source: packages/table-core/src/fns/aggregationFns.ts +Source: `packages/table-core/src/types/TableFeatures.ts#FeatureSlotPrereqs` -### [MEDIUM] Expecting grouped columns to keep their original position +### [HIGH] Treating rendered rows as underlying records -Wrong: - -```ts -const table = useTable({ - features, - columns, - data, - initialState: { - columnOrder: ['firstName', 'lastName', 'age', 'status'], // explicit - grouping: ['status'], - }, - // status jumps to position 0 — columnOrder is "overridden" -}) -``` +Wrong: `const leafCount = table.getRowModel().flatRows.filter(row => !row.subRows.length).length` Correct: ```ts -const table = useTable({ - features, - columns, - data, - initialState: { - columnOrder: ['firstName', 'lastName', 'age', 'status'], - grouping: ['status'], - }, - groupedColumnMode: false, // keep columnOrder intact -}) - -// Or hide grouped columns entirely: -// groupedColumnMode: 'remove' +const countLeafRows = (rows: typeof groupRow.subRows): number => + rows.reduce( + (count, row) => + count + (row.subRows.length ? countLeafRows(row.subRows) : 1), + 0, + ) +const leafCount = countLeafRows(groupRow.subRows) ``` -`columnGroupingFeature.getDefaultTableOptions` sets `groupedColumnMode: 'reorder'`, which moves grouped columns to the start. - -Source: packages/table-core/src/features/column-ordering/columnOrderingFeature.utils.ts - -### [LOW] Calling `getSelectedRowModel()` on a grouped table expecting grouped rows +Count descendants from one group row rather than the flattened render model, +where group rows and expanded rows may both appear. The current +`table.getRowModel().rows` is render order, while original dataset counts come +from the data owner. -Wrong: +Source: `docs/framework/react/guide/grouping.md` -```ts -// returns selection from the CORE model, not the grouped projection -const selectedRows = table.getSelectedRowModel().rows -// Doesn't reflect grouping — leaf rows only -``` - -Correct: - -```ts -table.getSelectedRowModel() // selection from raw data -table.getFilteredSelectedRowModel() // selection within current filters -table.getGroupedSelectedRowModel() // selection within current groups -``` +### [HIGH] Rendering every cell identically -Three distinct APIs. Pick the model matching the question being asked. +Wrong: `render(cell.getValue())` -Source: packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts +Correct: `render(cell.getIsPlaceholder() ? null : cell.getValue())` -### [CRITICAL] Reimplementing aggregation manually - -Wrong: - -```ts -// Hand-rolled groupBy + reduce, bypassing the table -const grouped = useMemo(() => { - const map = new Map() - data.forEach((row) => { - const key = row.status - if (!map.has(key)) map.set(key, []) - map.get(key)!.push(row) - }) - return Array.from(map.entries()).map(([k, rows]) => ({ - status: k, - avgAge: rows.reduce((s, r) => s + r.age, 0) / rows.length, - })) -}, [data]) -``` - -Correct: - -```ts -const features = tableFeatures({ - columnGroupingFeature, - rowExpandingFeature, - groupedRowModel: createGroupedRowModel(), - expandedRowModel: createExpandedRowModel(), - aggregationFns, -}) -const table = useTable({ - features, - columns: columnHelper.columns([ - columnHelper.accessor('status', { enableGrouping: true }), - columnHelper.accessor('age', { aggregationFn: 'mean' }), - ]), - data, -}) -table.setGrouping(['status']) -``` +Placeholder and aggregated cells do not represent ordinary leaf values. -Source: maintainer interview (Phase 4, 2026-05-17) +Source: `examples/react/grouping/src/main.tsx` -## See also +## API Discovery -- `tanstack-table/row-expanding` — required pairing for grouped drill-down -- `tanstack-table/customizing-feature-behavior` — `aggregationFn` authoring -- `tanstack-table/row-selection` — `getGroupedSelectedRowModel` distinction +Inspect `node_modules/@tanstack/table-core/src/features/column-grouping/` and `src/fns/aggregationFns.ts`. diff --git a/packages/table-core/skills/migrate-v8-to-v9/SKILL.md b/packages/table-core/skills/migrate-v8-to-v9/SKILL.md index 9bc02b9e47..496bb6a2cb 100644 --- a/packages/table-core/skills/migrate-v8-to-v9/SKILL.md +++ b/packages/table-core/skills/migrate-v8-to-v9/SKILL.md @@ -1,492 +1,332 @@ --- name: migrate-v8-to-v9 description: > - Mechanical breaking-change migration from TanStack Table v8 to v9 at the - `@tanstack/table-core` level. Covers hook/entry rename - (`useReactTable`/`createSolidTable`/… → `useTable`/`injectTable`/`createTable`/ - `constructTable`), the new required `features` option (which now carries row - model factories and fn registries — `rowModels` option is removed), - `createColumnHelper()` → `createColumnHelper()`, - row-model factory rename (`getCoreRowModel()` → automatic; `getSortedRowModel()` - → `createSortedRowModel()` as a slot on `tableFeatures({...})` with `sortFns` also - a slot; same for filtered/paginated/grouped/expanded/faceted), `table.getState()` → `table.store.state` / `table.atoms..get()`, - sorting renames (`sortingFn` → `sortFn`, etc.), `enablePinning` split, column - sizing/resizing split, underscore-prefixed APIs becoming public, `RowData` - type tightening, TFeatures-first generics, the `useLegacyTable` React escape - hatch (deprecated, removed in v10), and the `stockFeatures` v8-style "everything - on" registry. -type: lifecycle -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - setup - - state-management - - column-definitions + Perform a complete TanStack Table v8-to-v9 migration audit: feature registration, row-model and function-registry slots, state/store changes, prototype methods, column pinning and resizing renames, sorting and selection semantics, removed internals, helpers, meta typing, and generic changes. Load this shared inventory before the installed framework adapter's migration skill. +metadata: + type: lifecycle + library: '@tanstack/table-core' + library_version: '9.0.0-beta.38' +requires: ['core', 'table-features', 'typescript'] sources: - - TanStack/table:docs/framework/table-core/guide/migrating.md - - TanStack/table:docs/framework/react/guide/use-legacy-table.md - - TanStack/table:packages/react-table/src/legacy.ts + - 'TanStack/table:docs/framework/react/guide/migrating.md' + - 'TanStack/table:docs/framework/preact/guide/migrating.md' + - 'TanStack/table:docs/framework/solid/guide/migrating.md' + - 'TanStack/table:docs/framework/svelte/guide/migrating.md' + - 'TanStack/table:docs/framework/vue/guide/migrating.md' + - 'TanStack/table:docs/framework/angular/guide/migrating.md' + - 'TanStack/table:docs/framework/lit/guide/migrating.md' + - 'TanStack/table:packages/table-core/src/index.ts' + - 'TanStack/table:packages/table-core/src/types/TableFeatures.ts' + - 'TanStack/table:packages/table-core/src/features/column-pinning/columnPinningFeature.types.ts' + - 'TanStack/table:packages/table-core/src/features/column-resizing/columnResizingFeature.types.ts' + - 'TanStack/table:packages/react-table/src/legacy.ts' --- -## Setup +Apply this complete shared inventory before loading the installed adapter's `migrate-v8-to-v9` skill. The adapter skill owns hook/controller construction, reactive inputs, rendering helpers, and framework subscription APIs. -v9 is a substantial reshape, not a tweak. The breaking changes group into: +## Migration strategy -1. **Hook/entry rename** per adapter. -2. **`features` is required** — features are tree-shaken. Row model factories and fn registries (`sortFns`, `filterFns`, `aggregationFns`) now live as slots on the `features` object; the `rowModels` option is gone. -3. **Column helper generic order** — `` not ``. -4. **Row-model factories** moved out of root options and into `tableFeatures({...})` as named slots; factory args for `*Fns` are replaced by dedicated fn-registry slots on `features`. -5. **State surface renamed** — `table.getState()` → `table.store.state` / `table.atoms..get()` / `table.state` (selector). -6. **Sorting names**: `sortingFn` → `sortFn`, `sortingFns` → `sortFns`, `getSortingFn()` → `getSortFn()`, type `SortingFn` → `SortFn`. -7. **`enablePinning` split** into `enableColumnPinning` + `enableRowPinning` (table-level); per-column `enablePinning` stays. -8. **Column resizing split out** of column sizing. `columnSizingInfo` state → `columnResizing`. `onColumnSizingInfoChange` → `onColumnResizingChange`. -9. **Underscore-prefixed APIs are public now** — drop the `_` prefix (`row._getAllCellsByColumnId()` → `row.getAllCellsByColumnId()`, `table._getFacetedRowModel()` → public, etc.). -10. **Generics now lead with `TFeatures`** — `Column`, `Row`, `ColumnMeta`. -11. **`RowData` tightened** from `unknown | object | any[]` to `Record | Array`. -12. **`data` and `columns` are readonly** in v9 — flow changes through state, don't mutate. +1. Make the v8 table pass its existing tests before changing it. +2. Migrate construction and features while preserving behavior. +3. Let TypeScript expose missing features and stale names. +4. Migrate state ownership and rendering through the adapter skill. +5. Test every enabled client/server row-model stage and interaction. +6. Replace temporary `stockFeatures` usage with explicit features when practical. -For React projects that cannot migrate every table at once, `useLegacyTable` from `@tanstack/react-table/legacy` accepts the v8 shape on top of the v9 engine. Deprecated, ships every feature, no `table.Subscribe`. Removed in v10. +Treat `useLegacyTable` as a deprecated, React-only emergency bridge. It bundles every feature, can exceed the v8 bundle, and must not become the target architecture. Import it only from `@tanstack/react-table/legacy` when an existing incremental migration requires it. -## Core Patterns - -### Full v9 equivalent for the most common v8 shape +## Minimal v9 shape ```ts -// === v9 (correct) === import { - useTable, - tableFeatures, - rowSortingFeature, - rowPaginationFeature, - columnFilteringFeature, - columnSizingFeature, - columnResizingFeature, - createColumnHelper, - createSortedRowModel, createFilteredRowModel, - createPaginatedRowModel, - sortFns, - filterFns, -} from '@tanstack/react-table' -import type { ColumnDef } from '@tanstack/react-table' - -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, + createSortedRowModel, columnFilteringFeature, - columnSizingFeature, - columnResizingFeature, // explicit — formerly part of ColumnSizing - // Row model factories and fn registries are slots on features, not rowModels. - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, filterFns, -}) - -const columnHelper = createColumnHelper() - -const columns: ColumnDef[] = columnHelper.columns([ - columnHelper.accessor('name', { - header: 'Name', - sortFn: 'alphanumeric', // renamed from sortingFn - }), -]) - -const table = useTable({ - features, - columns, - data, - enableColumnPinning: true, // split from enablePinning - enableRowPinning: true, -}) - -// State reads -const allState = table.store.state // full snapshot -const sorting = table.atoms.sorting.get() // per-slice atom -const cells = row.getAllCellsByColumnId() // no underscore - -// Rendering -// -// -``` - -### v8 muscle-memory anti-shape - -```ts -// === v8 muscle memory — every line is broken in v9. === -import { - useReactTable, // (1) renamed → useTable - getCoreRowModel, // (2) no longer a root option - getFilteredRowModel, // move to rowModels as factories - getSortedRowModel, // createSortedRowModel(sortFns) etc. - getPaginationRowModel, - createColumnHelper, // (3) needs now - sortingFns, // (4) renamed → sortFns - filterFns, - flexRender, // still exists, prefer table.FlexRender -} from '@tanstack/react-table' -import type { ColumnDef, Row } from '@tanstack/react-table' - -const columnHelper = createColumnHelper() // wrong arity - -const columns: ColumnDef[] = [ - // (5) ColumnDef now - { accessorKey: 'name', header: 'Name', sortingFn: 'alphanumeric' }, // (6) renamed → sortFn -] - -const table = useReactTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), // (2) move into rowModels - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - filterFns, // (7) no longer a root option - sortingFns, // (4)+(7) - enablePinning: true, // (8) split → enableColumnPinning / enableRowPinning - onColumnSizingInfoChange: setInfo, // (9) renamed → onColumnResizingChange -}) - -const all = table.getState() // (10) → table.store.state -const cells = row._getAllCellsByColumnId() // underscore removed -``` - -### Transitional `useLegacyTable` (React only) - -```ts -import { - useLegacyTable, - getCoreRowModel, - legacyCreateColumnHelper, -} from '@tanstack/react-table/legacy' - -const legacyHelper = legacyCreateColumnHelper() -const legacyTable = useLegacyTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), -}) -``` - -This accepts the v8 shape on top of the v9 engine. Deprecated, ships every feature, no `table.Subscribe`, no atoms. Removed in v10. Use to unblock incremental migration only — not as a long-term API. Angular has no `useLegacyTable` equivalent; Angular projects must migrate directly. - -## Common Mistakes - -### [CRITICAL] Hallucinating react-table v7 / pre-v9 API names - -Wrong: - -```ts -// v7 -import { useTable, useSortBy } from 'react-table' -const table = useTable({ columns, data }, useSortBy) - -// v8 -import { useReactTable, getCoreRowModel } from '@tanstack/react-table' -const table = useReactTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), -}) -``` - -Correct: - -```ts -import { - useTable, - tableFeatures, rowSortingFeature, - createSortedRowModel, sortFns, -} from '@tanstack/react-table' + tableFeatures, +} from '@tanstack/table-core' -const features = tableFeatures({ +export const features = tableFeatures({ + columnFilteringFeature, rowSortingFeature, + filteredRowModel: createFilteredRowModel(), sortedRowModel: createSortedRowModel(), + filterFns, sortFns, }) -const table = useTable({ features, columns, data }) ``` -Every major release of TanStack Table has been a substantial upgrade. Agents trained on v7 or v8 will confidently emit shapes that no longer exist. This is the #2 AI failure (after reimplementing built-ins). +Pass `features` to the adapter's v9 table constructor. Define it statically outside render/setup work when possible. -Source: maintainer interview (Phase 4, 2026-05-17) +## Complete shared breaking-change inventory -### [CRITICAL] Importing pre-bundled `getCoreRowModel` / `getSortedRowModel` etc. +### 1. Register every non-core feature explicitly -Wrong: +V8 bundled all stock features. V9 exposes an API only when its feature is present in `tableFeatures({...})`. -```ts -// v8 pattern — won't drive v9 row models -const table = useTable({ - features, - data, - columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), -}) -``` +| Capability | V9 feature | +| --------------------------- | ------------------------- | +| Column faceting | `columnFacetingFeature` | +| Column filtering | `columnFilteringFeature` | +| Grouping and aggregation | `columnGroupingFeature` | +| Column ordering | `columnOrderingFeature` | +| Column pinning | `columnPinningFeature` | +| Interactive column resizing | `columnResizingFeature` | +| Column sizes and offsets | `columnSizingFeature` | +| Column visibility | `columnVisibilityFeature` | +| Global filtering | `globalFilteringFeature` | +| Row expansion | `rowExpandingFeature` | +| Pagination | `rowPaginationFeature` | +| Row pinning | `rowPinningFeature` | +| Row selection | `rowSelectionFeature` | +| Sorting | `rowSortingFeature` | -Correct: +The core row model and core table/row/column/header/cell behavior are automatic. `stockFeatures` restores a v8-like all-features surface, but use it as an audit shortcut rather than the default production recommendation. -```ts -import { - useTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, -} from '@tanstack/react-table' +Honor feature prerequisites in the same `tableFeatures` call: -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -``` +- `columnResizingFeature` requires `columnSizingFeature`. +- `globalFilteringFeature` requires `columnFilteringFeature`. +- Every row-model or function-registry slot requires its associated feature. +- Put prerequisite feature properties before dependent slots so inference and diagnostics remain clear. -In v9, row model factories are slots on `features`: `filteredRowModel: createFilteredRowModel()`, `sortedRowModel: createSortedRowModel()`, `groupedRowModel: createGroupedRowModel()`. The fn registries move too: `filterFns`, `sortFns`, `aggregationFns` are named slots on `features`. Core is automatic. +### 2. Move row models into feature slots and rename factories -Source: PR #6234 (atoms refactor); packages/table-core/src/index.ts +V8 `get*RowModel()` table options and the earlier-v9-beta `rowModels` object are gone. V9 `create*RowModel()` factories take no registry arguments and are registered as named feature slots. -### [CRITICAL] `createColumnHelper()` (v8 arity) +| V8 table option | V9 `tableFeatures` slot | V9 factory | +| -------------------------------------------------- | -------------------------------------- | ---------------------------------------------- | +| `getCoreRowModel: getCoreRowModel()` | automatic; omit for the built-in model | built-in `createCoreRowModel()` is the default | +| `getFilteredRowModel: getFilteredRowModel()` | `filteredRowModel` | `createFilteredRowModel()` | +| `getSortedRowModel: getSortedRowModel()` | `sortedRowModel` | `createSortedRowModel()` | +| `getPaginationRowModel: getPaginationRowModel()` | `paginatedRowModel` | `createPaginatedRowModel()` | +| `getExpandedRowModel: getExpandedRowModel()` | `expandedRowModel` | `createExpandedRowModel()` | +| `getGroupedRowModel: getGroupedRowModel()` | `groupedRowModel` | `createGroupedRowModel()` | +| `getFacetedRowModel: getFacetedRowModel()` | `facetedRowModel` | `createFacetedRowModel()` | +| `getFacetedMinMaxValues: getFacetedMinMaxValues()` | `facetedMinMaxValues` | `createFacetedMinMaxValues()` | +| `getFacetedUniqueValues: getFacetedUniqueValues()` | `facetedUniqueValues` | `createFacetedUniqueValues()` | -Wrong: +For a custom core model, use the `coreRowModel` slot rather than restoring the v8 table option. -```ts -const columnHelper = createColumnHelper() -``` +Move registries from table options or factory arguments into these feature slots: -Correct: +| V8 | V9 | +| ---------------- | ---------------- | +| `sortingFns` | `sortFns` | +| `filterFns` | `filterFns` | +| `aggregationFns` | `aggregationFns` | -```ts -const features = tableFeatures({ rowSortingFeature }) -const columnHelper = createColumnHelper() -``` +Register built-ins only when needed, or spread them with custom functions. A slot's keys become the valid string names in column definitions. -v9 requires ``. `typeof features` is the standard idiom — declare features once at module scope and reuse the type. +### 3. Migrate state reads and whole-state observation -Source: packages/table-core/src/helpers/columnHelper.ts; docs/framework/react/guide/migrating.md +`table.getState()` and the top-level `onStateChange` option are removed. Individual `on[Slice]Change` callbacks remain. -### [HIGH] Reading state via `table.getState()` +| V8 need | V9 shared surface | +| ------------------------------- | --------------------------------------------------- | +| Full current snapshot | `table.store.state` | +| One current slice | `table.atoms..get()` | +| Adapter-selected reactive state | `table.state` where the adapter exposes it | +| Observe all changes | `table.store.subscribe(...)` | +| Control one slice | `state.` plus `onChange` | +| Externally own one slice | `atoms.` with a writable TanStack Store atom | +| Internal state | omit both `state.` and `atoms.` | -Wrong: +Load the adapter `table-state` skill before choosing reactive reads; adapters intentionally differ. When both an external atom and `state` provide a slice, the atom wins. Table writes go directly to that atom, and `table.reset()` does not reset externally owned atoms. -```ts -const all = table.getState() -``` +### 4. Keep instance methods bound -Correct: +Row, cell, column, header, and related object methods moved to shared prototypes. Destructuring, passing a bare callback, spreading, `Object.keys`, and `JSON.stringify` no longer preserve or reveal those methods. ```ts -const all = table.store.state // flat snapshot, no subscription -const sorting = table.atoms.sorting.get() // per-slice -const selected = table.state // typed selector output (framework adapters) -``` - -`table.getState()` was removed. There are three reads now, picked by what you need. +// v8 code that breaks +const { getValue } = row +rows.map(row.getVisibleCells) -Source: docs/framework/table-core/guide/migrating.md - -### [HIGH] Sorting renames missed - -Wrong: - -```ts -{ accessorKey: 'age', sortingFn: 'alphanumeric' } // v8 name -useTable({ sortingFns: { ... } }) // v8 option -column.getSortingFn() // v8 method +// v9 +const value = row.getValue('name') +rows.map((row) => row.getVisibleCells()) ``` -Correct: +Audit all methods extracted from rows, cells, columns, headers, and header groups. Table-instance methods are not subject to this specific migration rule. -```ts -columnHelper.accessor('age', { sortFn: 'alphanumeric' }) -// sortFns is a slot on features: tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns }) -column.getSortFn() -``` +### 5. Replace physical column pinning with logical pinning -v9 renamed every sorting API: `sortingFn` → `sortFn`, `sortingFns` → `sortFns` (now a features slot, not a factory arg), type `SortingFn` → `SortFn`, `getSortingFn()` → `getSortFn()`. TypeScript surfaces these but agents try v8 names first. +V9 beta.38 has no `left`/`right` aliases. Replace all state keys, return-value comparisons, arguments, and API families: -Source: packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts +| V8 | V9 | +| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | +| `columnPinning.left` / `.right` | `.start` / `.end` | +| `column.pin('left' \| 'right')` | `column.pin('start' \| 'end')` | +| `column.getIsPinned() === 'left' \| 'right'` | compare with `'start' \| 'end'` | +| `row.getLeftVisibleCells()` / `getRightVisibleCells()` | `getStartVisibleCells()` / `getEndVisibleCells()` | +| `table.getLeftHeaderGroups()` / `getRightHeaderGroups()` | `getStartHeaderGroups()` / `getEndHeaderGroups()` | +| `table.getLeftFooterGroups()` / `getRightFooterGroups()` | `getStartFooterGroups()` / `getEndFooterGroups()` | +| `table.getLeftFlatHeaders()` / `getRightFlatHeaders()` | `getStartFlatHeaders()` / `getEndFlatHeaders()` | +| `table.getLeftLeafHeaders()` / `getRightLeafHeaders()` | `getStartLeafHeaders()` / `getEndLeafHeaders()` | +| `table.getLeftLeafColumns()` / `getRightLeafColumns()` | `getStartLeafColumns()` / `getEndLeafColumns()` | +| `table.getLeftVisibleLeafColumns()` / `getRightVisibleLeafColumns()` | `getStartVisibleLeafColumns()` / `getEndVisibleLeafColumns()` | +| `table.getLeftTotalSize()` / `getRightTotalSize()` | `getStartTotalSize()` / `getEndTotalSize()` | +| `'left' \| 'right'` passed to `getStart`, `getAfter`, `getIndex`, or pinned-region helpers | `'start' \| 'end'` | -### [HIGH] Using `enablePinning` at the table level +This names logical regions; it does not automatically apply DOM direction or sticky CSS. Use logical CSS such as `inset-inline-start`/`insetInlineStart` and `inset-inline-end`/`insetInlineEnd`. `columnResizeDirection` remains `'ltr' | 'rtl'`. -Wrong: +### 6. Split column sizing from resizing -```ts -const table = useTable({ - features: tableFeatures({ columnPinningFeature, rowPinningFeature }), - enablePinning: true, // ignored at table level in v9 -}) -``` +V8's combined sizing feature became two tree-shakeable features: -Correct: +- Register `columnSizingFeature` for sizes, offsets, and total-size APIs. +- Also register `columnResizingFeature` for drag handles and transient interaction state. +- `columnResizingFeature` cannot stand alone. -```ts -const table = useTable({ - features: tableFeatures({ columnPinningFeature, rowPinningFeature }), - enableColumnPinning: true, - enableRowPinning: true, -}) +| V8 | V9 | +| -------------------------- | ------------------------ | +| `columnSizingInfo` state | `columnResizing` state | +| `setColumnSizingInfo(...)` | `setColumnResizing(...)` | +| `onColumnSizingInfoChange` | `onColumnResizingChange` | -// Per-column opt-out is still `enablePinning`: -columnHelper.accessor('id', { enablePinning: false }) -``` +The current source spelling is `setColumnResizing` with an uppercase `C`. -v9 split `enablePinning` (table-level) into `enableColumnPinning` + `enableRowPinning`. The bare name now refers ONLY to per-column opt-out. +### 7. Rename sorting APIs -Source: packages/table-core/src/features/column-pinning/columnPinningFeature.types.ts +| V8 | V9 | +| --------------------------- | ------------------------ | +| column-def `sortingFn` | `sortFn` | +| `column.getSortingFn()` | `column.getSortFn()` | +| `column.getAutoSortingFn()` | `column.getAutoSortFn()` | +| `SortingFn` | `SortFn` | +| `SortingFns` | `SortFns` | +| built-in `sortingFns` | `sortFns` | -### [HIGH] Treating column resizing as part of column sizing +Also move the registry to `tableFeatures`, as described above. -Wrong: +### 8. Split the table-level pinning switch -```ts -// v8 — ColumnSizing implied resizing too -const table = useTable({ - features: tableFeatures({ columnSizingFeature }), - onColumnSizingInfoChange: setInfo, // v8 name -}) -``` +Replace the v8 table option `enablePinning` with `enableColumnPinning` and/or `enableRowPinning`. Do not mechanically rename a column definition's `enablePinning`: that column-level option still exists. -Correct: +### 9. Remove internal APIs and use public surfaces -```ts -const table = useTable({ - features: tableFeatures({ - columnSizingFeature, - columnResizingFeature, // explicit in v9 - }), - onColumnResizingChange: setResizing, // renamed - // state key columnSizingInfo → columnResizing -}) -``` +All underscore-prefixed internals are unsupported and removed. Known migration points include: -v9 split them: `columnSizingFeature` for fixed widths, `columnResizingFeature` for drag-to-resize. State key `columnSizingInfo` → `columnResizing`; option `onColumnSizingInfoChange` → `onColumnResizingChange`. +| Removed v8 internal | V9 public direction | +| --------------------------------- | ------------------------------------------------------------- | +| `row._getAllCellsByColumnId()` | `row.getAllCellsByColumnId()` | +| `table._getPinnedRows()` | `table.getTopRows()`, `getCenterRows()`, or `getBottomRows()` | +| `table._getFacetedRowModel()` | public faceting APIs on the relevant column/table | +| `table._getFacetedMinMaxValues()` | `getFacetedMinMaxValues()` | +| `table._getFacetedUniqueValues()` | `getFacetedUniqueValues()` | -Source: docs/framework/table-core/guide/migrating.md +For any other `_` API, do not guess. Inspect the installed v9 package source for the public replacement or redesign the integration. -### [MEDIUM] Calling underscore-prefixed APIs +### 10. Update row-selection predicates -Wrong: +`getIsSomeRowsSelected()` and `getIsSomePageRowsSelected()` now mean **at least one**, including the all-selected case. They no longer mean “some but not all.” Build an indeterminate checkbox with both predicates: ```ts -row._getAllCellsByColumnId() -table._getFacetedRowModel() -table._getFacetedMinMaxValues() -table._getFacetedUniqueValues() -table._getPinnedRows() +const indeterminate = + table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() ``` -Correct: +For a page checkbox, use `table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()`. -```ts -row.getAllCellsByColumnId() -table.getFacetedRowModel() -table.getFacetedMinMaxValues() -table.getFacetedUniqueValues() -table.getPinnedRows() -``` +### 11. Update TypeScript feature generics and helpers + +Most core types add `TFeatures` before their data/value parameters: -All became public — drop the underscore. +| V8 | V9 | +| ----------------------------- | ---------------------------------------- | +| `Column` | `Column` | +| `ColumnDef` | `ColumnDef` | +| `Table` | `Table` | +| `Row` | `Row` | +| `Cell` | `Cell` | +| `createColumnHelper()` | `createColumnHelper()` | -Source: docs/framework/table-core/guide/migrating.md +Prefer inference. Use `typeof features` only where an explicit type boundary is necessary; use `StockFeatures` when that is genuinely the selected feature set. Wrap column arrays with `columnHelper.columns([...])` to preserve individual and nested `TValue` inference. -### [MEDIUM] Module augmentation with v8 generic arity +`RowData` is now `Record | Array`, not `unknown`. Wrap primitive records in an object or array shape. -Wrong: +Global `TableMeta`/`ColumnMeta` declaration merging can remain, but add `TFeatures` as the first generic. Prefer per-table type-only slots where isolation helps: ```ts -declare module '@tanstack/react-table' { - interface ColumnMeta { - customProp: string - } -} +const features = tableFeatures({ + columnFilteringFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), + filterMeta: metaHelper(), +}) ``` -Correct: +Replace global `FilterFns`, `SortFns`, and `AggregationFns` augmentation with the matching registry slots; their object keys supply the string-literal names. Replace `FilterMeta` augmentation with the `filterMeta` slot unless global behavior is intentional. -```ts -declare module '@tanstack/react-table' { - interface ColumnMeta { - customProp: string - } -} -``` +### 12. Migrate adapter construction and rendering separately -v9 added `TFeatures` as the first generic. Module augmentation silently widens types if arity is wrong. +The following are breaking but not shared core mappings: adapter hook/factory/controller names, Svelte 5 requirements, reactive getter rules, state-selection components/helpers, and `FlexRender` syntax. Load the installed adapter's migration skill and follow its exact source. Do not apply React names to Preact, Solid, Svelte, Vue, Angular, or Lit. -Source: docs/framework/table-core/guide/migrating.md +## Optional v9 adoption after parity -### [MEDIUM] Mutating `data` or `columns` in place +Do not confuse new capabilities with required breakages. After the table works, consider `tableOptions()` for reusable partial configuration, `createTableHook()` for app-level table conventions, per-slice atoms/subscriptions for narrower rendering, per-table meta slots, and explicit features for smaller bundles. -Wrong: +## Complete audit checklist -```ts -// v8 pattern, breaks at TS layer in v9 -const data: Person[] = [] -function addRow(row: Person) { - data.push(row) - rerender() -} -``` +- [ ] Load the installed framework adapter's migration and table-state skills. +- [ ] Replace the v8 adapter constructor/hook/controller with its v9 entrypoint. +- [ ] Add a stable `features` object to every table. +- [ ] Inventory every feature API used by table, row, column, cell, and header code; register all 14 required stock features. +- [ ] Use `stockFeatures` only as a temporary parity aid and record an explicit-feature follow-up. +- [ ] Remove `getCoreRowModel()` unless supplying a deliberate custom `coreRowModel` slot. +- [ ] Move all remaining `get*RowModel()` options or earlier-beta `rowModels` entries to `create*RowModel()` feature slots. +- [ ] Register each dependent feature before its row-model slot. +- [ ] Move `filterFns`, `sortingFns`/`sortFns`, and `aggregationFns` into feature slots; pass no registries to factories. +- [ ] Register `columnFilteringFeature` before global filtering and filter/facet dependencies. +- [ ] Register `columnSizingFeature` before `columnResizingFeature`. +- [ ] Replace `table.getState()` and top-level `onStateChange` according to the adapter state guide. +- [ ] Verify every controlled slice has an update path; verify externally owned atoms are reset by their owner. +- [ ] Audit destructured, spread, serialized, or bare-callback instance methods. +- [ ] Replace every column-pinning `left`/`right` key, argument, comparison, method family, and sticky CSS declaration with logical start/end equivalents. +- [ ] Split table-level `enablePinning`; preserve column-def `enablePinning` where intended. +- [ ] Split sizing/resizing features and rename the resizing state, setter, and callback. +- [ ] Replace every `sortingFn`/`SortingFn`/`sortingFns` spelling with its v9 `sort*` spelling. +- [ ] Remove every consumed underscore-prefixed internal API. +- [ ] Recheck indeterminate selection logic against the new “at least one” semantics. +- [ ] Add `TFeatures` to unavoidable explicit core types, helpers, and retained meta augmentation; otherwise restore inference. +- [ ] Replace function-registry and filter-meta declaration merging with per-table slots where appropriate. +- [ ] Ensure every row is a record or array under the stricter `RowData` constraint. +- [ ] Migrate adapter-specific rendering, reactive data inputs, and subscription primitives. +- [ ] Type-check without `any`/casts added merely to suppress migration failures. +- [ ] Test sorting, filtering, faceting, grouping, expansion, pagination, selection, ordering, pinning, sizing, and resizing—only where enabled. +- [ ] Test client/server ownership for every row-model pipeline stage and ensure manual modes receive already-processed data. +- [ ] Test LTR and RTL layouts when column pinning or resizing is enabled. +- [ ] Remove `useLegacyTable` after the incremental migration step that required it. -Correct: +## Common migration failures -```ts -const [data, setData] = useState([]) -function addRow(row: Person) { - setData((prev) => [...prev, row]) -} -``` +### [CRITICAL] Silencing a missing API instead of registering its feature -PR #6183 makes `data` and `columns` readonly to force changes through state. +If `table.nextPage`, `column.toggleSorting`, or a state slice disappears, add the associated feature. Do not cast the table to a broader type. -Source: PR #6183 +### [CRITICAL] Mixing v8, early-v9-beta, and beta.38 configuration -### [MEDIUM] Reaching for `useLegacyTable` in new code +Do not combine v8 `get*RowModel` options, an earlier beta's `rowModels` object, or physical pinning names with the current `tableFeatures` slots. -Wrong: +### [HIGH] Treating `stockFeatures` or `useLegacyTable` as the finished migration -```ts -// Long-term use of the legacy shim -import { useLegacyTable, getCoreRowModel } from '@tanstack/react-table/legacy' -const table = useLegacyTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), -}) -``` +Both obscure missing feature decisions; `useLegacyTable` is deprecated and React-only. Reach behavior parity, then complete the explicit v9 setup. -Correct: +### [HIGH] Copying one adapter's state or rendering API into another -```ts -// Migrate to native v9 shape -import { - useTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, -} from '@tanstack/react-table' -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -``` +Core concepts are shared, but reactive reads, constructors, and rendering helpers are not. Load the package-local adapter skills. -`useLegacyTable` is React-only, deprecated, bundles every feature, doesn't support `table.Subscribe`, and is removed in v10. It exists to unblock incremental migration — not as a long-term API. +## Installed-source API discovery -Source: packages/react-table/src/legacy.ts; docs/framework/react/guide/use-legacy-table.md +Use the installed version, not main-branch memory: -## See also +1. Inspect `node_modules/@tanstack/table-core/src/index.ts` for exports. +2. Inspect `src/types/TableFeatures.ts` for valid slots and prerequisites. +3. Inspect `src/features//*.types.ts` for current options, state, and APIs. +4. Inspect the installed adapter's `src/index.ts` and its migration skill for entrypoints and rendering. +5. Inspect `src/legacy.ts` only to remove an existing bridge, never to design new v9 code. -- `tanstack-table/setup` — what the v9-native shape looks like -- `tanstack-table/state-management` — `table.store.state` / `table.atoms` / `table.state` ownership -- `tanstack-table/column-definitions` — `createColumnHelper()` generic order +If package-manager layout prevents that exact path, resolve the installed package root first. Do not substitute APIs from a different v9 beta. diff --git a/packages/table-core/skills/pagination/SKILL.md b/packages/table-core/skills/pagination/SKILL.md index 656aecfd67..af470841ed 100644 --- a/packages/table-core/skills/pagination/SKILL.md +++ b/packages/table-core/skills/pagination/SKILL.md @@ -1,387 +1,78 @@ --- name: pagination description: > - Paginate rows in TanStack Table v9 with the `paginatedRowModel` stage. Covers - `rowPaginationFeature` + `createPaginatedRowModel()`, `state.pagination` - ({ pageIndex, pageSize }), `onPaginationChange`, `manualPagination`, - `rowCount` and `pageCount` for server-side, `autoResetPageIndex`, - `paginateExpandedRows`, navigation APIs (`nextPage`/`previousPage`/`firstPage`/ - `lastPage`/`setPageIndex`/`setPageSize`), `getCanNextPage` / `getCanPreviousPage`, - `getPageCount` / `getRowCount` / `getPageOptions`, and `getPrePaginatedRowModel` - for "total filtered" counts. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management + Paginate with rowPaginationFeature and paginatedRowModel or manualPagination. Load for pageIndex/pageSize state, rowCount/pageCount, unknown next-page limits, already-paginated server data, or autoResetPageIndex surprises. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features', 'client-vs-server'] sources: - - TanStack/table:docs/guide/pagination.md - - TanStack/table:packages/table-core/src/features/row-pagination/rowPaginationFeature.utils.ts - - TanStack/table:packages/table-core/src/features/row-pagination/createPaginatedRowModel.ts - - TanStack/table:examples/react/pagination/src/main.tsx + - 'TanStack/table:docs/framework/react/guide/pagination.md' + - 'TanStack/table:packages/table-core/src/features/row-pagination' + - 'TanStack/table:examples/react/pagination' --- -This skill builds on `tanstack-table/state-management`. Read it first for the atom model and `manual*` mode. +This skill builds on `core`, `table-features`, and `client-vs-server`. Choose client slicing or server pages, never both accidentally. ## Setup ```ts import { - tableFeatures, - rowPaginationFeature, createPaginatedRowModel, - constructTable, + rowPaginationFeature, + tableFeatures, } from '@tanstack/table-core' -import type { PaginationState } from '@tanstack/table-core' -const features = tableFeatures({ +export const features = tableFeatures({ rowPaginationFeature, paginatedRowModel: createPaginatedRowModel(), }) - -const table = constructTable({ - features, - columns, - data, - initialState: { - pagination: { pageIndex: 0, pageSize: 10 } satisfies PaginationState, - }, -}) - -table.nextPage() -table.setPageIndex(5) -table.setPageSize(25) -table.getCanNextPage() -table.getPageCount() -table.getRowCount() +export const initialState = { pagination: { pageIndex: 0, pageSize: 25 } } ``` ## Core Patterns -### Pagination toolbar - -```tsx -// From examples/react/pagination/src/main.tsx -
- - - - - - - Page {table.store.state.pagination.pageIndex + 1} of{' '} - {table.getPageCount().toLocaleString()} - - - - | Go to page: - { - const page = e.target.value ? Number(e.target.value) - 1 : 0 - table.setPageIndex(page) - }} - /> - - - -
-``` - -### Server-side pagination - -```tsx -const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10, -}) -const { data: dataQuery } = useQuery({ - queryKey: ['rows', pagination], - queryFn: () => fetchPage(pagination.pageIndex, pagination.pageSize), -}) - -const table = useTable({ - features: tableFeatures({ rowPaginationFeature }), - // no paginatedRowModel registered — server paginates - data: dataQuery?.rows ?? EMPTY, - columns, - manualPagination: true, - rowCount: dataQuery?.rowCount, // server-provided total - // OR: pageCount: dataQuery.pageCount - // OR: pageCount: -1 if unknown (next button stays enabled) - state: { pagination }, - onPaginationChange: setPagination, -}) -``` - -### Disable auto page reset when filters change - -```ts -const features = tableFeatures({ - rowPaginationFeature, - columnFilteringFeature, - paginatedRowModel: createPaginatedRowModel(), - filteredRowModel: createFilteredRowModel(), - filterFns, -}) -const table = constructTable({ - features, - columns, - data, - autoResetPageIndex: false, // keep current page while user types in a filter -}) -``` - -`autoResetPageIndex` defaults to `!manualPagination` — true client-side, false manual. - -### "Total filtered rows" display while paginated - ```ts -const totalFiltered = table.getPrePaginatedRowModel().rows.length -const onCurrentPage = table.getRowModel().rows.length +const serverOptions = { manualPagination: true, rowCount: response.total } ``` -`getPrePaginatedRowModel` is the row model AFTER filtering/sorting/grouping/expansion but BEFORE the page slice. +Pass one already-processed page and either `rowCount` or `pageCount`. ## Common Mistakes -### [HIGH] Setting `manualPagination: true` without supplying `rowCount` or `pageCount` - -Wrong: - -```tsx -// getPageCount() returns 1, next/prev buttons are disabled -const table = useTable({ - features: tableFeatures({ rowPaginationFeature }), - data, // only 10 rows for the current page - columns, - manualPagination: true, - state: { pagination }, - onPaginationChange: setPagination, - // missing: rowCount or pageCount -}) -``` - -Correct: +### [CRITICAL] Expecting manual mode to slice -```tsx -const table = useTable({ - features: tableFeatures({ rowPaginationFeature }), - data: dataQuery.rows, - columns, - manualPagination: true, - rowCount: dataQuery.rowCount, // server tells the table the total - // OR: pageCount: dataQuery.pageCount - // OR: pageCount: -1 if unknown - state: { pagination }, - onPaginationChange: setPagination, -}) -``` - -`getRowCount()` defaults to `getPrePaginatedRowModel().rows.length` — which in manual mode is only the current page. Without `rowCount`/`pageCount`, the table thinks there's one page total. +Wrong: `const options = { data: allRows, manualPagination: true }` -Source: packages/table-core/src/features/row-pagination/rowPaginationFeature.utils.ts +Correct: `const options = { data: requestedPageRows, manualPagination: true, rowCount: totalRows }` -### [MEDIUM] Putting `pagination` in BOTH `state` and `initialState` - -Wrong: - -```tsx -const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }) -const table = useTable({ - initialState: { pagination: { pageSize: 25 } }, // IGNORED - state: { pagination }, // wins (pageSize 10) - onPaginationChange: setPagination, -}) -``` +Manual pagination trusts the provided data as the current page. -Correct: +Source: `https://github.com/TanStack/table/issues/4917` -```tsx -const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }) -const table = useTable({ - features: tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), - }), - columns, - data, - state: { pagination }, - onPaginationChange: setPagination, -}) -``` +### [HIGH] Omitting the server total -`state` always overrides `initialState`. Seed the controlled state in `useState` or use `initialState` alone — never both. +Wrong: `const options = { data: pageRows, manualPagination: true }` -Source: docs/guide/pagination.md +Correct: `const options = { data: pageRows, manualPagination: true, rowCount: 1234 }` -### [MEDIUM] `autoResetPageIndex: false` without manual clamping leaves empty pages +Without a count, last-page and next-page availability cannot reflect the server dataset. -Wrong: +Source: `packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts` -```ts -// features already has paginatedRowModel + filteredRowModel registered -const table = useTable({ - features, - columns, - data, - autoResetPageIndex: false, // user on page 5, then filters down to 2 pages - // → page 5 is empty. No automatic clamp. -}) -``` +### [HIGH] Fighting automatic page resets -Correct: +Wrong: `onPaginationChange: setPagination` -```ts -// Either leave autoResetPageIndex default (true) for client-side, -// or clamp manually after a data-altering effect: -useEffect(() => { - const lastPage = Math.max(0, table.getPageCount() - 1) - if (table.atoms.pagination.get().pageIndex > lastPage) { - table.setPageIndex(lastPage) - } -}, [data, columnFilters]) -``` - -`setPageIndex` only clamps against `options.pageCount` (max safe int when unset). It doesn't clamp against the current row model. - -Source: packages/table-core/src/features/row-pagination/rowPaginationFeature.utils.ts - -### [LOW] Expecting `getRowCount()` to equal `data.length` under filtering/grouping/expansion - -Wrong: - -```ts -console.log(table.getRowCount()) // count after all transforms -console.log(data.length) // raw input count -// These diverge under filtering, grouping, or tree vs flat input. -``` - -Correct: - -```ts -table.getCoreRowModel().rows.length // raw row count (flat) -table.getPreFilteredRowModel().rows.length // before filtering -table.getFilteredRowModel().rows.length // after filtering -table.getRowCount() // pre-paginated count (or `rowCount` option in manual mode) -table.getRowModel().rows.length // current page only -``` - -`getRowCount` returns `options.rowCount ?? getPrePaginatedRowModel().rows.length`. Pick the model that matches the question. - -Source: docs/guide/row-models.md - -### [HIGH] `getToggleAllRowsSelectedHandler` only selects current page under server pagination - -Wrong: - -```tsx -// Header checkbox only affects current page in manualPagination mode - -``` - -Correct: - -```tsx -// Use page-aware APIs with server pagination - -// For "select all server-side rows", track a separate "all rows mode" -// boolean alongside the row map. -``` - -The table only knows about the current page's rows under `manualPagination`. "Select all" can never mean "every row on the server" without an explicit out-of-band selection mode. - -Source: https://github.com/TanStack/table/issues/4781 - -### [MEDIUM] `autoResetPageIndex` resets to `initialState.pageIndex` (not 0) - -Wrong: - -```ts -// Filtering data resets to the deep-linked pageIndex, not 0 -const table = useTable({ - data, - columns, - initialState: { pagination: { pageIndex: 5, pageSize: 10 } }, - autoResetPageIndex: true, -}) -``` - -Correct: - -```ts -useEffect(() => { - table.setPageIndex(0) -}, [columnFilters, globalFilter]) -// Or: don't rely on autoResetPageIndex when deep-linking pageIndex -``` - -`_autoResetPageIndex` calls `resetPageIndex()` without `true`, which restores to `initialState.pagination.pageIndex` — typically a URL deep-link page that's now invalid. - -Source: https://github.com/TanStack/table/issues/6207 - -### [CRITICAL] Reimplementing pagination math manually - -Wrong: - -```ts -// Hand-rolled page slicing instead of using the API -const paginatedRows = useMemo( - () => allRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize), - [allRows, pageIndex, pageSize], -) -``` - -Correct: - -```ts -const table = useTable({ - features: tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), - }), - columns, - data, -}) -// table.nextPage(), table.setPageIndex(...), table.getRowModel().rows -``` +Correct: `const options = { onPaginationChange: setPagination, autoResetPageIndex: false }` -The built-in APIs honor `autoResetPageIndex`, `paginateExpandedRows`, and reset interactions across other features. +Client data and processing changes may reset page index; configure the policy when product state must retain it. -Source: maintainer interview (Phase 4, 2026-05-17) +Source: `docs/framework/react/guide/pagination.md#auto-reset-page-index` -## See also +## API Discovery -- `tanstack-table/state-management` — `manualPagination` + `autoResetAll` -- `tanstack-table/row-expanding` — `paginateExpandedRows` interaction -- `tanstack-table/row-selection` — "select all" pitfalls under server pagination +Inspect `node_modules/@tanstack/table-core/src/features/row-pagination/` for reset rules, count calculation, and navigation APIs. diff --git a/packages/table-core/skills/row-expanding/SKILL.md b/packages/table-core/skills/row-expanding/SKILL.md deleted file mode 100644 index cb86d911a9..0000000000 --- a/packages/table-core/skills/row-expanding/SKILL.md +++ /dev/null @@ -1,356 +0,0 @@ ---- -name: row-expanding -description: > - Expand and collapse rows in TanStack Table v9 with the `expandedRowModel` - stage. Two patterns: (1) tree sub-rows via `getSubRows`, (2) detail panels - via `getRowCanExpand`. Covers `rowExpandingFeature` + `createExpandedRowModel()`, - `state.expanded` (ExpandedState = true | Record), - `onExpandedChange`, `manualExpanding`, `paginateExpandedRows` (default true), - `autoResetExpanded`, `row.toggleExpanded` / `getIsExpanded` / `getCanExpand` / - `getIsAllParentsExpanded` / `getToggleExpandedHandler`, `table.toggleAllRowsExpanded`, - `row.depth` for indentation, and the `filterFromLeafRows` / - `maxLeafRowFilterDepth` interaction with filtering. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management -sources: - - TanStack/table:docs/guide/expanding.md - - TanStack/table:packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts - - TanStack/table:examples/react/expanding/src/main.tsx - - TanStack/table:examples/react/sub-components/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management`. Read it first for the atom model. - -## Setup - -```ts -import { - tableFeatures, - rowExpandingFeature, - createExpandedRowModel, - constructTable, -} from '@tanstack/table-core' -import type { ExpandedState } from '@tanstack/table-core' - -const features = tableFeatures({ - rowExpandingFeature, - expandedRowModel: createExpandedRowModel(), -}) - -// Tree mode — data has nested subRows -const table = constructTable({ - features, - columns, - data, - getSubRows: (row) => row.subRows, - initialState: { expanded: {} satisfies ExpandedState }, -}) - -// Or detail-panel mode — every row can expand to a sub-component -const detailTable = constructTable({ - features, - columns, - data, - getRowCanExpand: () => true, -}) -``` - -## Core Patterns - -### Tree table with indentation - -```tsx -// From examples/react/expanding/src/main.tsx -{ - row.getVisibleCells().map((cell, i) => ( - - {i === 0 && row.getCanExpand() ? ( - - ) : null} - - - )) -} -``` - -`row.depth` is 0-based. `row.getCanExpand()` returns true when `row.subRows.length > 0`. - -### Detail panels for flat data - -```tsx -// From examples/react/sub-components/src/main.tsx -{ - table.getRowModel().rows.map((row) => ( - - - {row.getVisibleCells().map((cell) => ( - - - - ))} - - {row.getIsExpanded() && ( - - - - - - )} - - )) -} -``` - -### Toggle ALL rows expanded at once - -```tsx - -``` - -### Keep expanded children on their parent's page - -```ts -const table = constructTable({ - features: tableFeatures({ - rowExpandingFeature, - rowPaginationFeature, - expandedRowModel: createExpandedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - }), - columns, - data, - getSubRows: (r) => r.subRows, - paginateExpandedRows: false, // children stay glued to their parent -}) -``` - -`paginateExpandedRows: true` (default) flows expanded children through pagination — each child counts toward `pageSize`. `false` keeps them stuck under their parent. - -### Tree filtering with leaf-match propagation - -```ts -const table = constructTable({ - features: tableFeatures({ - rowExpandingFeature, - columnFilteringFeature, - expandedRowModel: createExpandedRowModel(), - filteredRowModel: createFilteredRowModel(), - filterFns, - }), - columns, - data, - getSubRows: (r) => r.subRows, - filterFromLeafRows: true, // keep parent visible when ANY descendant matches -}) -``` - -## Common Mistakes - -### [HIGH] Setting `getRowCanExpand: () => true` together with tree data - -Wrong: - -```ts -// every row gets an expander icon, including leaves with no subRows -const table = useTable({ - getRowCanExpand: () => true, - getSubRows: (r) => r.subRows, -}) -// In cell: row.getCanExpand() always true → leaf rows show 👉 with nothing to expand -``` - -Correct: - -```ts -// For pure tree data, omit getRowCanExpand and let it auto-detect: -const table = useTable({ - features, - columns, - data, - getSubRows: (row) => row.subRows, - // row.getCanExpand() is true only when subRows.length > 0 -}) - -// For pure detail panels, override and skip getSubRows: -const table = useTable({ - features: tableFeatures({ - rowExpandingFeature, - expandedRowModel: createExpandedRowModel(), - }), - columns, - data, - getRowCanExpand: () => true, -}) -``` - -`row_getCanExpand` resolves to `options.getRowCanExpand?.(row) ?? (enableExpanding ?? true) && !!row.subRows.length`. When `getRowCanExpand` is set, it wins — including for leaves. - -Source: packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts - -### [MEDIUM] Setting `paginateExpandedRows: false` and expecting `pageSize` to be a hard cap - -Wrong: - -```ts -const table = useTable({ - paginateExpandedRows: false, - initialState: { pagination: { pageSize: 10 } }, - // user expands all parents → 10 parents * 5 children = 60 visible rows -}) -``` - -Correct: - -```ts -// Default behavior — children flow through pagination, pageSize is enforced: -const table = useTable({ - features, - columns, - data, - getSubRows: (r) => r.subRows, - // paginateExpandedRows defaults to true -}) - -// OR keep children with parent — accept that pageSize is a soft cap: -// paginateExpandedRows: false -``` - -`paginateExpandedRows: false` inflates each parent's page slice via `expandRows` — more rows render than `pageSize`. Pick deliberately. - -Source: packages/table-core/src/features/row-pagination/createPaginatedRowModel.ts - -### [MEDIUM] Storing `expanded` as `true` then writing into it like a Record - -Wrong: - -```ts -// spreading `true` (a boolean) into an object gives {} - all rows collapse except [id] -setExpanded((old) => ({ ...old, [row.id]: true })) -// When old === true, this becomes { [row.id]: true } — everything else collapses! -``` - -Correct: - -```ts -// Prefer the built-in handler — it materializes properly: - - -// Or handle materialization yourself: -table.setExpanded((old) => { - if (old === true) { - const map: Record = {} - Object.keys(table.getRowModel().rowsById).forEach((id) => { map[id] = true }) - return { ...map, [row.id]: !map[row.id] } - } - return { ...old, [row.id]: !(old as Record)[row.id] } -}) -``` - -`ExpandedState = true | Record`. The `true` literal means "all rows expanded" — `row.toggleExpanded` materializes it correctly before applying per-row changes. - -Source: packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts - -### [MEDIUM] `manualExpanding: true` with `expandedRowModel` registered - -Wrong: - -```ts -// manualExpanding bypasses the expanded row model; sub-rows are never flattened -const table = useTable({ - features: tableFeatures({ - rowExpandingFeature, - expandedRowModel: createExpandedRowModel(), // ignored when manualExpanding: true - }), - columns, - data, - getSubRows: (r) => r.subRows, - manualExpanding: true, -}) -``` - -Correct: - -```ts -// Manual expanding is for server-side patterns where the server returns -// a pre-flattened view based on which rows are expanded. -const table = useTable({ - features: tableFeatures({ rowExpandingFeature }), - // no expandedRowModel registered for manual mode - columns, - data: dataQuery.data, // server returns flattened rows when expanded - manualExpanding: true, - state: { expanded }, - onExpandedChange: setExpanded, -}) - -// For client-side tree, omit manualExpanding: -const clientTable = useTable({ - features: tableFeatures({ - rowExpandingFeature, - expandedRowModel: createExpandedRowModel(), - }), - columns, - data, - getSubRows: (r) => r.subRows, -}) -``` - -With `manualExpanding: true`, `getExpandedRowModel` skips the registered factory and returns `getPreExpandedRowModel()` (sorted rows). The expanded state still tracks "which rows are open" but the row model is NOT inflated. - -Source: packages/table-core/src/core/row-models/coreRowModelsFeature.utils.ts - -### [CRITICAL] Reimplementing tree flattening manually - -Wrong: - -```ts -// Hand-rolled tree-to-flat conversion -const flatRows = useMemo(() => { - const out: Person[] = [] - function walk(rows: Person[], depth = 0) { - rows.forEach((r) => { - out.push({ ...r, depth }) - if (expanded[r.id]) walk(r.subRows ?? [], depth + 1) - }) - } - walk(data) - return out -}, [data, expanded]) -``` - -Correct: - -```ts -const table = useTable({ - features: tableFeatures({ - rowExpandingFeature, - expandedRowModel: createExpandedRowModel(), - }), - columns, - data, - getSubRows: (r) => r.subRows, -}) - -// then: table.getRowModel().rows already has row.depth and the expanded view -table.getRowModel().rows.map((row) => /* render with row.depth */) -``` - -Source: maintainer interview (Phase 4, 2026-05-17) - -## See also - -- `tanstack-table/grouping` — pairs with expanding for drill-down on grouped rows -- `tanstack-table/pagination` — `paginateExpandedRows` interaction -- `tanstack-table/filtering` — `filterFromLeafRows` / `maxLeafRowFilterDepth` for tree filtering diff --git a/packages/table-core/skills/row-pinning/SKILL.md b/packages/table-core/skills/row-pinning/SKILL.md index 58e5a62a97..62d545f3c7 100644 --- a/packages/table-core/skills/row-pinning/SKILL.md +++ b/packages/table-core/skills/row-pinning/SKILL.md @@ -1,273 +1,82 @@ --- name: row-pinning description: > - Pin specific rows to a top or bottom region in TanStack Table v9. State shape - is `rowPinning: { top: string[]; bottom: string[] }` keyed by `row.id`. Covers - `rowPinningFeature`, `row.pin(position, includeLeafRows?, includeParentRows?)`, - `row.getIsPinned` / `getPinnedIndex` / `getCanPin`, `table.getTopRows` / - `getBottomRows` / `getCenterRows` / `getIsSomeRowsPinned`, the - `enableRowPinning` option (bool or row predicate), and `keepPinnedRows` - (default true — persist across pagination/filtering vs. hide when filtered out). - Simpler pipeline than column pinning — only one reorder step: Row Pinning → - Sorting. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management + Pin stable row IDs into top, center, and bottom collections with rowPinningFeature and keepPinnedRows. Load for filtering/pagination visibility, explicit region rendering, or renderer-owned sticky CSS. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features'] sources: - - TanStack/table:docs/guide/row-pinning.md - - TanStack/table:packages/table-core/src/features/row-pinning/rowPinningFeature.utils.ts - - TanStack/table:examples/react/row-pinning/src/main.tsx + - 'TanStack/table:docs/framework/react/guide/row-pinning.md' + - 'TanStack/table:packages/table-core/src/features/row-pinning' + - 'TanStack/table:examples/react/row-pinning' --- -This skill builds on `tanstack-table/state-management`. Read it first for the atom model. +This skill builds on `core` and `table-features`. Pinning creates row regions; the renderer controls order and sticky layout. ## Setup ```ts -import { - tableFeatures, - rowPinningFeature, - rowPaginationFeature, - createPaginatedRowModel, - constructTable, -} from '@tanstack/table-core' -import type { RowPinningState } from '@tanstack/table-core' - -const features = tableFeatures({ - rowPinningFeature, - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), -}) - -const table = constructTable({ - features, - columns, - data, - getRowId: (row) => row.userId, // ← essentially mandatory - initialState: { - rowPinning: { top: [], bottom: [] } satisfies RowPinningState, - }, -}) - -// Pin a row -row.pin('top') // or 'bottom' | false +import { rowPinningFeature, tableFeatures } from '@tanstack/table-core' + +type Person = { id: string; name: string } +export const features = tableFeatures({ rowPinningFeature }) +export const options = { + getRowId: (row: Person) => row.id, + keepPinnedRows: true, +} ``` ## Core Patterns -### Pin/unpin buttons in a cell - -```tsx -// From examples/react/row-pinning/src/main.tsx - - -{row.getIsPinned() && } -``` - -For grouped/expanded data, pass include flags: - -```ts -row.pin('top', /* includeLeafRows */ true, /* includeParentRows */ false) -``` - -### Render pinned rows separately - -```tsx - - {table.getTopRows().map((row) => ( - - ))} - {table.getCenterRows().map((row) => ( - - {row.getAllCells().map((cell) => ( - - - - ))} - - ))} - {table.getBottomRows().map((row) => ( - - ))} - -``` - -### Disable persistence across pagination - ```ts -const table = constructTable({ - features: tableFeatures({ - rowPinningFeature, - columnFilteringFeature, - filteredRowModel: createFilteredRowModel(), - filterFns, - }), - columns, - data, - getRowId: (row) => row.id, - keepPinnedRows: false, // pinned rows disappear when filtered/paginated out -}) +const regions = [ + table.getTopRows(), + table.getCenterRows(), + table.getBottomRows(), +] +for (const rows of regions) rows.forEach(renderRow) ``` -`keepPinnedRows: true` (default) keeps pinned rows visible even when their underlying row would otherwise be filtered or paginated away. - -### Conditional pin permission - -```ts -const table = constructTable({ - features, - columns, - data, - enableRowPinning: (row) => !row.original.archived, // predicate form -}) -``` +Render each region explicitly if the visual order matters. ## Common Mistakes -### [HIGH] Omitting `getRowId` so pins attach to array indices - -Wrong: - -```ts -// row.id defaults to row.index; pin survives wrong rows after refetch -const table = useTable({ - features: tableFeatures({ rowPinningFeature, rowPaginationFeature }), - data, // refetched periodically -}) -``` - -Correct: - -```ts -const table = useTable({ - features: tableFeatures({ rowPinningFeature, rowPaginationFeature }), - data, - getRowId: (row) => row.userId, // or row.uuid, row.id from API, etc. -}) - -// For grouped/expanded data, pass the include flags too: -row.pin('top', includeLeafRows, includeParentRows) -``` - -`rowPinning.top` and `rowPinning.bottom` are arrays of string row ids. Default `row.id` is the data array index — refetched data reuses index 3 for a different record, but the pinning state still pins index 3. - -Source: docs/guide/row-selection.md (same root principle); examples/react/row-pinning/src/main.tsx - -### [MEDIUM] Surprise behavior from `keepPinnedRows: true` default - -Wrong: - -```ts -// Expecting pinned rows to vanish on filter, but they don't (default) -const table = useTable({ - features: tableFeatures({ rowPinningFeature, columnFilteringFeature }), - // keepPinnedRows defaults to true; pinned rows survive filtering -}) -``` - -Correct: - -```ts -// Be explicit about the UX you want -const table = useTable({ - features: tableFeatures({ rowPinningFeature, columnFilteringFeature }), - keepPinnedRows: false, // pinned rows disappear when filtered/paginated out -}) - -// Or keep the default and render pinned separately: - - {table.getTopRows().map((row) => )} - {table.getCenterRows().map((row) => )} - {table.getBottomRows().map((row) => )} - -``` - -`keepPinnedRows: true` makes `getTopRows()` / `getBottomRows()` search the full pre-pagination row set; `false` only finds rows currently in the row model. - -Source: packages/table-core/src/features/row-pinning/rowPinningFeature.utils.ts; examples/react/row-pinning/src/main.tsx - -### [MEDIUM] Rendering pinned rows TWICE (once at top/bottom, once in main flow) +### [HIGH] Persisting index-based IDs -Wrong: +Wrong: `const options = { getRowId: (_row: Person, index: number) => String(index) }` -```tsx - - {table.getTopRows().map((row) => ( - - ))} - {table.getRowModel().rows.map( - ( - row, // ← still includes pinned rows - ) => ( - ... - ), - )} - {table.getBottomRows().map((row) => ( - - ))} - -``` +Correct: `const options = { getRowId: (row: Person) => row.id }` -Correct: +Indexes change under sorting, filtering, pagination, and insertion. -```tsx - - {table.getTopRows().map((row) => ( - - ))} - {table.getCenterRows().map((row) => ( - - {row.getAllCells().map((cell) => ( - - - - ))} - - ))} - {table.getBottomRows().map((row) => ( - - ))} - -``` +Source: `docs/framework/react/guide/row-pinning.md` -`getRowModel()` returns the complete current row model with pinned rows still in it. Use `getCenterRows()` for the main flow. Use `getRowModel()` only if you intentionally want pinned rows duplicated. +### [HIGH] Expecting sticky rows automatically -Source: examples/react/row-pinning/src/main.tsx +Wrong: `row.pin('top')` -### [CRITICAL] Reimplementing pin behavior manually +Correct: `topRowElement.style.position = 'sticky'` -Wrong: +Pinning state does not style or place DOM elements. -```ts -// Hand-rolled "pinned" map + manual filter on render -const [pinned, setPinned] = useState>({}) -const pinnedRows = rows.filter((r) => pinned[r.id]) -const otherRows = rows.filter((r) => !pinned[r.id]) -``` +Source: `examples/react/row-pinning/src/main.tsx` -Correct: +## Choose the pinned-row visibility policy -```ts -const table = useTable({ - features: tableFeatures({ rowPinningFeature }), - columns, - data, - getRowId: (row) => row.id, -}) +- `keepPinnedRows: true` is the default. Pinned rows stay visible in their + pinned region even when filtering or pagination removes them from the center + row model. +- `keepPinnedRows: false` limits pinned rows to those present in the current + filtered and paginated row model. -row.pin('top') // pin one row -table.setRowPinning({ top: ['a', 'b'], bottom: [] }) // bulk set -table.getTopRows() -table.getCenterRows() -table.getBottomRows() -``` +Choose explicitly based on product behavior; neither value is inherently wrong. -Source: maintainer interview (Phase 4, 2026-05-17) +Source: `packages/table-core/src/features/row-pinning/rowPinningFeature.types.ts` -## See also +## API Discovery -- `tanstack-table/state-management` — `rowPinning` state slice ownership -- `tanstack-table/row-selection` — same `getRowId` stability concern -- `tanstack-table/column-layout` — column pinning sits in a separate, more complex pipeline +Inspect `node_modules/@tanstack/table-core/src/features/row-pinning/` for region getters, row APIs, and `keepPinnedRows` semantics. diff --git a/packages/table-core/skills/row-selection/SKILL.md b/packages/table-core/skills/row-selection/SKILL.md index 79143955db..1aed10f3e2 100644 --- a/packages/table-core/skills/row-selection/SKILL.md +++ b/packages/table-core/skills/row-selection/SKILL.md @@ -1,388 +1,76 @@ --- name: row-selection description: > - Track which rows are selected in TanStack Table v9 via - `rowSelection: Record`. Covers `rowSelectionFeature`, - the three selected-row APIs (`getSelectedRowModel`, `getFilteredSelectedRowModel`, - `getGroupedSelectedRowModel`), `row.toggleSelected` / `getIsSelected` / - `getIsSomeSelected` (indeterminate) / `getCanSelect` / `getCanMultiSelect`, - `row.getToggleSelectedHandler`, header APIs (`getIsAllRowsSelected` / - `getIsSomeRowsSelected` / `getToggleAllRowsSelectedHandler` and the page-aware - variants), `enableRowSelection` (bool or predicate), - `enableMultiRowSelection: false` for radio-style, `enableSubRowSelection`, - and why `getRowId` is essentially mandatory — especially under server pagination. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management - - column-definitions + Maintain rowSelection ID state with stable getRowId, single, multi, and subrow rules, selected row models, and manual-pagination semantics. Load when selected IDs outlive loaded Row objects or data removal. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features'] sources: - - TanStack/table:docs/guide/row-selection.md - - TanStack/table:packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts - - TanStack/table:packages/table-core/src/features/row-selection/rowSelectionFeature.types.ts - - TanStack/table:examples/react/row-selection/src/main.tsx + - 'TanStack/table:docs/framework/react/guide/row-selection.md' + - 'TanStack/table:packages/table-core/src/features/row-selection' + - 'TanStack/table:examples/react/row-selection' --- -This skill builds on `tanstack-table/state-management` and `tanstack-table/column-definitions`. Read those first for state ownership and `getRowId`. +This skill builds on `core` and `table-features`. Selection is independent ID state; selected row models can only materialize loaded rows. ## Setup ```ts -import { - tableFeatures, - rowSelectionFeature, - constructTable, -} from '@tanstack/table-core' -import type { RowSelectionState } from '@tanstack/table-core' +import { rowSelectionFeature, tableFeatures } from '@tanstack/table-core' -const features = tableFeatures({ rowSelectionFeature }) - -const table = constructTable({ - features, - columns, - data, - getRowId: (row) => row.id, // ← essentially mandatory - initialState: { rowSelection: {} satisfies RowSelectionState }, - enableRowSelection: true, -}) +type Person = { id: string; name: string } +export const features = tableFeatures({ rowSelectionFeature }) +export const options = { + getRowId: (row: Person) => row.id, + enableSubRowSelection: false, +} ``` ## Core Patterns -### Select column with header "select all" + per-row checkbox - -```tsx -// From examples/react/row-selection/src/main.tsx -columnHelper.display({ - id: 'select', - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), -}), -``` - -### Single-select (radio-style) - -```tsx -const table = useTable({ - features: tableFeatures({ rowSelectionFeature }), - columns, - data, - getRowId: (row) => row.id, - enableMultiRowSelection: false, // ← radio-like -}) - -// Cell renders a radio, no "select all" header makes sense -columnHelper.display({ - id: 'select', - header: '', // no select-all in single-select mode - cell: ({ row }) => ( - - ), -}) -``` - -### Conditional selection per row - ```ts -const table = useTable({ - features, - columns, - data, - getRowId: (row) => row.id, - enableRowSelection: (row) => row.original.age > 18, // predicate form -}) +const selectedIds = table.getSelectedRowIds() +const loadedSelectedRows = table.getSelectedRowModel().rows ``` -### Three selected-row APIs - -```ts -table.getSelectedRowModel() // built off core — raw data -table.getFilteredSelectedRowModel() // built off filtered — current filters applied -table.getGroupedSelectedRowModel() // built off grouped — current groups applied -``` - -### Hoist selection to an external atom - -```tsx -import { useCreateAtom } from '@tanstack/react-store' - -const rowSelectionAtom = useCreateAtom({}) - -const table = useTable({ - features, - columns, - data, - getRowId: (row) => row.id, - atoms: { rowSelection: rowSelectionAtom }, -}) - -// Send selected IDs to an API call from a sibling component -function ExportButton() { - const selection = useStore(rowSelectionAtom) - return ( - - ) -} -``` +Use IDs for database-wide intent and row models for currently loaded objects. ## Common Mistakes -### [HIGH] Omitting `getRowId` under `manualPagination` +### [HIGH] Expecting selection to clean itself -Wrong: +Wrong: `data = data.filter(row => row.id !== deletedId)` -```ts -// Server-side pagination + default row.id = row.index -const table = useTable({ - features: tableFeatures({ rowSelectionFeature, rowPaginationFeature }), - data, // only current page from server - manualPagination: true, - rowCount, -}) -// After paging, rowSelection: { '5': true } is ambiguous -// — selection appears to move with the user. -``` +Correct: `data = data.filter(row => row.id !== deletedId); table.setRowSelection(old => { const next = { ...old }; delete next[deletedId]; return next })` -Correct: +Selection is independent state and can retain IDs after data removal. -```ts -const table = useTable({ - features: tableFeatures({ rowSelectionFeature, rowPaginationFeature }), - data, - manualPagination: true, - rowCount, - getRowId: (row) => row.uuid, // stable across pages -}) +Source: `https://github.com/TanStack/table/issues/5850` -// For "X of Y selected" with server-side pagination, read state directly: -const totalSelected = Object.keys(table.state.rowSelection).length -``` - -`row.id` defaults to `row.index`. Under `manualPagination`, every page reuses indices 0..n-1, so selection IDs collide across pages. - -Source: docs/guide/row-selection.md; examples/react/row-selection/src/main.tsx - -### [MEDIUM] `enableMultiRowSelection: false` + a "select all" checkbox header - -Wrong: - -```tsx -const table = useTable({ - features: tableFeatures({ rowSelectionFeature }), - enableMultiRowSelection: false, // radio-like -}) - -// Header still renders a checkbox + indeterminate - -``` - -Correct: - -```tsx -const table = useTable({ - features: tableFeatures({ rowSelectionFeature }), - enableMultiRowSelection: false, - getRowId: (row) => row.id, -}) - -// Drop the toggle-all header in single-select mode -columnHelper.display({ - id: 'select', - header: '', - cell: ({ row }) => ( - - ), -}) -``` - -In single-select mode, `mutateRowIsSelected` clears all other ids before adding the new one. "Select all" becomes effectively no-op and indeterminate is meaningless. - -Source: docs/guide/row-selection.md; packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts +### [HIGH] Selecting mutable indexes -### [HIGH] `getSelectedRowModel().flatRows` for counts under `manualPagination` +Wrong: `const options = { getRowId: (_row: Person, index: number) => String(index) }` -Wrong: +Correct: `const options = { getRowId: (row: Person) => row.id }` -```ts -// Under manualPagination, only counts the visible page's selected rows -const selectedCount = table.getSelectedRowModel().flatRows.length +Stable application IDs preserve identity as row order and pages change. -const handleBulkAction = () => { - const ids = table.getSelectedRowModel().flatRows.map((row) => row.original.id) - api.archive(ids) // missing all selections from other pages! -} -``` +Source: `docs/framework/react/guide/row-selection.md#useful-row-ids` -Correct: - -```ts -// For counts and id lists under manualPagination, read state directly -const selectedCount = Object.keys(table.state.rowSelection).length - -const handleBulkAction = () => { - const ids = Object.keys(table.state.rowSelection) - api.archive(ids) -} -// (Client-side: getSelectedRowModel is fine — data contains every row.) -``` +### [HIGH] Treating loaded model as global selection -`getSelectedRowModel` walks the core row model — which under `manualPagination` only contains the current page. `state.rowSelection` may contain ids that aren't in `data` (that's by design). +Wrong: `const allSelectedRecords = table.getSelectedRowModel().rows` -Source: docs/guide/row-selection.md; packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts +Correct: `const allSelectedIds = table.getSelectedRowIds()` -### [MEDIUM] Surprise sub-row propagation from `enableSubRowSelection` default - -Wrong: - -```ts -// Default behavior: clicking the parent selects all children -const table = useTable({ - features: tableFeatures({ rowSelectionFeature, rowExpandingFeature }), - getSubRows: (row) => row.subRows, - // enableSubRowSelection unset — defaults to true -}) -``` - -Correct: - -```ts -const table = useTable({ - features: tableFeatures({ rowSelectionFeature, rowExpandingFeature }), - getSubRows: (row) => row.subRows, - enableSubRowSelection: false, // toggling parent doesn't touch subRows -}) - -// Or selectively: -enableSubRowSelection: (row) => row.depth > 0, - -// Indeterminate parent checkbox - -``` - -`enableSubRowSelection: true` is the default. `mutateRowIsSelected` recurses into `row.subRows` when truthy. Decide deliberately — "select group as a whole" UX wants this off. - -Source: docs/guide/row-selection.md; packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts - -### [HIGH] Parent checkbox stuck "unchecked" with all sub-rows selected (deep trees) - -Wrong: - -```ts -// Returns false even when all leaf descendants are selected -const isParentChecked = row.getIsAllSubRowsSelected() -``` - -Correct: - -```ts -const allLeafs = row.getLeafRows() -const allSelected = - allLeafs.length > 0 && allLeafs.every((r) => r.getIsSelected()) -const someSelected = allLeafs.some((r) => r.getIsSelected()) -``` - -`getIsAllSubRowsSelected` only counts direct children. With multi-level grouping, a parent reports based on a partial count. - -Source: https://github.com/TanStack/table/issues/4878; https://github.com/TanStack/table/issues/4759 - -### [HIGH] Stale rowSelection IDs after data refresh - -Wrong: - -```ts -useEffect(() => { - refreshData() // selection still references deleted IDs -}, [trigger]) -``` - -Correct: - -```ts -useEffect(() => { - setRowSelection((prev) => { - const validIds = new Set(data.map((row) => row.id)) - const next: RowSelectionState = {} - for (const id in prev) if (validIds.has(id)) next[id] = prev[id] - return next - }) -}, [data]) -``` - -v8 removed v7's `autoResetSelectedRows`. With websockets / refetch, IDs that no longer exist remain in `rowSelection` and `getIsAllRowsSelected()` returns true based on stale state. Prune yourself. - -Source: https://github.com/TanStack/table/issues/5850; https://github.com/TanStack/table/issues/4498 - -### [CRITICAL] Reimplementing selection state manually - -Wrong: - -```ts -// Hand-rolled "selected" set, bypassing the table -const [selected, setSelected] = useState(new Set()) -const toggle = (id: string) => { - setSelected((s) => { - const next = new Set(s) - next.has(id) ? next.delete(id) : next.add(id) - return next - }) -} -``` - -Correct: - -```ts -const table = useTable({ - features: tableFeatures({ rowSelectionFeature }), - columns, - data, - getRowId: (row) => row.id, -}) -row.toggleSelected() -row.toggleSelected(true) -table.toggleAllRowsSelected() -table.setRowSelection({ abc: true }) -``` +Under manual pagination, unloaded selected IDs have no `Row` object in the current model. -Source: maintainer interview (Phase 4, 2026-05-17) +Source: `docs/framework/react/guide/row-selection.md#note-if-you-are-using-manualpagination` -## See also +## API Discovery -- `tanstack-table/column-definitions` — `getRowId` is the foundation of every row-keyed feature -- `tanstack-table/state-management` — `rowSelection` slice + atoms for sharing selection -- `tanstack-table/pagination` — server-pagination "select all" pitfalls -- `tanstack-table/grouping` — `getGroupedSelectedRowModel` distinction +Inspect `node_modules/@tanstack/table-core/src/features/row-selection/` for state, row-model variants, and selection enablement callbacks. diff --git a/packages/table-core/skills/setup/SKILL.md b/packages/table-core/skills/setup/SKILL.md deleted file mode 100644 index 70ea95d122..0000000000 --- a/packages/table-core/skills/setup/SKILL.md +++ /dev/null @@ -1,390 +0,0 @@ ---- -name: setup -description: > - Install a TanStack Table v9 framework adapter and wire up a first table with - `tableFeatures({...})` declaring `features` (which now also carries row model - factories like `sortedRowModel: createSortedRowModel()`, `filteredRowModel: - createFilteredRowModel()`, `paginatedRowModel: createPaginatedRowModel()`, and - fn registries like `sortFns`/`filterFns`), a - `createColumnHelper()` column set, and the framework - `useTable` / `injectTable` / `createTable` / `constructTable` entry point. - Covers the registry model, why `features` must be module-scoped, when to reach - for `stockFeatures`, and `coreFeatures`. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management - - column-definitions -sources: - - TanStack/table:docs/installation.md - - TanStack/table:docs/overview.md - - TanStack/table:docs/guide/tables.md - - TanStack/table:docs/guide/features.md - - TanStack/table:packages/table-core/src/index.ts - - TanStack/table:examples/react/basic-use-table/src/main.tsx ---- - -This skill builds on `tanstack-table/state-management` and `tanstack-table/column-definitions`. Read those first for the v9 atom model and the `createColumnHelper()` shape. - -## Setup - -TanStack Table v9 separates a framework-agnostic core (`@tanstack/table-core`) from per-framework adapters. Every table — vanilla or framework — must declare the `features` option at construction time: the registry of feature plugins, row model factories, and fn registries. - -```ts -// Framework-agnostic, using @tanstack/table-core directly. -import { - constructTable, - tableFeatures, - createColumnHelper, - rowSortingFeature, - createSortedRowModel, - sortFns, -} from '@tanstack/table-core' - -type Person = { firstName: string; lastName: string; age: number } - -// 1. Declare features at MODULE scope (stable reference). This is required — -// a fresh `features` object on every call destroys feature registration. -const features = tableFeatures({ - rowSortingFeature, - // Row model factories and fn registries live on the features object. - sortedRowModel: createSortedRowModel(), - sortFns, -}) - -// 2. Build a column helper bound to BOTH TFeatures and TData. -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { header: 'First' }), - columnHelper.accessor('lastName', { header: 'Last' }), - columnHelper.accessor('age', { header: 'Age', sortFn: 'alphanumeric' }), -]) - -const data: Person[] = [ - { firstName: 'Ada', lastName: 'Lovelace', age: 36 }, - { firstName: 'Grace', lastName: 'Hopper', age: 85 }, -] - -const table = constructTable({ - features, - columns, - data, -}) - -// Read header groups and row model to render. -table.getHeaderGroups() -table.getRowModel().rows -``` - -Framework adapters wrap this with reactivity: - -| Framework | Entry point | Package | -| ------------------------------ | ---------------------------------------------- | ------------------------- | -| React, Preact, Vue, Solid, Lit | `useTable` / `createTable` / `TableController` | `@tanstack/-table` | -| Angular | `injectTable` | `@tanstack/angular-table` | -| Svelte 5 | `createTable` (runes) | `@tanstack/svelte-table` | -| Vanilla JS | `constructTable` + `storeReactivityBindings()` | `@tanstack/table-core` | - -## Core Patterns - -### Minimum table (no extra features) - -```ts -const features = tableFeatures({}) // core features only -const table = constructTable({ - features, - columns, - data, -}) -``` - -`tableFeatures({})` is the canonical "I only want the core read pipeline" setup. - -### Add features and their row models together - -```ts -import { - rowSortingFeature, - rowPaginationFeature, - columnFilteringFeature, - createSortedRowModel, - createFilteredRowModel, - createPaginatedRowModel, - sortFns, - filterFns, -} from '@tanstack/table-core' - -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - columnFilteringFeature, - sortedRowModel: createSortedRowModel(), - filteredRowModel: createFilteredRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, - filterFns, -}) - -const table = constructTable({ - features, - columns, - data, -}) -``` - -Each feature and its row-model factory are registered together — TypeScript exposes the feature's APIs (`table.setSorting`, `table.nextPage`, `table.setColumnFilters`) only when its feature plugin is present in `features`. - -### Vanilla JS reactivity binding - -```ts -import { constructTable, tableFeatures } from '@tanstack/table-core' -import { storeReactivityBindings } from '@tanstack/table-core' - -const features = tableFeatures({}) - -const table = constructTable({ - features, - columns, - data, - // Use the vanilla binding when no framework adapter exists - _processingMode: 'core', -}) - -// Subscribe to a slice -const unsub = table.atoms.sorting.subscribe((sorting) => { - console.log('sorting changed', sorting) -}) -``` - -### `stockFeatures` for v8-like "everything on" - -Reach for this only as a transitional aid. It re-introduces v8 bundle size (~15–20kb) and undoes v9's tree-shaking benefit. - -```ts -import { stockFeatures, tableFeatures } from '@tanstack/table-core' - -// Discouraged in new code. Register only what you use. -const features = tableFeatures(stockFeatures) -``` - -## Common Mistakes - -### [CRITICAL] API/state slice missing because the feature was not registered in `features` - -Wrong: - -```ts -// rowSortingFeature missing — table.setSorting / state.sorting unavailable -const features = tableFeatures({}) // empty -const table = useTable({ features, columns, data }) -table.setSorting([{ id: 'age', desc: true }]) // ❌ does not exist on this table type -``` - -Correct: - -```ts -// Register every feature you intend to use; include its row model factory and fns -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -``` - -In v9, `features` is a tree-shakeable registry — TypeScript hides APIs for unregistered features and the runtime atom is never created. Agents who see `table.setColumnFilters` "missing" often incorrectly conclude the API was removed. - -Source: maintainer interview (Phase 4, 2026-05-17) - -### [CRITICAL] Hallucinating v7 / v8 `useReactTable` + `getCoreRowModel()` shape - -Wrong: - -```ts -// v7 -import { useTable, useSortBy } from 'react-table' -const table = useTable({ columns, data }, useSortBy) - -// v8 -import { useReactTable, getCoreRowModel } from '@tanstack/react-table' -const table = useReactTable({ - columns, - data, - getCoreRowModel: getCoreRowModel(), -}) -``` - -Correct: - -```ts -import { - useTable, - tableFeatures, - rowSortingFeature, - createSortedRowModel, - sortFns, -} from '@tanstack/react-table' - -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -``` - -v7→v8 and v8→v9 both reshaped the API substantially. Agents trained on older data confidently emit v7/v8 shapes; v9 requires `features` with row model factories registered on it. - -Source: maintainer interview (Phase 4, 2026-05-17) - -### [HIGH] Calling `tableFeatures({})` inside the component body - -Wrong: - -```tsx -function MyTable() { - // ❌ new object every render — destroys stable feature registration - const features = tableFeatures({ rowSortingFeature }) - const table = useTable({ features, columns, data }) -} -``` - -Correct: - -```tsx -// ✅ module-scoped, stable reference -const features = tableFeatures({ rowSortingFeature }) - -function MyTable() { - const table = useTable({ features, columns, data }) -} -``` - -A fresh `features` reference on each render churns the table's feature registry — the same way unstable `columns` or `data` cause infinite re-renders. Hoist to module scope or memoize. - -Source: docs/guide/data.md; examples/react/basic-use-table/src/main.tsx - -### [HIGH] Adding a row-model factory without registering its feature - -Wrong: - -```ts -// rowSortingFeature missing — sortedRowModel is orphaned and never runs -const features = tableFeatures({ - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), // no-op without rowSortingFeature - paginatedRowModel: createPaginatedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -``` - -Correct: - -```ts -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -``` - -Row-model factories only run if their matching feature is in `features`. Runtime silently degrades. - -Source: docs/guide/row-models.md; examples/react/basic-external-atoms/src/main.tsx - -### [CRITICAL] Reimplementing what built-in APIs already provide - -Wrong: - -```ts -// ❌ Reimplements sorting state manually instead of using the API -const [sorting, setSorting] = useState([]) -const sortedData = useMemo(() => [...data].sort((a, b) => /* …custom… */), [data, sorting]) -// then uses sortedData directly, bypassing the table -``` - -Correct: - -```ts -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -// then: table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() -``` - -TanStack Table IS a state-management coordinator. The maintainer flags hand-rolled state/sort/filter loops as the #1 tell that an AI wrote the code. Reach for `table.setSorting`, `row.toggleSelected`, `table.nextPage`, `table.setColumnFilters`, `column.toggleVisibility` first. - -Source: maintainer interview (Phase 4, 2026-05-17) - -### [HIGH] Bundling `stockFeatures` when only a few features are used - -Wrong: - -```ts -// Pulls in every feature even though only sorting+pagination are used -import { stockFeatures, tableFeatures } from '@tanstack/react-table' -const features = tableFeatures(stockFeatures) -``` - -Correct: - -```ts -import { - tableFeatures, - rowSortingFeature, - rowPaginationFeature, -} from '@tanstack/react-table' -const features = tableFeatures({ rowSortingFeature, rowPaginationFeature }) -``` - -Tree-shaking via `features` is the headline reason for the v9 redesign. `stockFeatures` exists as a v8-style transitional escape hatch, not a default. - -Source: maintainer interview (Phase 4, 2026-05-17) - -### [CRITICAL] Empty array literal for `data` causes infinite re-renders - -Wrong: - -```tsx -// ❌ Fresh [] each render — infinite loop when items is undefined -const table = useTable({ - features, - columns, - data: items ?? [], -}) -``` - -Correct: - -```tsx -// ✅ Hoist the empty fallback OR memoize -const EMPTY: Person[] = [] - -const table = useTable({ - features, - columns, - data: items ?? EMPTY, -}) -// or: const data = useMemo(() => items ?? [], [items]) -``` - -A new `[]` reference each render means the table sees a fresh `data` prop and rebuilds row models. Top recurring beginner issue. - -Source: https://github.com/TanStack/table/issues/4566; https://github.com/TanStack/table/issues/6002 - -## See also - -- `tanstack-table/state-management` — atom model, ownership precedence, state slices -- `tanstack-table/column-definitions` — `createColumnHelper()` and `getRowId` -- `tanstack-table/migrate-v8-to-v9` — full rename + restructure table for v8 → v9 upgrades diff --git a/packages/table-core/skills/sorting/SKILL.md b/packages/table-core/skills/sorting/SKILL.md index dafed89328..b32c55b8c5 100644 --- a/packages/table-core/skills/sorting/SKILL.md +++ b/packages/table-core/skills/sorting/SKILL.md @@ -1,344 +1,80 @@ --- name: sorting description: > - Sort rows in TanStack Table v9 with the `sortedRowModel` stage. Covers - `rowSortingFeature` + `createSortedRowModel()` (registered on `features`), the built-in `sortFns` - registry (renamed from v8 `sortingFns`), `state.sorting` (SortingState = - Array<{ id, desc }>), `onSortingChange`, `columnDef.sortFn` - (string | function | 'auto'), `sortDescFirst`, `sortUndefined` - ('first'|'last'|-1|1|false), `invertSorting`, `enableMultiSort`, - `maxMultiSortColCount`, `isMultiSortEvent`, `table.setSorting` / - `resetSorting`, `column.getToggleSortingHandler` / - `getNextSortingOrder` / `clearSorting` / `getCanSort` / `getCanMultiSort`, - `manualSorting` for server-side, and fuzzy `compareItems` pairing. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management - - customizing-feature-behavior + Sort with rowSortingFeature, sortedRowModel, sortFns, multi-sort and removal options, sortUndefined, and manualSorting. Load for comparator direction, incoming server order, or product-specific sorting cycles. +metadata: + { + type: sub-skill, + library: '@tanstack/table-core', + library_version: '9.0.0-beta.38', + } +requires: ['core', 'table-features', 'client-vs-server'] sources: - - TanStack/table:docs/guide/sorting.md - - TanStack/table:packages/table-core/src/fns/sortFns.ts - - TanStack/table:packages/table-core/src/features/row-sorting/createSortedRowModel.ts - - TanStack/table:packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts - - TanStack/table:examples/react/sorting/src/main.tsx + - 'TanStack/table:docs/framework/react/guide/sorting.md' + - 'TanStack/table:packages/table-core/src/features/row-sorting' + - 'TanStack/table:examples/react/sorting' --- -This skill builds on `tanstack-table/state-management` and `tanstack-table/customizing-feature-behavior`. Read those first for the atom model and `sortFn` overrides. +This skill builds on `core`, `table-features`, and `client-vs-server`. Sorting state describes order; client processing requires a sorted model and server processing requires sorted input. ## Setup ```ts import { - tableFeatures, - rowSortingFeature, createSortedRowModel, + rowSortingFeature, sortFns, - createColumnHelper, - constructTable, + tableFeatures, } from '@tanstack/table-core' -import type { SortingState } from '@tanstack/table-core' - -type Person = { - firstName: string - lastName: string - age: number - status: 'single' | 'complicated' | 'relationship' -} -const features = tableFeatures({ +export const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns, }) -const columnHelper = createColumnHelper() - -const columns = columnHelper.columns([ - columnHelper.accessor('firstName', { sortFn: 'alphanumeric' }), - columnHelper.accessor('lastName', { - sortUndefined: 'last', - sortDescFirst: false, - }), - columnHelper.accessor('age', { sortFn: 'basic' }), -]) - -const table = constructTable({ - features, - columns, - data, - initialState: { sorting: [] satisfies SortingState }, -}) - -table.setSorting([{ id: 'age', desc: true }]) ``` ## Core Patterns -### Clickable header sorting with multi-sort on Shift+click - -```tsx -// From examples/react/sorting/src/main.tsx -{ - headerGroup.headers.map((header) => ( - - - {{ asc: ' 🔼', desc: ' 🔽' }[header.column.getIsSorted() as string] ?? - null} - - )) -} -``` - -`getToggleSortingHandler` already handles multi-sort when the user holds Shift (configurable via `isMultiSortEvent`). - -### Custom `sortFn` for an enum - ```ts -// From examples/react/sorting/src/main.tsx -const sortStatusFn: SortFn = ( - rowA, - rowB, - _columnId, -) => { - const statusOrder = ['single', 'complicated', 'relationship'] - return ( - statusOrder.indexOf(rowA.original.status) - - statusOrder.indexOf(rowB.original.status) - ) -} - -columnHelper.accessor('status', { sortFn: sortStatusFn }) -``` - -Always return an ascending-order comparison. The row model multiplies by `-1` for descending and again for `invertSorting`. - -### Direction control with `sortUndefined` and `invertSorting` - -```ts -columnHelper.accessor('rank', { - invertSorting: true, // rank 1 above rank 2 even when "descending" -}) - -columnHelper.accessor('lastName', { - sortUndefined: 'last', // ABSOLUTE: end regardless of asc/desc - sortDescFirst: false, -}) +const options = { enableSortingRemoval: false, enableMultiSort: true } +const numericColumn = { sortUndefined: 'last' as const } ``` -`sortUndefined` literals (`'first'`, `'last'`) are absolute. Numeric (`-1`, `1`) flips with `desc`. - -### Server-side sorting - -```tsx -const [sorting, setSorting] = useState([]) -const { data } = useQuery({ - queryKey: ['rows', sorting], - queryFn: () => - fetch('/api/rows?sort=' + serialize(sorting)).then((r) => r.json()), -}) - -const table = useTable({ - features: tableFeatures({ rowSortingFeature }), - // omit sortedRowModel from features — server sorts - columns, - data, - manualSorting: true, - state: { sorting }, - onSortingChange: setSorting, -}) -``` +Configure sort cycles and undefined placement to match the product rather than relying on implicit defaults. ## Common Mistakes -### [HIGH] Using v8 `sortingFn` / `sortingFns` names - -Wrong: - -```tsx -{ - accessorKey: 'fullName', - sortingFn: 'alphanumeric', // v8 name — falls through to sortFn_basic -} -// useTable({ sortingFns: { ...sortingFns, myFn } }) // v8 option name -``` - -Correct: - -```tsx -import { sortFns, createSortedRowModel } from '@tanstack/react-table' - -columnHelper.accessor('firstName', { - sortFn: 'alphanumeric', -}) - -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns: { - ...sortFns, - myCustom: (a, b, id) => a.original[id] - b.original[id], - }, -}) - -const table = useTable({ features, columns, data }) -``` - -v9 renamed `columnDef.sortingFn → sortFn`, the fn registry slot `sortingFns → sortFns` (now registered on `features`), exported registry `sortingFns → sortFns`. The new column option defaults to `'auto'` and falls back to `sortFn_basic` when lookup misses — wrong names sort wrong, silently. - -Source: packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts - -### [MEDIUM] Expecting `sortUndefined: 'first' | 'last'` to work in v8 +### [CRITICAL] Expecting manual mode to reorder -Wrong: +Wrong: `const options = { data: unsortedRows, manualSorting: true }` -```tsx -// agent assumes numeric and literal forms are interchangeable -{ accessorKey: 'lastName', sortUndefined: -1 } // ascending-first, descending-LAST -``` - -Correct: - -```tsx -// From examples/react/sorting/src/main.tsx -columnHelper.accessor((row) => row.lastName, { - id: 'lastName', - sortUndefined: 'last', // ABSOLUTE: always at end regardless of asc/desc - sortDescFirst: false, -}) -``` - -v8 only had `false | -1 | 1`. v9 added `'first'` / `'last'`. Numeric flips with `desc`; literals are absolute. - -Source: packages/table-core/src/features/row-sorting/createSortedRowModel.ts - -### [MEDIUM] Custom `sortFn` factors `desc` in itself - -Wrong: - -```tsx -// takes sort direction into account, breaks the toggle -const customSort: SortFn = (a, b, id, desc) => { - // desc isn't even a parameter — agents try to detect via state - const cmp = a.original[id] - b.original[id] - return desc ? -cmp : cmp -} -``` - -Correct: - -```tsx -// From examples/react/sorting/src/main.tsx -// Always return ascending; the row model handles desc & invertSorting. -const sortStatusFn: SortFn = (rowA, rowB, _columnId) => { - const statusOrder = ['single', 'complicated', 'relationship'] - return ( - statusOrder.indexOf(rowA.original.status) - - statusOrder.indexOf(rowB.original.status) - ) -} -``` - -From the docs guide: "The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take care of that logic." Doubly-flipping yields broken toggles. - -Source: packages/table-core/src/features/row-sorting/createSortedRowModel.ts - -### [MEDIUM] Fuzzy filter without a fuzzy-aware `sortFn` - -Wrong: - -```ts -columnHelper.accessor('fullName', { - filterFn: 'fuzzy', - // BUG: rows sort alphabetically, not by match rank -}) -``` - -Correct: +Correct: `const options = { data: serverSortedRows, manualSorting: true }` -```ts -import { compareItems } from '@tanstack/match-sorter-utils' - -const fuzzySort: SortFn = (rowA, rowB, columnId) => { - let dir = 0 - if (rowA.columnFiltersMeta[columnId]) { - dir = compareItems( - rowA.columnFiltersMeta[columnId].itemRank!, - rowB.columnFiltersMeta[columnId].itemRank!, - ) - } - return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir -} - -columnHelper.accessor('fullName', { filterFn: 'fuzzy', sortFn: fuzzySort }) -``` - -The fuzzy filter writes `{ itemRank }` into `row.columnFiltersMeta[columnId]` via `addMeta`. Without a sortFn that reads it, results sort alphabetically and defeat the fuzzy ranking. +Manual sorting bypasses `sortedRowModel` and trusts incoming order. -Source: examples/react/filters-fuzzy/src/main.tsx +Source: `packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts` -### [MEDIUM] `getCanSort` returns false for display columns under `manualSorting` +### [HIGH] Reversing inside the comparator -Wrong: +Wrong: `const newest: SortFn = (a, b, id) => b.getValue(id) - a.getValue(id)` -```ts -// getCanSort() returns false even though manualSorting is true -const table = useTable({ - manualSorting: true, - columns: [ - { id: 'computed', header: 'Computed', cell: (info) => row.x + row.y }, - ], -}) -``` +Correct: `const numeric: SortFn = (a, b, id) => a.getValue(id) - b.getValue(id)` -Correct: +Return ascending comparison only; Table reverses it when sorting is descending. -```ts -columnHelper.display({ - id: 'computed', - header: 'Computed', - enableSorting: true, // force-enable for manualSorting - cell: (info) => info.row.original.x + info.row.original.y, -}) -``` +Source: `docs/framework/react/guide/sorting.md#custom-sorting-functions` -`getCanSort` checks for `accessorKey`/`accessorFn` even under `manualSorting`. Force it on display columns via `enableSorting: true` (and let the server sort). +### [MEDIUM] Leaving ambiguous sort policy -Source: https://github.com/TanStack/table/issues/4136 +Wrong: `const options = { enableSortingRemoval: true }` -### [CRITICAL] Reimplementing what built-in APIs provide +Correct: `const options = { enableSortingRemoval: false }; const rankColumn = { sortUndefined: 'last' as const }` -Wrong: - -```ts -const [sorting, setSorting] = useState([]) -const sortedData = useMemo( - () => [...data].sort(/* …custom… */), - [data, sorting], -) -// then uses sortedData directly, bypassing the table -``` - -Correct: - -```ts -const features = tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, -}) -const table = useTable({ features, columns, data }) -// table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() -``` +Undefined placement and removal cycles can differ from expected product behavior unless configured. -Source: maintainer interview (Phase 4, 2026-05-17) +Source: `packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts` -## See also +## API Discovery -- `tanstack-table/customizing-feature-behavior` — `sortFn` authoring + `addMeta` chain -- `tanstack-table/filtering` — fuzzy filter pattern that pairs with `fuzzySort` -- `tanstack-table/state-management` — `manualSorting` + server-side state ownership +Inspect `node_modules/@tanstack/table-core/src/features/row-sorting/` and `src/fns/sortFns.ts` for current names and comparator contracts. diff --git a/packages/table-core/skills/state-management/SKILL.md b/packages/table-core/skills/state-management/SKILL.md deleted file mode 100644 index b214491080..0000000000 --- a/packages/table-core/skills/state-management/SKILL.md +++ /dev/null @@ -1,403 +0,0 @@ ---- -name: state-management -description: > - Coordinate TanStack Table v9 state across `initialState`, controlled - `state`+`on*Change`, and external `atoms`. Covers the atom model - (`table.atoms.`, `table.baseAtoms.`, `table.store`, `table.state`), - per-slice precedence (atoms beat state beat initialState beat baseAtoms), - `manualSorting` / `manualFiltering` / `manualPagination` / `manualGrouping` / - `manualExpanding` for server-side data, `autoResetPageIndex` / `autoResetAll`, - reset APIs (`resetSorting`, `resetPagination`, `reset()`), and the - `SortingState` / `PaginationState` / `RowSelectionState` / `ColumnFiltersState` / - `GroupingState` shapes. Foundational for every other skill. -type: core -library: tanstack-table -library_version: '9.0.0-alpha.48' -sources: - - TanStack/table:docs/framework/vanilla/guide/table-state.md - - TanStack/table:docs/framework/react/guide/table-state.md - - TanStack/table:packages/table-core/src/store-reactivity-bindings.ts - - TanStack/table:packages/table-core/src/reactivity.ts - - TanStack/table:packages/table-core/src/core/table/constructTable.ts ---- - -## Setup - -TanStack Table v9 is built on TanStack Store. Each state slice (sorting, pagination, columnFilters, rowSelection, columnVisibility, …) is a separate atom. There are four ownership patterns, and the table reads from them in a fixed precedence. - -```ts -import { - constructTable, - tableFeatures, - rowSortingFeature, - rowPaginationFeature, - createSortedRowModel, - createPaginatedRowModel, - sortFns, -} from '@tanstack/table-core' - -const features = tableFeatures({ - rowSortingFeature, - rowPaginationFeature, - sortedRowModel: createSortedRowModel(), - paginatedRowModel: createPaginatedRowModel(), - sortFns, -}) - -const table = constructTable({ - features, - columns, - data, - // 1. initialState — set starting values; read once at construction - initialState: { - sorting: [{ id: 'lastName', desc: false }], - pagination: { pageIndex: 0, pageSize: 10 }, - }, -}) - -// Read APIs: -table.store.state // flat snapshot of every slice (no subscription) -table.atoms.sorting.get() // single-slice atom read (no subscription) -table.state // typed output of the `useTable` selector (framework adapters) - -// Write APIs use the feature setters — they're atom-aware: -table.setSorting([{ id: 'firstName', desc: true }]) -table.setPageIndex(2) -``` - -The four ownership patterns per slice: - -| Pattern | When to use | Wins over | -| ----------------------------------- | ---------------------------------------------------- | ------------------------------- | -| internal (default) | Most slices in a simple table | nothing — baseline | -| `initialState.` | Set starting value only | internal default | -| `state.` + `onChange` | v8-style controlled state | `initialState` | -| `atoms.` | v9 preferred — share with other components / queries | `state`+`on*Change` (silently!) | - -## Core Patterns - -### Internal state with `initialState` - -```ts -const table = constructTable({ - features, - columns, - data, - initialState: { sorting: [{ id: 'age', desc: false }] }, -}) -// State lives entirely inside the table. -table.setSorting([{ id: 'firstName', desc: true }]) -``` - -### Controlled state with `state` + `on*Change` (v8-style) - -```tsx -const [sorting, setSorting] = React.useState([]) - -const table = useTable({ - features, - columns, - data, - state: { sorting }, - onSortingChange: setSorting, -}) -``` - -`state` and `on*Change` must be paired. Without the callback the table cannot update React state, so toggling sort appears to do nothing. - -### External atom (v9 preferred for shared slices) - -```tsx -import { useCreateAtom } from '@tanstack/react-store' - -function MyTable() { - // Hoist or pass via context to share with queries / other components. - const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, - }) - - const table = useTable({ - features, - columns, - data, - atoms: { pagination: paginationAtom }, - // no state.pagination, no onPaginationChange needed - }) - - return -} -``` - -The atom IS the source of truth; `table.atoms.pagination` derives from it. - -### Server-side / manual mode - -```tsx -const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: 10, -}) -const dataQuery = useQuery({ - queryKey: ['rows', pagination], - queryFn: () => fetchPage(pagination), -}) - -const table = useTable({ - features: tableFeatures({ rowPaginationFeature }), - // no paginatedRowModel registered — server paginates - columns, - data: dataQuery.data?.rows ?? EMPTY, - rowCount: dataQuery.data?.rowCount, // server tells the table the total - state: { pagination }, - onPaginationChange: setPagination, - manualPagination: true, // ← tell the table to NOT re-paginate -}) -``` - -The same shape applies to `manualSorting`, `manualFiltering`, `manualGrouping`, `manualExpanding`. Without the flag, the table re-applies its client-side pipeline on top of already-prepared server data. - -## Common Mistakes - -### [CRITICAL] Passing both `state.` and `atoms.` - -Wrong: - -```tsx -// both ownership paths for the same slice -const paginationAtom = useCreateAtom({ pageIndex: 0, pageSize: 10 }) -const [pagination, setPagination] = React.useState(...) - -const table = useTable({ - features, - columns, - data, - state: { pagination }, // ignored - onPaginationChange: setPagination, - atoms: { pagination: paginationAtom }, // wins -}) -``` - -Correct: - -```tsx -// pick one ownership path per slice — here, external atoms -const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, -}) - -const table = useTable({ - features, - columns, - data, - atoms: { pagination: paginationAtom }, -}) -``` - -When both are supplied, the external atom wins silently. `state.pagination` becomes dead config and `setPagination` writes never reach the table. - -Source: docs/framework/react/guide/table-state.md; packages/table-core/src/core/table/constructTable.ts - -### [CRITICAL] Using external `state` without the matching `on*Change` callback - -Wrong: - -```tsx -const [sorting, setSorting] = React.useState([]) -const table = useTable({ - features, - columns, - data, - state: { sorting }, // no onSortingChange -}) -``` - -Correct: - -```tsx -const [sorting, setSorting] = React.useState([]) -const table = useTable({ - features, - columns, - data, - state: { sorting }, - onSortingChange: setSorting, -}) -``` - -The table keeps reading from `state.sorting`, so the UI looks stuck — sort toggles never make it back into React state. - -Source: docs/framework/react/guide/table-state.md; examples/react/basic-external-state/src/main.tsx - -### [HIGH] Using `initialState` to control or update state - -Wrong: - -```tsx -// updates to initialState are ignored after first render -function MyTable({ defaultSort }: { defaultSort: SortingState }) { - const table = useTable({ - features, - columns, - data, - initialState: { sorting: defaultSort }, // later changes never sync - }) -} -``` - -Correct: - -```tsx -function MyTable({ defaultSort }: { defaultSort: SortingState }) { - const [sorting, setSorting] = React.useState(defaultSort) - const table = useTable({ - features, - columns, - data, - state: { sorting }, - onSortingChange: setSorting, - }) -} -``` - -`initialState` is read once at construction to seed `baseAtoms`. Mutating it later does nothing. - -Source: docs/framework/vanilla/guide/table-state.md; docs/framework/react/guide/table-state.md - -### [HIGH] Writing to `table.baseAtoms.` while `atoms.` owns the slice - -Wrong: - -```ts -const paginationAtom = useCreateAtom({ - pageIndex: 0, - pageSize: 10, -}) -const table = useTable({ - features, - columns, - data, - atoms: { pagination: paginationAtom }, -}) - -table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 })) -// baseAtom updated, but table.atoms.pagination still reads from paginationAtom -``` - -Correct: - -```ts -// Write to the external atom directly, OR use the feature's setter API -paginationAtom.set((old) => ({ ...old, pageIndex: 0 })) -// or -table.setPageIndex(0) // setter writes through the slice's updater (atom-aware) -``` - -When an external atom owns a slice, `table.atoms.` derives from it — not from `baseAtoms`. Direct base-atom writes drift and never surface in the UI. - -Source: docs/framework/vanilla/guide/table-state.md; packages/table-core/src/core/table/constructTable.ts - -### [CRITICAL] Forgetting `manualSorting` / `manualFiltering` / `manualPagination` for server-side data - -Wrong: - -```tsx -// data is already paginated server-side, but table still slices it -const dataQuery = useQuery({ - queryKey: ['data', pagination], - queryFn: fetchPage, -}) -// features has paginatedRowModel registered -const table = useTable({ - features, - columns, - data: dataQuery.data?.rows ?? [], - rowCount: dataQuery.data?.rowCount, - atoms: { pagination: paginationAtom }, - // ❌ missing manualPagination: true -}) -``` - -Correct: - -```tsx -// Use features WITHOUT paginatedRowModel for fully server-side pagination -const table = useTable({ - features: tableFeatures({ rowPaginationFeature }), - columns, - data: dataQuery.data?.rows ?? [], - rowCount: dataQuery.data?.rowCount, - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -Without the manual flag, the table re-applies its client-side row models on top of already-prepared server data — wrong rows, broken page math, blank pages. - -Source: docs/framework/react/guide/table-state.md; packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts - -### [HIGH] Using `table.reset()` to clear externally owned state - -Wrong: - -```ts -// external atom keeps its current value; only baseAtoms reset -const sortingAtom = useCreateAtom([]) -const table = useTable({ - features, - columns, - data, - atoms: { sorting: sortingAtom }, -}) -table.reset() // sortingAtom is NOT cleared -``` - -Correct: - -```ts -// Use feature-specific reset — atom-aware -table.resetSorting() -// or, to clear the external atom specifically: -sortingAtom.set([]) -``` - -`table.reset()` only resets `baseAtoms` to `initialState`; slices owned by external atoms or external `state` are untouched. The atom split makes `reset()` less safe than v8. - -Source: docs/framework/vanilla/guide/table-state.md; packages/table-core/src/core/table/coreTablesFeature.utils.ts - -### [CRITICAL] Reimplementing what built-in setters provide - -Wrong: - -```ts -// Reimplements sorting state manually instead of using the API -const [sorting, setSorting] = useState([]) -const sortedData = useMemo(() => [...data].sort(/* ... */), [data, sorting]) -// then uses sortedData directly, bypassing the table -``` - -Correct: - -```ts -const table = useTable({ - features: tableFeatures({ - rowSortingFeature, - sortedRowModel: createSortedRowModel(), - sortFns, - }), - columns, - data, -}) -// table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler() -``` - -The setters honor reset behavior, multi-sort, internal invariants. Hand-rolled state loops skip all of that. - -Source: maintainer interview (Phase 4, 2026-05-17) - -## See also - -- `tanstack-table/setup` — how `features` (with row model factory and fn slots) is wired -- `tanstack-table/pagination`, `tanstack-table/sorting`, `tanstack-table/filtering` — feature-specific `manual*` and reset semantics -- `tanstack-table/migrate-v8-to-v9` — `table.getState()` → `table.store.state` / `table.atoms..get()` diff --git a/packages/table-core/skills/table-features/SKILL.md b/packages/table-core/skills/table-features/SKILL.md new file mode 100644 index 0000000000..57f91bd293 --- /dev/null +++ b/packages/table-core/skills/table-features/SKILL.md @@ -0,0 +1,153 @@ +--- +name: table-features +description: > + Register TanStack Table v9 tableFeatures, feature plugins, create*RowModel factories, and function registries in prerequisite order. Load when an option, state slice, or instance API is missing, or when choosing explicit features versus stockFeatures. +metadata: + type: sub-skill + library: '@tanstack/table-core' + library_version: '9.0.0-beta.38' +requires: ['core'] +sources: + - 'TanStack/table:docs/guide/row-models.md' + - 'TanStack/table:packages/table-core/src/types/TableFeatures.ts' + - 'TanStack/table:packages/table-core/src/features/stockFeatures.ts' + - 'TanStack/table:packages/table-core/src/core/table/constructTable.ts' +--- + +This skill builds on `core`. Read it first for the headless model and stable inputs. + +## Setup + + + +```ts +import { + aggregationFns, + columnGroupingFeature, + createFilteredRowModel, + createSortedRowModel, + columnFilteringFeature, + filterFns, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/table-core' + +export const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns, + columnGroupingFeature, + aggregationFns, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) +``` + +## Core Patterns + +### Register feature before its dependent slot + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +``` + +`tableFeatures` checks slot prerequisites and its inferred type gates APIs throughout the table. + +### Register named function slots with their features + +```ts +const features = tableFeatures({ + columnFilteringFeature, + filterFns, + rowSortingFeature, + sortFns, + columnGroupingFeature, + aggregationFns, +}) +``` + +`filterFns`, `sortFns`, and `aggregationFns` are feature slots, not table +options. They respectively require `columnFilteringFeature`, +`rowSortingFeature`, and `columnGroupingFeature`. A registered key can be used +as a typed string name; pass a function directly when no registry name is +needed. + +### Prefer explicit features + +```ts +const features = tableFeatures({ columnFilteringFeature }) +``` + +Use `stockFeatures` only for deliberate kitchen-sink or temporary migration behavior. + +## Common Mistakes + +### [CRITICAL] Calling an unregistered feature API + +Wrong: + +```ts +const features = tableFeatures({}) +table.setSorting([{ id: 'name', desc: false }]) +``` + +Correct: + +```ts +const features = tableFeatures({ rowSortingFeature }) +``` + +Optional feature state and APIs are installed only when their feature is registered. + +Source: `packages/table-core/src/core/table/constructTable.ts` + +### [HIGH] Omitting a slot prerequisite + +Wrong: + +```ts +const features = tableFeatures({ sortedRowModel: createSortedRowModel() }) +``` + +Correct: + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +``` + +The sorted model slot requires `rowSortingFeature`; the same rule applies to every mapped slot. + +Source: `packages/table-core/src/types/TableFeatures.ts#FeatureSlotPrereqs` + +### [MEDIUM] Shipping all features by default + +Wrong: + +```ts +const features = stockFeatures +``` + +Correct: + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +``` + +`stockFeatures` registers every stock plugin and processing slot, defeating v9's normal tree-shaking strategy. + +Source: `packages/table-core/src/features/stockFeatures.ts` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/types/TableFeatures.ts` for current slots and `FeatureSlotPrereqs`, and `src/features/stockFeatures.ts` for the stock inventory. diff --git a/packages/table-core/skills/typescript/SKILL.md b/packages/table-core/skills/typescript/SKILL.md new file mode 100644 index 0000000000..c30491d72b --- /dev/null +++ b/packages/table-core/skills/typescript/SKILL.md @@ -0,0 +1,126 @@ +--- +name: typescript +description: > + Preserve TanStack Table v9 inference with createColumnHelper, columns(), tableOptions, tableFeatures, and metaHelper. Load for ColumnDef errors, reusable tables, typed meta, named registries, or unnecessary manual feature generics. +metadata: + type: sub-skill + library: '@tanstack/table-core' + library_version: '9.0.0-beta.38' +requires: ['core', 'table-features'] +sources: + - 'TanStack/table:docs/guide/helpers.md' + - 'TanStack/table:docs/guide/column-defs.md' + - 'TanStack/table:docs/guide/table-and-column-meta.md' + - 'TanStack/table:packages/table-core/src/helpers' +--- + +This skill builds on `core` and `table-features`. Let the feature object and helpers carry types through userland. + +## Setup + +```ts +import { + createColumnHelper, + metaHelper, + tableFeatures, +} from '@tanstack/table-core' + +type Person = { id: string; age: number } +type ColumnMeta = { align?: 'start' | 'end' } +const features = tableFeatures({ columnMeta: metaHelper() }) +const helper = createColumnHelper() +export const columns = helper.columns([ + helper.accessor('age', { meta: { align: 'end' } }), +]) +``` + +## Core Patterns + +### Preserve heterogeneous accessor values + +```ts +const columns = helper.columns([ + helper.accessor('id', { header: 'ID' }), + helper.accessor('age', { header: 'Age' }), +]) +``` + +`columns()` preserves each accessor's `TValue` instead of widening the array. + +### Compose options through the helper + +```ts +const defaults = tableOptions({ + defaultColumn: { meta: { align: 'start' } }, +}) +``` + +Use `tableOptions` for reusable fragments; direct adapter options are enough for one-offs. + +## Common Mistakes + +### [HIGH] Erasing accessor value inference + +Wrong: + +```ts +const columns: ColumnDef[] = [ + helper.accessor('age', {}), +] +``` + +Correct: + +```ts +const columns = helper.columns([helper.accessor('age', {})]) +``` + +The broad annotation discards the accessor-specific `number` value type. + +Source: `docs/guide/helpers.md#createcolumnhelper` + +### [MEDIUM] Threading internal feature generics manually + +Wrong: + +```ts +type Features = TableFeatures +``` + +Correct: + +```ts +type Features = typeof features +``` + +The concrete registry is the source of feature-gated APIs and registry keys. + +Source: `packages/table-core/src/types/TableFeatures.ts` + +### [MEDIUM] Globally merging per-table meta + +Wrong: + +```ts +declare module '@tanstack/table-core' { + interface ColumnMeta { + align?: string + } +} +``` + +Correct: + +```ts +const features = tableFeatures({ + columnMeta: metaHelper<{ align?: 'start' | 'end' }>(), +}) +``` + +V9 type-only feature slots keep meta types scoped to the table factory. + +Source: `docs/guide/table-and-column-meta.md` + +## API Discovery + +Inspect `node_modules/@tanstack/table-core/src/helpers/` and the signatures re-exported by `src/index.ts`; avoid copying deep internal generic signatures into application code. diff --git a/packages/table-devtools/skills/compose-with-tanstack-devtools/SKILL.md b/packages/table-devtools/skills/compose-with-tanstack-devtools/SKILL.md deleted file mode 100644 index 8ab925bcb1..0000000000 --- a/packages/table-devtools/skills/compose-with-tanstack-devtools/SKILL.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -name: compose-with-tanstack-devtools -description: > - Internal implementation of TanStack Table devtools. Consumers should NOT - depend on `@tanstack/table-devtools` directly — install the framework-specific - adapter (`@tanstack/react-table-devtools`, `@tanstack/vue-table-devtools`, - `@tanstack/solid-table-devtools`, or `@tanstack/preact-table-devtools`) - instead. This skill explains the underlying solid-js-based implementation for - maintainers and contributors investigating internals. -type: composition -library: tanstack-table -library_version: '9.0.0-alpha.48' -requires: - - state-management -sources: - - TanStack/table:docs/devtools.md - - TanStack/table:packages/table-devtools/src/index.ts - - TanStack/table:packages/table-devtools/src/production.ts - - TanStack/table:packages/table-devtools/src/core.tsx - - TanStack/table:packages/table-devtools/src/tableTarget.ts ---- - -> **For consumers:** stop reading and install the framework adapter: -> -> - React: `@tanstack/react-table-devtools` -> - Vue: `@tanstack/vue-table-devtools` -> - Solid: `@tanstack/solid-table-devtools` -> - Preact: `@tanstack/preact-table-devtools` -> -> Angular, Lit, Svelte, and the vanilla `@tanstack/table-core` package **do not** currently ship table devtools adapters. There is no supported way to mount the devtools in those frameworks today. -> -> See the per-framework skills under `tanstack-table//compose-with-tanstack-devtools` for setup. This skill documents internals only. - -## Setup (internals) - -`@tanstack/table-devtools` is the shared core that every framework adapter wraps. It is **not** meant to be installed by application code. Its package.json declares dependencies on `solid-js`, `@tanstack/solid-store`, `@tanstack/devtools-ui`, `@tanstack/devtools-utils`, and `goober` — pulling it directly into a React/Vue/Preact app drags Solid's runtime into the bundle for no benefit, since the framework adapter already does the right thing. - -The package exports two surfaces: - -- **`@tanstack/table-devtools`** — `TableDevtoolsCore` resolves to a no-op when `process.env.NODE_ENV !== 'development'`. -- **`@tanstack/table-devtools/production`** — `TableDevtoolsCore` always resolves to the real implementation. - -Both surfaces also re-export the registration target store: - -```ts -import { - getTableDevtoolsTargets, - removeTableDevtoolsTarget, - setTableDevtoolsTarget, - subscribeTableDevtoolsTargets, - upsertTableDevtoolsTarget, -} from '@tanstack/table-devtools' -``` - -These are the functions the framework adapter hooks (`useTanStackTableDevtools`) call to register/unregister `Table` instances by id. - -## Architecture - -### Solid-based core - -The panel UI is built in Solid even when it is mounted inside a React/Vue/Preact tree: - -- `core.tsx` constructs `TableDevtoolsCore` via `constructCoreClass` from `@tanstack/devtools-utils/solid` and lazy-loads `./TableDevtools` (the Solid panel component). -- `useTableStore.ts` uses `@tanstack/solid-store` to back the panel's reactive state. -- Styling is handled with `goober` (atomic CSS-in-JS), keeping the bundle small and side-effect free. - -Framework adapters mount this Solid component inside the host framework via `@tanstack/devtools-utils` interop helpers: - -- `@tanstack/devtools-utils/react` (`createReactPlugin`) — used by `react-table-devtools` and `preact-table-devtools` plugin builders. -- `@tanstack/devtools-utils/solid` — used directly by `solid-table-devtools`. -- The vue adapter uses its own interop in `packages/vue-table-devtools/src/plugin.ts`. - -Each adapter exports `tableDevtoolsPlugin()` (returns a plugin descriptor for the `TanStackDevtools` host) and a `useTanStackTableDevtools(table, options?)` hook idiomatic to the framework. The hook's only job is to call `upsertTableDevtoolsTarget(...)` on mount/update and runs the returned cleanup on unmount. - -### Registration target store (`tableTarget.ts`) - -The target store is the bridge between framework-specific hooks and the Solid panel: - -- `upsertTableDevtoolsTarget({ table })` — register or update a table by `table.options.key`. -- `removeTableDevtoolsTarget(id)` — unregister on cleanup. -- `setTableDevtoolsTarget(...)` — register a table through the same key-based path (used internally). -- `subscribeTableDevtoolsTargets(listener)` — the Solid panel subscribes here to keep its selector in sync. -- `getTableDevtoolsTargets()` — snapshot for non-reactive reads. - -Devtools use `table.options.key` as the stable registration id. No adapter generates ids automatically. - -### Dev vs. production swap - -Every entrypoint (root and adapters) re-exports a development binding and a no-op binding. The default index file picks between them based on `process.env.NODE_ENV`; the `/production` entrypoint always returns the real binding. Bundlers (Vite, Rollup, webpack, esbuild) tree-shake the unused branch. - -## Common Mistakes - -### Importing `@tanstack/table-devtools` directly into a non-Solid framework app - -You will pay for Solid's runtime in your bundle and you will not get a working integration — there is no React/Vue/Preact-aware hook on this package. Install `@tanstack/-table-devtools` instead and import `tableDevtoolsPlugin` + `useTanStackTableDevtools` from there. - -### Confusing this with the framework adapter packages - -The framework-specific packages (`react-table-devtools`, etc.) re-export adapter hooks and plugins; this package re-exports only the registration target store and a Solid `TableDevtoolsCore` class. The names overlap (`tableDevtoolsPlugin` exists only on adapters; `TableDevtoolsCore` exists only here), so reading source code requires noting which package you are in. - -### Expecting Angular/Lit/Svelte/vanilla support - -There is no `@tanstack/angular-table-devtools`, `-lit-`, `-svelte-`, or `-table-core-devtools` package today. Document this gap when answering user questions — do not invent an import path. A future release may add adapters; until then the only way to inspect a table in those frameworks is to log `table.getState()` and `table.getRowModel()` manually. diff --git a/packages/table-devtools/skills/devtools/SKILL.md b/packages/table-devtools/skills/devtools/SKILL.md new file mode 100644 index 0000000000..c0f031166a --- /dev/null +++ b/packages/table-devtools/skills/devtools/SKILL.md @@ -0,0 +1,90 @@ +--- +name: devtools +description: > + Register TanStack Table targets and inspect options, state, features, columns, rows, and row models with @tanstack/table-devtools. Load for missing connections, required unique table options.key, target replacement/cleanup, or an explicit request for production Devtools entrypoints. +metadata: + type: core + library: '@tanstack/table-devtools' + library_version: '9.0.0-beta.40' +requires: + - '@tanstack/table-core#core' +sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/table-devtools/src/index.ts' + - 'TanStack/table:packages/table-devtools/src/tableTarget.ts' + - 'TanStack/table:packages/table-devtools/src/production.ts' +--- + +## Setup + +Framework adapters should use their lifecycle hook or injector. The framework-neutral registration primitive is: + +```ts +import { constructTable, tableFeatures } from '@tanstack/table-core' +import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings' +import { upsertTableDevtoolsTarget } from '@tanstack/table-devtools' + +const features = tableFeatures({ + coreReactivityFeature: storeReactivityBindings(), +}) + +const table = constructTable({ + key: 'users-table', + features, + columns: [], + data: [], +}) + +const cleanup = upsertTableDevtoolsTarget({ table }) +cleanup() +``` + +## Core Patterns + +### Give every live table a descriptive unique key + +The non-empty `options.key` is both registry identity and panel label. Use stable application identity, not an array index. + +### Let adapter lifecycles own registration + +React/Preact/Solid/Vue hooks and the Angular injector register and clean up at the correct time. Use core target functions only for unsupported frameworks or infrastructure code. + +### Keep ordinary Devtools development-only + +Default entrypoints export no-op panels/plugins outside development. Use `/production` only when the application explicitly chooses production inspection and its security/bundle implications. + +## Common Mistakes + +### HIGH Missing key skips registration + +Wrong: register a table whose `options.key` is absent or whitespace. + +Correct: set a stable value such as `key: 'billing-invoices'` before registration. + +The registry logs the missing-key error and returns without adding a target. + +Source: TanStack/table:packages/table-devtools/src/tableTarget.ts + +### HIGH Two tables share one key + +Wrong: use `key: 'table'` for simultaneous tables. + +Correct: use unique domain identities such as `users-table` and `orders-table`. + +Upserting a different table under an existing key replaces that registration. + +Source: TanStack/table:packages/table-devtools/src/tableTarget.ts + +### MEDIUM Production panel expected by default + +Wrong: debug why the normal entrypoint renders nothing in production. + +Correct: keep Devtools development-only unless production inspection was explicitly requested; then import the documented `/production` entrypoint. + +Default production exports intentionally use no-op implementations. + +Source: TanStack/table:docs/devtools.md + +## API Discovery + +Inspect `node_modules/@tanstack/table-devtools/src/index.ts`, `tableTarget.ts`, and `production.ts`. Framework registration belongs to the matching `@tanstack/-table-devtools` package. diff --git a/packages/vue-table-devtools/skills/devtools/SKILL.md b/packages/vue-table-devtools/skills/devtools/SKILL.md new file mode 100644 index 0000000000..a80fb48967 --- /dev/null +++ b/packages/vue-table-devtools/skills/devtools/SKILL.md @@ -0,0 +1,81 @@ +--- +name: devtools +description: > + Connect Vue Table v9 refs or instances to Devtools with tableDevtoolsPlugin and useTanStackTableDevtools. Load for missing targets, reactive table replacement, required options.key, enabled state, cleanup, or development gating. +metadata: + type: framework + library: '@tanstack/vue-table-devtools' + framework: vue + library_version: '9.0.0-beta.40' +requires: + - '@tanstack/table-core#core' + - '@tanstack/table-devtools#devtools' +sources: + - 'TanStack/table:docs/devtools.md' + - 'TanStack/table:packages/vue-table-devtools/src/index.ts' + - 'TanStack/table:packages/vue-table-devtools/src/useTanStackTableDevtools.ts' +--- + +This skill builds on @tanstack/table-core#core and @tanstack/table-devtools#devtools. + +## Setup + +```vue + + + +``` + +Mount the Vue TanStackDevtools host once with `tableDevtoolsPlugin({})`, following `docs/devtools.md`. + +## Hooks and Components + +The hook accepts a table or ref-like table and tracks replacement. Keep that reactive wrapper intact rather than unwrapping once. + +## Common Mistakes + +### HIGH Ref unwrapped before registration + +Wrong: read a ref once and register only that snapshot. + +Correct: pass the ref/maybe-ref shape accepted by the hook. + +The adapter watches the resolved table and cleans up when it changes. + +Source: TanStack/table:packages/vue-table-devtools/src/useTanStackTableDevtools.ts + +### HIGH Missing or duplicate key + +Wrong: omit `key` or reuse a key across mounted tables. + +Correct: use one descriptive stable key for each target. + +The registry skips keyless tables and replaces duplicate targets. + +Source: TanStack/table:packages/table-devtools/src/tableTarget.ts + +### MEDIUM Development gate mistaken for broken plugin + +Wrong: expect the default panel/plugin/hook in production. + +Correct: keep normal Devtools development-only unless explicitly importing `/production`. + +The package index selects no-op exports outside development. + +Source: TanStack/table:packages/vue-table-devtools/src/index.ts + +## API Discovery + +Inspect `node_modules/@tanstack/vue-table-devtools/src/index.ts` and `useTanStackTableDevtools.ts` for current ref handling. diff --git a/packages/vue-table-devtools/skills/vue/compose-with-tanstack-devtools/SKILL.md b/packages/vue-table-devtools/skills/vue/compose-with-tanstack-devtools/SKILL.md deleted file mode 100644 index 3b26ff2900..0000000000 --- a/packages/vue-table-devtools/skills/vue/compose-with-tanstack-devtools/SKILL.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -name: vue/compose-with-tanstack-devtools -description: > - Wire up TanStack Devtools for TanStack Table in Vue. Mount `TanStackDevtools` - with `tableDevtoolsPlugin({})` from the app root and call - `useTanStackTableDevtools(table)` inside ` - - -``` - -`tableDevtoolsPlugin({})` returns a plugin descriptor for the multi-panel TanStack Devtools UI. `useTanStackTableDevtools` is a Vue composable that registers/unregisters the table via a `watchEffect`, so it reacts to `MaybeRef` inputs for the table, name, and `enabled` option. - -## Patterns - -### Keying Tables - -Add a stable `key` option to the table options. Devtools use this key as both the registration id and the panel selector label. - -```ts -useTanStackTableDevtools(table) -``` - -The name may be reactive — pass a `Ref` to update the label live. - -### Multiple Tables - -Register multiple tables and the Table panel renders a selector. Name each one. - -```vue - -``` - -### Disabling Per Table - -`useTanStackTableDevtools` accepts an `enabled` option (reactive). When `false`, the registration is removed but the composable runs cleanly. - -```ts -import { ref } from 'vue' -const showTableDevtools = ref(false) - -useTanStackTableDevtools(table, { - enabled: showTableDevtools.value, -}) -``` - -### Production Builds - -The default `@tanstack/vue-table-devtools` entrypoint swaps to no-op implementations when `process.env.NODE_ENV !== 'development'`. To ship the real devtools to production, switch BOTH imports to the `/production` entrypoint: - -```ts -// main.ts -import { tableDevtoolsPlugin } from '@tanstack/vue-table-devtools/production' -``` - -```vue - -``` - -If you mix entrypoints (one from `/production`, one from the default), one side is a no-op in production and the panel will appear empty. - -### Conditional Devtools by Env - -A common pattern is to dynamically import the production entrypoint behind a feature flag: - -```ts -import { defineAsyncComponent } from 'vue' - -const TableDevtoolsRoot = defineAsyncComponent(async () => { - const { tableDevtoolsPlugin } = - await import('@tanstack/vue-table-devtools/production') - const { TanStackDevtools } = await import('@tanstack/vue-devtools') - return { - setup() { - return () => h(TanStackDevtools, { plugins: [tableDevtoolsPlugin({})] }) - }, - } -}) -``` - -## Common Mistakes - -### Forgetting to mount `TanStackDevtools` at the app root - -Calling `useTanStackTableDevtools(table)` alone does nothing visible — it only registers the table with the devtools target store. Without a `` (or `h(TanStackDevtools, ...)`) somewhere in the tree, there is no panel to render the registration. Symptom: composable runs without errors, no devtools button appears. - -### Importing devtools from the default path in a prod-only bundle - -If you only deploy production builds, `@tanstack/vue-table-devtools` resolves to no-op implementations. The plugin will mount, but the panel will be empty. Use `@tanstack/vue-table-devtools/production` if you want the real devtools available there. - -### Accidentally shipping devtools to end users via `/production` - -The flip side: importing from `/production` in your default app bundle means every visitor downloads and runs the devtools UI. That is usually not what you want. Restrict `/production` imports to dev/preview entrypoints or code-split them behind a flag. - -### Calling `useTanStackTableDevtools` outside `setup` - -The composable uses `watchEffect`, so it should run inside a component's `setup` / ` + + +``` + +## Core Patterns + +### Preserve Vue option shapes + +`useTable` accepts refs/computed values and unwraps them while watching dependencies. Keep `data`, controlled state, and other reactive options as refs or computed values; keep static columns/features stable. + +### Add a client row model explicitly + +```ts +import { + createSortedRowModel, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/vue-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) +``` + +The slot follows its prerequisite feature in the same call. + +## Common Mistakes + +### HIGH Flattening a ref into a snapshot + +Wrong: + +```ts +const table = useTable({ features, columns, data: data.value }) +``` + +Correct: + +```ts +const table = useTable({ features, columns, data }) +``` + +Passing `.value` captures one array instead of letting the adapter watch the ref. + +Source: `packages/vue-table/src/useTable.ts` + +### HIGH Using the v8 entrypoint + +Wrong: + +```ts +const table = useVueTable({ data, columns, getCoreRowModel: getCoreRowModel() }) +``` + +Correct: + +```ts +const table = useTable({ features, columns, data }) +``` + +V9 uses `useTable`; core processing is automatic and optional row models live in `tableFeatures`. + +Source: `docs/framework/vue/guide/migrating.md` + +### HIGH Assuming headless means prebuilt UI + +Wrong: + +```vue + +``` + +Correct: + +```vue + +``` + +The adapter renders definitions but owns no table component, CSS, or design-system integration. + +Source: `examples/vue/basic-use-table/src/App.tsx` + +## API Discovery + +Inspect `node_modules/@tanstack/vue-table/src/index.ts`, then `useTable.ts` and `FlexRender.ts`. Inspect core feature APIs in `node_modules/@tanstack/table-core/src/features//`. diff --git a/packages/vue-table/skills/migrate-v8-to-v9/SKILL.md b/packages/vue-table/skills/migrate-v8-to-v9/SKILL.md new file mode 100644 index 0000000000..666ecac329 --- /dev/null +++ b/packages/vue-table/skills/migrate-v8-to-v9/SKILL.md @@ -0,0 +1,185 @@ +--- +name: migrate-v8-to-v9 +description: > + Complete Vue v8-to-v9 migration reference: useTable, explicit features and row-model slots, ref/atom state, FlexRender shorthand, prototype methods, type generics, sorting, sizing, selection, and logical pinning. +metadata: + type: lifecycle + library: '@tanstack/vue-table' + framework: vue + library_version: '9.0.0-beta.40' +requires: + - '@tanstack/table-core#migrate-v8-to-v9' + - getting-started + - table-state +sources: + - 'TanStack/table:docs/framework/vue/guide/migrating.md' + - 'TanStack/table:packages/vue-table/src/index.ts' + - 'TanStack/table:examples/vue/basic-use-table' +--- + +Use this as the complete breaking-change checklist. V9 is the current API; do not stop after renaming the Vue composable. + +Framework prerequisite: Vue 3.2 or newer (`vue >=3.2`). + +## Recommended Migration Order + +1. Replace `useVueTable` with `useTable` while preserving reactive inputs. +2. Define explicit features, then move row models and registries into `tableFeatures`. +3. Update state reads, controlled ownership, and rendering. +4. Apply every shared API and type rename below. +5. Use `stockFeatures` only as a temporary audit bridge; explicit features are the production target. + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) +const data = ref(makeData()) +const table = useTable({ features, columns, data }) +``` + +## Construction and Feature Registration + +| v8 | v9 | +| -------------------------------------------- | ---------------------------------------------------------- | +| `useVueTable(options)` | `useTable(options)` | +| All features bundled | Required `features: tableFeatures({...})` | +| `getCoreRowModel()` option | Remove; core row model is automatic | +| `get*RowModel()` table options | `create*RowModel()` slots in `tableFeatures` | +| `sortingFns` table option | `sortFns` feature slot | +| `filterFns` / `aggregationFns` table options | Same-named feature slots | +| Top-level `onStateChange` | Per-slice callbacks, external atoms, or store subscription | + +Available feature imports are `columnFilteringFeature`, `globalFilteringFeature`, `rowSortingFeature`, `rowPaginationFeature`, `rowSelectionFeature`, `rowExpandingFeature`, `rowPinningFeature`, `columnPinningFeature`, `columnVisibilityFeature`, `columnOrderingFeature`, `columnSizingFeature`, `columnResizingFeature`, `columnGroupingFeature`, and `columnFacetingFeature`. APIs are feature-gated. Put every feature before its dependent slot in the same `tableFeatures` call. + +### Row-model mapping + +| v8 option | v9 slot and factory | +| -------------------------- | ------------------------------------------------------------------- | +| `getFilteredRowModel()` | `filteredRowModel: createFilteredRowModel()` after column filtering | +| `getSortedRowModel()` | `sortedRowModel: createSortedRowModel()` after row sorting | +| `getPaginationRowModel()` | `paginatedRowModel: createPaginatedRowModel()` after pagination | +| `getExpandedRowModel()` | `expandedRowModel: createExpandedRowModel()` after expanding | +| `getGroupedRowModel()` | `groupedRowModel: createGroupedRowModel()` after grouping | +| `getFacetedRowModel()` | `facetedRowModel: createFacetedRowModel()` after faceting | +| `getFacetedMinMaxValues()` | `facetedMinMaxValues: createFacetedMinMaxValues()` | +| `getFacetedUniqueValues()` | `facetedUniqueValues: createFacetedUniqueValues()` | + +Factories take no arguments. Register `filterFns`, `sortFns`, and `aggregationFns` as sibling feature slots. + +## Vue State Migration + +- Pass a `ref` or `computed` as `data`; the adapter unwraps and syncs it. Do not pass `data.value`, which is only a snapshot. A getter returning `data.value` is also supported. +- `table.getState().sorting` becomes the narrow `table.atoms.sorting.get()`. Use `table.store.get()` only for a full snapshot/debug output. +- Wrap atom reads in Vue `computed` when deriving template values. +- In JSX/render functions, `table.Subscribe` provides a fine-grained boundary. Pass the callback as the explicit `children` prop because Vue JSX element children become slots. +- Controlled refs need getter-backed state slices plus per-slice callbacks that resolve value-or-function `Updater`s. +- The top-level `onStateChange` is removed. Use per-slice callbacks, external atoms, or `table.store.subscribe` to observe everything. +- External atoms come from `@tanstack/vue-store` and are supplied through `atoms`. Never provide both `atoms.pagination` and `state.pagination`. +- `table.baseAtoms` is internal writable state; prefer feature APIs or external atoms. + +## Rendering and Composition + +| v8 | v9 target | +| -------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| `` | `` | +| Manual header/footer render props | `` / `:footer="footer"` | +| Repeated raw options | `tableOptions(...)` composition | +| Repeated table conventions | `createTableHook({ features, ... })` and pre-bound helpers | + +The old `render`/`props` FlexRender shape still compiles, but shorthand is the migration target. `createTableHook` is optional and intended for application-wide conventions. + +## Complete Shared Breaking-Change Map + +### Instance methods + +Row, cell, column, header, and related methods now live on shared prototypes and use `this`. Call them on their instances. Do not destructure them, pass them bare, or expect them in object spread, `Object.keys`, or JSON. Table methods are not affected. + +### Logical column pinning + +There are no `left`/`right` aliases in beta.38. + +| old | new | +| -------------------------------------------------------------- | ------------------------------------------------------------- | +| `columnPinning.left` / `.right` | `.start` / `.end` | +| `column.pin('left' \| 'right')` | `column.pin('start' \| 'end')` | +| `getIsPinned() === 'left' \| 'right'` | `'start' \| 'end'` | +| `row.getLeftVisibleCells()` / `getRightVisibleCells()` | `getStartVisibleCells()` / `getEndVisibleCells()` | +| `getLeftHeaderGroups()` / `getRightHeaderGroups()` | `getStartHeaderGroups()` / `getEndHeaderGroups()` | +| `getLeftFooterGroups()` / `getRightFooterGroups()` | `getStartFooterGroups()` / `getEndFooterGroups()` | +| `getLeftFlatHeaders()` / `getRightFlatHeaders()` | `getStartFlatHeaders()` / `getEndFlatHeaders()` | +| `getLeftLeafHeaders()` / `getRightLeafHeaders()` | `getStartLeafHeaders()` / `getEndLeafHeaders()` | +| `getLeftLeafColumns()` / `getRightLeafColumns()` | `getStartLeafColumns()` / `getEndLeafColumns()` | +| `getLeftVisibleLeafColumns()` / `getRightVisibleLeafColumns()` | `getStartVisibleLeafColumns()` / `getEndVisibleLeafColumns()` | +| `getLeftTotalSize()` / `getRightTotalSize()` | `getStartTotalSize()` / `getEndTotalSize()` | +| `column.getStart('left')` | `column.getStart('start')` | +| `column.getAfter('right')` | `column.getAfter('end')` | +| `column.getIndex('left' \| 'right')` | `column.getIndex('start' \| 'end')` | + +Use CSS `inset-inline-start`/`inset-inline-end`; logical names do not automatically set DOM direction. `columnResizeDirection` is unchanged. + +### Pinning, sizing, and resizing + +- `enablePinning` splits into `enableColumnPinning` and `enableRowPinning`. +- Interactive resizing requires `columnSizingFeature` plus `columnResizingFeature`; fixed sizing needs only the former. +- `columnSizingInfo` becomes `columnResizing`. +- `setColumnSizingInfo()` becomes `setColumnResizing()`. +- `onColumnSizingInfoChange` becomes `onColumnResizingChange`. + +### Sorting, rows, and selection + +| v8 | v9 | +| ------------------------------ | ----------------------------- | +| `sortingFn` | `sortFn` | +| `sortingFns` | `sortFns` | +| `getSortingFn()` | `getSortFn()` | +| `getAutoSortingFn()` | `getAutoSortFn()` | +| `SortingFn` / `SortingFns` | `SortFn` / `SortFns` | +| `row._getAllCellsByColumnId()` | `row.getAllCellsByColumnId()` | + +Other `_`-prefixed internals are removed, including `_getPinnedRows`, `_getFacetedRowModel`, `_getFacetedMinMaxValues`, and `_getFacetedUniqueValues`. + +`getIsSomeRowsSelected()` and `getIsSomePageRowsSelected()` now mean at least one, including all. Indeterminate UI must also check `!getIsAllRowsSelected()` or `!getIsAllPageRowsSelected()`. + +## TypeScript Migration + +- Add `TFeatures` first: `ColumnDef`, `Column`, `Row`, `Table`. +- Replace `createColumnHelper()` with `createColumnHelper()`; use `columnHelper.columns([...])` for nested-array inference. +- Use `StockFeatures` when `stockFeatures` is the configuration. +- Existing `TableMeta`/`ColumnMeta` declaration merging must add `TFeatures` first. Prefer per-table `tableMeta`/`columnMeta: metaHelper<...>()` slots. +- Replace global `FilterFns`, `SortFns`, `AggregationFns`, and `FilterMeta` augmentation with registry slots and `filterMeta: metaHelper<...>()`; registered keys become valid strings in column defs. +- `RowData` is restricted to records or arrays; prefer explicit object row types. + +## Common Migration Failures + +### HIGH: Renaming only the composable + +`useTable({ getSortedRowModel: ... })` is still a v8 configuration. Move the row model and its prerequisite feature into `tableFeatures`. + +### HIGH: Unwrapping refs before useTable + +Pass `data`, not `data.value`, or use a getter. Preserve the reactive source. + +### HIGH: Passing prototype methods bare + +Use `row.getValue('name')`, not `const read = row.getValue`; shallow copies also lose methods. + +### MEDIUM: JSX children as slots + +For `table.Subscribe`, use `children={(atoms) => ...}` explicitly. + +## Final Checklist + +- [ ] `useVueTable` is replaced with `useTable`; refs/computed inputs remain reactive. +- [ ] Features, row models, and registries are in `tableFeatures`; core row model is removed. +- [ ] State reads use atoms/computed or the store intentionally; `onStateChange` is removed. +- [ ] External atom and controlled state ownership do not overlap. +- [ ] FlexRender shorthand is adopted where applicable. +- [ ] Prototype methods, pinning, sizing/resizing, sorting, row, and selection changes are audited. +- [ ] Helpers, types, meta, registries, and `RowData` use v9 shapes. +- [ ] Temporary `stockFeatures` usage has an explicit removal plan. + +## API Discovery + +Inspect `node_modules/@tanstack/vue-table/src/index.ts` and `useTable.ts`; verify feature slots and exact beta APIs in `node_modules/@tanstack/table-core/src`. Do not reconstruct v9 from v8 memory. diff --git a/packages/vue-table/skills/table-state/SKILL.md b/packages/vue-table/skills/table-state/SKILL.md new file mode 100644 index 0000000000..1f3669633a --- /dev/null +++ b/packages/vue-table/skills/table-state/SKILL.md @@ -0,0 +1,191 @@ +--- +name: table-state +description: > + Read Vue-backed table.atoms/store in templates, computed, watch, or table.Subscribe; own slices with refs/computed or external Vue Store atoms; and apply updater callbacks while preserving reactive option shapes. +metadata: + type: framework + library: '@tanstack/vue-table' + framework: vue + library_version: '9.0.0-beta.40' +requires: + - '@tanstack/table-core#core' + - getting-started +sources: + - 'TanStack/table:docs/framework/vue/guide/table-state.md' + - 'TanStack/table:examples/vue/basic-external-state' + - 'TanStack/table:packages/vue-table/src/useTable.ts' +--- + +This skill builds on `@tanstack/table-core#core` and `getting-started`. Read them first for state ownership and Vue construction. + +## State Mental Model + +TanStack Table is primarily a state coordinator. Keep state internal unless another system needs to read, persist, or drive it. Without `initialState`, `atoms`, `state`, or `on[State]Change`, the table owns every registered slice. + +- `table.baseAtoms` are internal writable atoms created from resolved initial state. +- `table.atoms` are readonly derived atoms for the active owner of each registered slice. +- `table.store` combines those atoms into one readonly flat store. + +The Vue adapter backs atoms with refs/computed values and tracks reactive table options. Atom reads become reactive inside templates, `computed`, `watch`, or `table.Subscribe`; a read cached outside tracking is only a snapshot. State is feature-based, so a missing pagination atom or option means `rowPaginationFeature` was not registered. Keep `features` and `columns` stable; pass reactive `data` as a ref/computed instead of recreating arrays in table options. + +## Setup + +```ts +import { computed, ref } from 'vue' +import { + rowPaginationFeature, + tableFeatures, + useTable, +} from '@tanstack/vue-table' + +const features = tableFeatures({ rowPaginationFeature }) +const data = ref([{ name: 'Ada' }]) +const columns = [{ accessorKey: 'name' }] +const table = useTable({ features, columns, data }) +const pageIndex = computed(() => table.atoms.pagination.get().pageIndex) +``` + +Internal state is usually enough. Atom reads are reactive only when Vue evaluates them in a tracked template, computed, watch, or render boundary. + +## Core Patterns + +### Control a slice without losing updater semantics + +```ts +import { computed, ref } from 'vue' +import type { PaginationState } from '@tanstack/vue-table' + +const pagination = ref({ pageIndex: 0, pageSize: 20 }) +const controlledState = computed(() => ({ pagination: pagination.value })) +const onPaginationChange = ( + next: PaginationState | ((old: PaginationState) => PaginationState), +) => { + pagination.value = typeof next === 'function' ? next(pagination.value) : next +} +``` + +Pass `state: controlledState` and `onPaginationChange` to `useTable`. + +### Use Subscribe as a render boundary + +```tsx +table.Subscribe({ + children: (atoms) => {atoms.pagination.get().pageIndex + 1}, +}) +``` + +In Vue JSX, `children` is an explicit prop, not a slot child. + +## Choose State Ownership + +Use exactly one owner per slice: + +- Prefer internal state and feature methods for table-local behavior. +- Use `initialState` for starting/reset values; changing it later does not reset current state. +- Prefer a stable `@tanstack/vue-store` atom in `atoms` for cross-system ownership. Feature APIs update it directly, so omit `on[State]Change`. +- Use a ref/computed `state` value plus the matching callback for simple controlled state. Preserve the reactive wrapper and resolve raw values and updater functions. + +External atoms take precedence over external `state`, which syncs into the internal base atom. Do not configure multiple owners. The global v8 `onStateChange` option is gone; observe `table.store` if all state changes matter. + +## Initialize, Update, and Reset + +Use feature methods such as `setSorting`, `nextPage`, `toggleVisibility`, and `toggleSelected`. Direct `baseAtoms` writes are a rare escape hatch for internally owned state; write the external atom when it owns the slice. + +```ts +table.resetSorting() +table.resetPagination() +table.resetPagination(true) +``` + +Feature resets use `table.initialState` unless `true` requests the feature default and can update external owners. Core `table.reset()` only resets internal base atoms. Use slice types such as `PaginationState`; use `TableState` for the complete feature-inferred state. + +## Common Mistakes + +### HIGH Reading an untracked snapshot + +Wrong: + +```ts +const pageIndex = table.atoms.pagination.get().pageIndex +``` + +Correct: + +```ts +const pageIndex = computed(() => table.atoms.pagination.get().pageIndex) +``` + +The first read is current but does not make its consumer reactive. + +Source: `docs/framework/vue/guide/table-state.md` + +### HIGH Passing state.value once + +Wrong: + +```ts +const table = useTable({ + features, + columns, + data, + state: controlledState.value, +}) +``` + +Correct: + +```ts +const table = useTable({ features, columns, data, state: controlledState }) +``` + +The adapter watches the computed ref; a one-time `.value` breaks future option synchronization. + +Source: `packages/vue-table/src/useTable.ts` + +### HIGH Assigning updater functions as values + +Wrong: + +```ts +const onPaginationChange = (next) => { + pagination.value = next +} +``` + +Correct: + +```ts +const onPaginationChange = (next) => { + pagination.value = typeof next === 'function' ? next(pagination.value) : next +} +``` + +Table callbacks accept either a value or a function of the previous value. + +Source: `examples/vue/basic-external-state/src/App.tsx` + +### MEDIUM Supplying JSX children as a slot + +Wrong: + +```tsx + + {(atoms) => {atoms.pagination.get().pageIndex}} + +``` + +Correct: + +```tsx + {atoms.pagination.get().pageIndex}} +/> +``` + +The Vue adapter declares `Subscribe(props: { children })` and expects the explicit prop. + +Source: `packages/vue-table/src/useTable.ts` + +## API Discovery + +Inspect `node_modules/@tanstack/vue-table/src/useTable.ts` and `reactivity.ts`; inspect the exact state slice in the installed core feature directory. diff --git a/packages/vue-table/skills/vue/client-to-server/SKILL.md b/packages/vue-table/skills/vue/client-to-server/SKILL.md deleted file mode 100644 index 2723876065..0000000000 --- a/packages/vue-table/skills/vue/client-to-server/SKILL.md +++ /dev/null @@ -1,360 +0,0 @@ ---- -name: vue/client-to-server -description: > - Convert a client-side `@tanstack/vue-table` to server-side. Set `manualPagination` / - `manualSorting` / `manualFiltering` / `manualGrouping` / `manualExpanding` for whichever - slices the server owns; omit the matching row model factory from `tableFeatures` (don't ship - `paginatedRowModel` when the server paginates); supply `rowCount` so `table.getPageCount()` - works; own the relevant state slices via either external atoms (`@tanstack/vue-store` - `createAtom` + `options.atoms`) or `state` + `on[State]Change` with getter wrappers. Key any - data fetch (TanStack Query / fetch / rxResource alternative) on the controlled state and use - `placeholderData: keepPreviousData` (or equivalent) to avoid a 0-rows flash between pages. -type: lifecycle -library: tanstack-table -framework: vue -library_version: '9.0.0-alpha.48' -requires: - - state-management - - pagination - - filtering - - sorting -sources: - - examples/vue/basic-external-atoms/ - - examples/vue/basic-external-state/ - - examples/vue/with-tanstack-query/ - - docs/framework/vue/guide/table-state.md ---- - -# Client-to-Server Conversion (Vue) - -## Dependencies - -```bash -pnpm add @tanstack/vue-table @tanstack/vue-store -# Recommended fetch layer: -pnpm add @tanstack/vue-query -``` - -External atoms (`@tanstack/vue-store`) are recommended for server-managed slices. They cut the -`on[State]Change` plumbing entirely — the table writes to the atom; the query keys on the atom. - -## Setup — minimal server-paginated table - -```vue - -``` - -Source: `examples/vue/with-tanstack-query/src/App.tsx`, `examples/vue/basic-external-atoms/`. - -## Core Patterns - -### 1. The `manual*` flag + factory omission pair - -Pick which slices live server-side and flip the matching `manual*` flag. **Also omit the -matching row model factory from `tableFeatures`** — otherwise the table re-processes -server-processed rows. - -| Server owns | Set | Omit from `tableFeatures` | -| ----------- | ------------------------ | ------------------------- | -| Pagination | `manualPagination: true` | `paginatedRowModel` | -| Sorting | `manualSorting: true` | `sortedRowModel` | -| Filtering | `manualFiltering: true` | `filteredRowModel` | -| Grouping | `manualGrouping: true` | `groupedRowModel` | -| Expanding | `manualExpanding: true` | `expandedRowModel` | - -Column visibility / ordering / pinning / row selection are client-side state and stay as-is. - -### 2. `rowCount` so `getPageCount()` works - -Without `rowCount`, `getPageCount()` falls back to `Math.ceil(data.length / pageSize)` — i.e. -`1` if the server already paginated. The pager locks at "Page 1 of 1". - -```ts -useTable({ - features, - columns, - data: tableData, - rowCount: dataQuery.data.value?.rowCount, // or a stable ref/computed - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -If `rowCount` isn't immediately available, hold the last known value in a `ref` and update via -`watchEffect` so the pager doesn't reset to 0 during refetches. - -### 3. Two state-ownership shapes (pick one per slice) - -**External atoms (recommended with Query).** Table writes through to the atom. No -`on[State]Change` needed. - -```ts -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) -useTable({ - features, - columns, - data, - rowCount, - atoms: { pagination: paginationAtom }, - manualPagination: true, -}) -``` - -**Classic `state` + `on[State]Change` with getters.** Required when migrating from v8 or -integrating with existing Vue ref-based state. Each slice must be a getter so Vue tracks -`.value`. - -```ts -const pagination = ref({ pageIndex: 0, pageSize: 10 }) - -useTable({ - features, - columns, - data, - rowCount, - state: { - get pagination() { - return pagination.value - }, - }, - onPaginationChange: (u) => { - pagination.value = typeof u === 'function' ? u(pagination.value) : u - }, - manualPagination: true, -}) -``` - -**Precedence:** `atoms[slice]` > `state[slice]` > internal `baseAtoms[slice]`. Don't pass the -same slice through both — the atoms wins silently. - -### 4. Cache keys must include controlled state - -```ts -const sortingAtom = createAtom([]) -const paginationAtom = createAtom({ - pageIndex: 0, - pageSize: 10, -}) -const sorting = useSelector(sortingAtom) -const pagination = useSelector(paginationAtom) - -const dataQuery = useQuery(() => ({ - queryKey: [ - 'people', - { sorting: sorting.value, pagination: pagination.value }, - ], - queryFn: () => - fetchPeople({ sorting: sorting.value, pagination: pagination.value }), - placeholderData: keepPreviousData, -})) -``` - -If pagination/sort/filter aren't in `queryKey`, Query won't refetch when the user clicks a -pager button — the buttons "do nothing" from the user's POV. - -### 5. Mixed client + server features still work - -Column visibility, ordering, pinning, and row selection are client state — they don't depend -on the row model and continue to function with `manualPagination`/`manualSorting`/`manualFiltering`. -You can have a server-paginated table where the user pins or hides columns locally. - -## Common Mistakes - -### Forgetting `manualPagination` / `manualSorting` / `manualFiltering` (CRITICAL) - -The table double-processes server-processed rows. If the server returned page 2 of 50, -the table will paginate that 10-row slice again and show "Page 1 of 1". - -```ts -// ❌ -useTable({ - features, - columns, - data: serverPage.rows, - rowCount, - atoms: { pagination: paginationAtom }, - // missing: manualPagination: true -}) -``` - -### Leaving `paginatedRowModel` registered when server paginates (CRITICAL) - -```ts -// ❌ Factory ships for nothing AND the table re-paginates server-sliced data. -const features = tableFeatures({ - rowPaginationFeature, - paginatedRowModel: createPaginatedRowModel(), -}) - -// ✅ Omit the factory when the server paginates; keep the feature for its API. -const features = tableFeatures({ rowPaginationFeature }) -``` - -Same applies to `sortedRowModel`, `filteredRowModel`, `groupedRowModel`, `expandedRowModel` -when the server owns the slice. - -### Omitting `rowCount` (CRITICAL) - -`getPageCount()` returns `1` if the server already paginated. The pager UI locks at -"Page 1 of 1" and users can't navigate. - -### Passing `state.pagination` without `onPaginationChange` (CRITICAL) - -```ts -// ❌ table.setPageIndex(2) is a no-op — no writeback handler. -const pagination = ref({ pageIndex: 0, pageSize: 10 }) -useTable({ - features, - columns, - data, - rowCount, - state: { - get pagination() { - return pagination.value - }, - }, - manualPagination: true, -}) - -// ✅ Either pair `state` with `on[State]Change`, OR use `atoms`. -``` - -### Mixing `state.pagination` AND `atoms.pagination` for the same slice (HIGH) - -```ts -useTable({ - // ... - state: { - get pagination() { - return localPagination.value - }, - }, // silently ignored - onPaginationChange: setLocalPagination, // silently ignored - atoms: { pagination: paginationAtom }, // wins -}) -``` - -Atoms beat `state`; the `state` plumbing is dead but lingering in the code. Pick one mechanism. - -### Forgetting to include controlled state in `queryKey` (CRITICAL) - -```ts -// ❌ Never refetches when pagination changes. -useQuery(() => ({ - queryKey: ['people'], - queryFn: () => fetchPeople(pagination.value), -})) - -// ✅ -useQuery(() => ({ - queryKey: ['people', pagination.value], - queryFn: () => fetchPeople(pagination.value), -})) -``` - -### Skipping `placeholderData: keepPreviousData` (HIGH) - -Between fetches the table collapses to 0 rows, the row container collapses, and scroll -position jumps. `keepPreviousData` keeps the previous page visible during the refetch. - -### Passing a raw `ref` to `state.pagination` without a getter (CRITICAL — Vue-specific) - -```ts -// ❌ Vue can't track .value changes on the captured ref object. -state: { pagination: pagination } - -// ✅ -state: { get pagination() { return pagination.value } } -``` - -### Hand-rolling sort/page state instead of using the API (CRITICAL — #1 AI tell) - -```ts -// ❌ Manual state machine. -const pageIndex = ref(0) -const next = () => { - pageIndex.value++ - refetch() -} - -// ✅ Built-ins. -table.nextPage() -table.setPageIndex(0) -table.setSorting([{ id: 'age', desc: true }]) -table.setColumnFilters(/* ... */) -``` - -### "API missing" because the feature isn't in `features` (CRITICAL — v9-specific) - -Server-side pagination still needs `rowPaginationFeature` in `tableFeatures({...})` — that's -what surfaces `table.setPageIndex`, `table.nextPage`, `table.getPageCount`. The row model -factory (`paginatedRowModel`) is what you omit; the feature stays. - -```ts -const features = tableFeatures({ rowPaginationFeature }) // ✅ even with manualPagination -``` - -## See Also - -- `tanstack-table/vue/compose-with-tanstack-query` — the Query-specific wiring -- `tanstack-table/vue/compose-with-tanstack-store` — external atoms in depth -- `tanstack-table/vue/table-state` — getter rule, atom precedence -- `tanstack-table/table-core/pagination` — `manualPagination` / `rowCount` semantics diff --git a/packages/vue-table/skills/vue/compose-with-tanstack-form/SKILL.md b/packages/vue-table/skills/vue/compose-with-tanstack-form/SKILL.md deleted file mode 100644 index dd5214be0b..0000000000 --- a/packages/vue-table/skills/vue/compose-with-tanstack-form/SKILL.md +++ /dev/null @@ -1,369 +0,0 @@ ---- -name: vue/compose-with-tanstack-form -description: > - Editable cells with `@tanstack/vue-form` + `@tanstack/vue-table` v9. The table is the layout - primitive; the form owns state. Wire `data: form.state.values.data` (where `data` is the - array field) so the table reads from the form. In each column's `cell` renderer use a - `` slot to bind an input. Typing gotcha: if - your row type has recursive `subRows`, type the form rows as `Omit` — - TanStack Form's `DeepKeys` walks the recursion and hits TS2589. Subscribe to - `form.state.values.data.length` (not the whole array) to drive row add/remove re-renders. - Pair with TanStack Pacer for debounced filter inputs on the same screen. -type: composition -library: tanstack-table -framework: vue -library_version: '9.0.0-alpha.48' -requires: - - row-selection - - column-definitions -sources: - - examples/react/with-tanstack-form/ - - packages/vue-table/src/useTable.ts ---- - -# Compose @tanstack/vue-table with @tanstack/vue-form - -## Dependencies - -```bash -pnpm add @tanstack/vue-table @tanstack/vue-form -``` - -`@tanstack/vue-form` exposes `useForm` and the `` / `form.Field` component pattern. It -does NOT currently ship a `createFormHook` factory — that's a React-only convenience. In Vue, -you write component-local `` bindings directly. The canonical example for the -_shape_ of this pattern lives in `examples/react/with-tanstack-form/`; the Vue translation -maps the React `` to Vue's `` slot. - -## Setup — editable rows - -```vue - - - -``` - -Source: `examples/react/with-tanstack-form/src/main.tsx` (React canonical); pattern translated -to Vue's `` slot API. - -## Core Patterns - -### 1. Form owns the data, table renders it - -```ts -const form = useForm({ defaultValues: { data: makeData(100) } }) - -const table = useTable({ - features, - columns, - get data() { - return form.state.values.data - }, -}) -``` - -The table is a layout primitive — pagination, sorting, filtering on the form's data. The form -handles editing, validation, dirty tracking, submit. - -### 2. Cell bindings via `` - -In React-form, the convention is `` inside `cell:`. In Vue-form, -prefer rendering `` directly in the `