@tanstack/svelte-table 9.0.0-beta.5 → 9.0.0-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +2 -0
  2. package/dist/createTable.svelte.d.ts +0 -1
  3. package/dist/createTable.svelte.js +1 -2
  4. package/dist/createTableHook.svelte.d.ts +47 -13
  5. package/dist/createTableHook.svelte.js +16 -8
  6. package/dist/experimental-worker-plugin.d.ts +1 -0
  7. package/dist/experimental-worker-plugin.js +1 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/render-component.js +4 -0
  10. package/package.json +10 -5
  11. package/skills/create-table-hook/SKILL.md +168 -0
  12. package/skills/getting-started/SKILL.md +172 -0
  13. package/skills/migrate-v8-to-v9/SKILL.md +195 -0
  14. package/skills/table-state/SKILL.md +205 -0
  15. package/skills/with-tanstack-query/SKILL.md +147 -0
  16. package/skills/with-tanstack-virtual/SKILL.md +150 -0
  17. package/src/createTable.svelte.ts +1 -2
  18. package/src/createTableHook.svelte.ts +127 -21
  19. package/src/experimental-worker-plugin.ts +1 -0
  20. package/src/index.ts +1 -0
  21. package/skills/svelte/client-to-server/SKILL.md +0 -238
  22. package/skills/svelte/compose-with-tanstack-form/SKILL.md +0 -295
  23. package/skills/svelte/compose-with-tanstack-pacer/SKILL.md +0 -176
  24. package/skills/svelte/compose-with-tanstack-query/SKILL.md +0 -299
  25. package/skills/svelte/compose-with-tanstack-store/SKILL.md +0 -277
  26. package/skills/svelte/compose-with-tanstack-virtual/SKILL.md +0 -286
  27. package/skills/svelte/getting-started/SKILL.md +0 -340
  28. package/skills/svelte/migrate-v8-to-v9/SKILL.md +0 -256
  29. package/skills/svelte/production-readiness/SKILL.md +0 -256
  30. package/skills/svelte/table-state/SKILL.md +0 -441
@@ -0,0 +1,150 @@
1
+ ---
2
+ name: with-tanstack-virtual
3
+ description: >
4
+ 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.
5
+ metadata:
6
+ type: composition
7
+ library: '@tanstack/svelte-table'
8
+ framework: svelte
9
+ library_version: '9.0.0-beta.51'
10
+ requires:
11
+ - '@tanstack/table-core#core'
12
+ - getting-started
13
+ - table-state
14
+ sources:
15
+ - 'TanStack/table:docs/framework/svelte/guide/virtualization.md'
16
+ - 'TanStack/table:examples/svelte/virtualized-rows'
17
+ - 'TanStack/table:examples/svelte/virtualized-columns'
18
+ - 'TanStack/table:examples/svelte/virtualized-infinite-scrolling'
19
+ ---
20
+
21
+ 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.
22
+
23
+ ## Setup
24
+
25
+ ```svelte
26
+ <script lang="ts">
27
+ import { get } from 'svelte/store'
28
+ import { createVirtualizer } from '@tanstack/svelte-virtual'
29
+
30
+ let scrollElement = $state<HTMLDivElement>()
31
+ const rows = $derived(table.getRowModel().rows)
32
+ const rowVirtualizer = createVirtualizer({
33
+ count: rows.length,
34
+ getScrollElement: () => scrollElement ?? null,
35
+ estimateSize: () => 34,
36
+ getItemKey: (index) => rows[index]!.id,
37
+ overscan: 5,
38
+ })
39
+
40
+ // The store adapter does not track getter options. Push reactive inputs.
41
+ $effect(() => {
42
+ get(rowVirtualizer).setOptions({
43
+ count: rows.length,
44
+ getScrollElement: () => scrollElement ?? null,
45
+ })
46
+ })
47
+ </script>
48
+
49
+ <div
50
+ bind:this={scrollElement}
51
+ style="height: 500px; overflow: auto; position: relative"
52
+ >
53
+ <div style:height={`${$rowVirtualizer.getTotalSize()}px`}>
54
+ {#each $rowVirtualizer.getVirtualItems() as item (item.key)}
55
+ <div style={`position:absolute;transform:translateY(${item.start}px)`}>
56
+ {rows[item.index].id}
57
+ </div>
58
+ {/each}
59
+ </div>
60
+ </div>
61
+ ```
62
+
63
+ ## Core Patterns
64
+
65
+ ### Virtualize visible models
66
+
67
+ Use `table.getRowModel().rows` for rows and `table.getVisibleLeafColumns()` for columns. Recompute counts when filtering, sorting, expansion, or visibility changes.
68
+
69
+ ### Make CSS geometry agree with measurement
70
+
71
+ 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.
72
+
73
+ ### Fetch before the virtual end
74
+
75
+ 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.
76
+
77
+ ## Common Mistakes
78
+
79
+ ### HIGH Virtualizing raw data
80
+
81
+ Wrong:
82
+
83
+ ```ts
84
+ const rows = data
85
+ ```
86
+
87
+ Correct:
88
+
89
+ ```ts
90
+ const rows = $derived(table.getRowModel().rows)
91
+ ```
92
+
93
+ Raw data ignores Table filtering, sorting, grouping, expansion, and pagination decisions.
94
+
95
+ Source: `examples/svelte/virtualized-rows/src/App.svelte`
96
+
97
+ ### HIGH Expecting getter options to stay reactive
98
+
99
+ Wrong:
100
+
101
+ ```ts
102
+ const virtualizer = createVirtualizer({
103
+ get count() {
104
+ return rows.length
105
+ },
106
+ getScrollElement,
107
+ })
108
+ ```
109
+
110
+ Correct:
111
+
112
+ ```ts
113
+ const virtualizer = createVirtualizer({ count: rows.length, getScrollElement })
114
+ $effect(() => {
115
+ get(virtualizer).setOptions({
116
+ count: rows.length,
117
+ getScrollElement,
118
+ })
119
+ })
120
+ ```
121
+
122
+ `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(...)`.
123
+
124
+ Source: `examples/svelte/virtualized-rows/src/App.svelte`
125
+
126
+ ### HIGH Omitting the geometry contract
127
+
128
+ Wrong:
129
+
130
+ ```svelte
131
+ {#each rowVirtualizer.getVirtualItems() as item}<div>
132
+ {rows[item.index].id}
133
+ </div>{/each}
134
+ ```
135
+
136
+ Correct:
137
+
138
+ ```svelte
139
+ <div style:height={`${$rowVirtualizer.getTotalSize()}px`}>
140
+ <div style="position:absolute"></div>
141
+ </div>
142
+ ```
143
+
144
+ 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.
145
+
146
+ Source: `docs/framework/svelte/guide/virtualization.md`
147
+
148
+ ## API Discovery
149
+
150
+ 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.
@@ -51,7 +51,6 @@ export type SvelteTable<
51
51
  * const table = createTable(
52
52
  * {
53
53
  * features,
54
- * rowModels: {},
55
54
  * columns,
56
55
  * data,
57
56
  * },
@@ -73,7 +72,7 @@ export function createTable<
73
72
  // 1. Merge reactivity into options using mergeObjects (preserves getters)
74
73
  const mergedOptions = mergeObjects(tableOptions, {
75
74
  features: {
76
- coreReativityFeature: svelteReactivity(),
75
+ coreReactivityFeature: svelteReactivity(),
77
76
  ...tableOptions.features,
78
77
  },
79
78
  }) as TableOptions<TFeatures, TData>
@@ -376,6 +376,84 @@ export type AppSvelteTable<
376
376
  FlexRender: typeof FlexRenderSvelte
377
377
  }
378
378
 
379
+ export interface CreateTableHookResult<
380
+ TFeatures extends TableFeatures,
381
+ TTableComponents extends Record<string, ComponentType<any>>,
382
+ TCellComponents extends Record<string, ComponentType<any>>,
383
+ THeaderComponents extends Record<string, ComponentType<any>>,
384
+ > {
385
+ /** The features object that was passed to `createTableHook`. */
386
+ appFeatures: TFeatures
387
+ /**
388
+ * A column helper pre-bound to `TFeatures` and the registered components, so
389
+ * the cell/header/footer render props expose the bound components.
390
+ */
391
+ createAppColumnHelper: <TData extends RowData>() => AppColumnHelper<
392
+ TFeatures,
393
+ TData,
394
+ TCellComponents,
395
+ THeaderComponents
396
+ >
397
+ /**
398
+ * Creates a table with the `App*` wrapper components and registered
399
+ * `tableComponents` attached. `TData` is inferred from the `data` option.
400
+ */
401
+ createAppTable: <TData extends RowData, TSelected = TableState<TFeatures>>(
402
+ tableOptions: Omit<TableOptions<TFeatures, TData>, 'features'>,
403
+ selector?: (state: TableState<TFeatures>) => TSelected,
404
+ ) => AppSvelteTable<
405
+ TFeatures,
406
+ TData,
407
+ TSelected,
408
+ TTableComponents,
409
+ TCellComponents,
410
+ THeaderComponents
411
+ >
412
+ /**
413
+ * Reads the table provided by the nearest `<table.AppTable>`. This is the same
414
+ * extended instance `createAppTable` returns, so the `App*` components and your
415
+ * `tableComponents` are available on it.
416
+ *
417
+ * Pass `TSelected` to match the selector you gave `createAppTable`, so
418
+ * `table.state` is typed as the selected slice. It cannot be inferred
419
+ * automatically (context does not carry the provider's generics), so it
420
+ * defaults to the full table state, which is correct for the common case of
421
+ * `createAppTable` without a selector.
422
+ */
423
+ useTableContext: <
424
+ TData extends RowData = RowData,
425
+ TSelected = TableState<TFeatures>,
426
+ >() => AppSvelteTable<
427
+ TFeatures,
428
+ TData,
429
+ TSelected,
430
+ TTableComponents,
431
+ TCellComponents,
432
+ THeaderComponents
433
+ >
434
+ /**
435
+ * Reads the cell provided by the nearest `<table.AppCell>`, extended with your
436
+ * `cellComponents` and a context-bound `FlexRender`.
437
+ */
438
+ useCellContext: <TValue extends CellData = CellData>() => Cell<
439
+ TFeatures,
440
+ any,
441
+ TValue
442
+ > &
443
+ TCellComponents & { FlexRender: typeof FlexRenderSvelte }
444
+ /**
445
+ * Reads the header provided by the nearest `<table.AppHeader>` /
446
+ * `<table.AppFooter>`, extended with your `headerComponents` and a
447
+ * context-bound `FlexRender`.
448
+ */
449
+ useHeaderContext: <TValue extends CellData = CellData>() => Header<
450
+ TFeatures,
451
+ any,
452
+ TValue
453
+ > &
454
+ THeaderComponents & { FlexRender: typeof FlexRenderSvelte }
455
+ }
456
+
379
457
  // =============================================================================
380
458
  // createTableHook Factory
381
459
  // =============================================================================
@@ -404,12 +482,12 @@ export type AppSvelteTable<
404
482
  * rowPaginationFeature,
405
483
  * rowSortingFeature,
406
484
  * columnFilteringFeature,
407
- * }),
408
- * rowModels: {
409
485
  * paginatedRowModel: createPaginatedRowModel(),
410
- * sortedRowModel: createSortedRowModel(sortFns),
411
- * filteredRowModel: createFilteredRowModel(filterFns),
412
- * },
486
+ * sortedRowModel: createSortedRowModel(),
487
+ * filteredRowModel: createFilteredRowModel(),
488
+ * sortFns,
489
+ * filterFns,
490
+ * }),
413
491
  * tableComponents: { PaginationControls, RowCount },
414
492
  * cellComponents: { TextCell, NumberCell },
415
493
  * headerComponents: { SortIndicator, ColumnFilter },
@@ -431,7 +509,12 @@ export function createTableHook<
431
509
  TTableComponents,
432
510
  TCellComponents,
433
511
  THeaderComponents
434
- >) {
512
+ >): CreateTableHookResult<
513
+ TFeatures,
514
+ TTableComponents,
515
+ TCellComponents,
516
+ THeaderComponents
517
+ > {
435
518
  /**
436
519
  * Create a column helper pre-bound to the features and components configured in this table hook.
437
520
  * The cell, header, and footer contexts include pre-bound components (e.g., `cell.TextCell`).
@@ -455,9 +538,16 @@ export function createTableHook<
455
538
  * Use this in custom `tableComponents` passed to `createTableHook`.
456
539
  * TFeatures is already known from the createTableHook call.
457
540
  */
458
- function useTableContext<TData extends RowData = RowData>(): SvelteTable<
541
+ function useTableContext<
542
+ TData extends RowData = RowData,
543
+ TSelected = TableState<TFeatures>,
544
+ >(): AppSvelteTable<
459
545
  TFeatures,
460
- TData
546
+ TData,
547
+ TSelected,
548
+ TTableComponents,
549
+ TCellComponents,
550
+ THeaderComponents
461
551
  > {
462
552
  const table = getContext(tableContextKey)
463
553
 
@@ -468,7 +558,17 @@ export function createTableHook<
468
558
  )
469
559
  }
470
560
 
471
- return table as SvelteTable<TFeatures, TData>
561
+ // `<table.AppTable>` provides the extended table (the App* wrapper
562
+ // components and `tableComponents` are Object.assign-ed onto the same
563
+ // instance `createAppTable` returns), so this asserts the runtime shape.
564
+ return table as unknown as AppSvelteTable<
565
+ TFeatures,
566
+ TData,
567
+ TSelected,
568
+ TTableComponents,
569
+ TCellComponents,
570
+ THeaderComponents
571
+ >
472
572
  }
473
573
 
474
574
  /**
@@ -480,7 +580,8 @@ export function createTableHook<
480
580
  TFeatures,
481
581
  any,
482
582
  TValue
483
- > {
583
+ > &
584
+ TCellComponents & { FlexRender: typeof FlexRenderSvelte } {
484
585
  const cell = getContext(cellContextKey)
485
586
 
486
587
  if (!cell) {
@@ -490,7 +591,11 @@ export function createTableHook<
490
591
  )
491
592
  }
492
593
 
493
- return cell as Cell<TFeatures, any, TValue>
594
+ // `<table.AppCell>` Object.assign-es `cellComponents` and `FlexRender` onto
595
+ // the same cell instance it puts in context, so this asserts the runtime
596
+ // shape.
597
+ return cell as unknown as Cell<TFeatures, any, TValue> &
598
+ TCellComponents & { FlexRender: typeof FlexRenderSvelte }
494
599
  }
495
600
 
496
601
  /**
@@ -502,7 +607,8 @@ export function createTableHook<
502
607
  TFeatures,
503
608
  any,
504
609
  TValue
505
- > {
610
+ > &
611
+ THeaderComponents & { FlexRender: typeof FlexRenderSvelte } {
506
612
  const header = getContext(headerContextKey)
507
613
 
508
614
  if (!header) {
@@ -511,7 +617,10 @@ export function createTableHook<
511
617
  )
512
618
  }
513
619
 
514
- return header as Header<TFeatures, any, TValue>
620
+ // `<table.AppHeader>` / `<table.AppFooter>` Object.assign `headerComponents`
621
+ // and `FlexRender` onto the same header instance they put in context.
622
+ return header as unknown as Header<TFeatures, any, TValue> &
623
+ THeaderComponents & { FlexRender: typeof FlexRenderSvelte }
515
624
  }
516
625
 
517
626
  /**
@@ -527,10 +636,7 @@ export function createTableHook<
527
636
  TData extends RowData,
528
637
  TSelected = TableState<TFeatures>,
529
638
  >(
530
- tableOptions: Omit<
531
- TableOptions<TFeatures, TData>,
532
- 'features' | 'rowModels'
533
- >,
639
+ tableOptions: Omit<TableOptions<TFeatures, TData>, 'features'>,
534
640
  selector?: (state: TableState<TFeatures>) => TSelected,
535
641
  ): AppSvelteTable<
536
642
  TFeatures,
@@ -573,7 +679,7 @@ export function createTableHook<
573
679
  return AppTableSvelte(internal, { ...props })
574
680
  }) as Component<{ children: Snippet }>
575
681
 
576
- const AppCell = ((internal: any, { children, cell, ...rest }: any) => {
682
+ const AppCell = ((internal: any, { children, cell }: any) => {
577
683
  setContext(cellContextKey, cell)
578
684
  return AppCellSvelte(internal, {
579
685
  cell,
@@ -585,7 +691,7 @@ export function createTableHook<
585
691
  children: Snippet<[any]>
586
692
  }>
587
693
 
588
- const AppHeader = ((internal: any, { children, header, ...rest }: any) => {
694
+ const AppHeader = ((internal: any, { children, header }: any) => {
589
695
  setContext(headerContextKey, header)
590
696
  return AppHeaderSvelte(internal, {
591
697
  header,
@@ -598,7 +704,7 @@ export function createTableHook<
598
704
  }>
599
705
 
600
706
  // AppFooter reuses AppHeaderSvelte (footers use Header type in table-core)
601
- const AppFooter = ((internal: any, { children, header, ...rest }: any) => {
707
+ const AppFooter = ((internal: any, { children, header }: any) => {
602
708
  setContext(headerContextKey, header)
603
709
  return AppHeaderSvelte(internal, {
604
710
  header,
@@ -629,7 +735,7 @@ export function createTableHook<
629
735
  }
630
736
 
631
737
  return {
632
- appFeatures: defaultTableOptions.features as TFeatures,
738
+ appFeatures: defaultTableOptions.features,
633
739
  createAppColumnHelper,
634
740
  createAppTable,
635
741
  useTableContext,
@@ -0,0 +1 @@
1
+ export * from '@tanstack/table-core/experimental-worker-plugin'
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export type {
14
14
  AppSvelteTable,
15
15
  ComponentType,
16
16
  CreateTableHookOptions,
17
+ CreateTableHookResult,
17
18
  } from './createTableHook.svelte'
18
19
  export { createTableState } from './createTableState.svelte'
19
20
  export { default as FlexRender } from './FlexRender.svelte'
@@ -1,238 +0,0 @@
1
- ---
2
- name: svelte/client-to-server
3
- description: >
4
- Convert a client-side Svelte table to server-side (manual) modes. Toggle `manualPagination`,
5
- `manualSorting`, `manualFiltering`, `manualGrouping`, `manualExpanding` for whatever the server
6
- owns, drop the matching `rowModels` factories and `features` you no longer need, supply
7
- `rowCount` for the pager, then drive the request from `table.atoms.pagination` /
8
- `table.atoms.sorting` / etc. (or external atoms you own) — using rune-aware getters
9
- (`get data()`, `get rowCount()`) so the table re-syncs in `$effect.pre`. Svelte 5+ only.
10
- type: lifecycle
11
- library: tanstack-table
12
- framework: svelte
13
- library_version: '9.0.0-alpha.48'
14
- requires:
15
- - state-management
16
- - pagination
17
- - filtering
18
- - sorting
19
- - svelte/table-state
20
- sources:
21
- - TanStack/table:examples/svelte/basic-external-atoms/
22
- - TanStack/table:examples/svelte/basic-external-state/
23
- - TanStack/table:examples/svelte/with-tanstack-query/
24
- - TanStack/table:docs/framework/svelte/guide/table-state.md
25
- ---
26
-
27
- # Client → Server (Svelte)
28
-
29
- You have a working client-side table. The dataset is too big to ship to the browser, or it
30
- lives behind an API. You want sorting / filtering / pagination to run on the server while the
31
- table still feels the same in the UI.
32
-
33
- ## Mental model
34
-
35
- Each "manual mode" flag tells the table: **don't run this stage of the pipeline; trust the data
36
- you receive.** You can mix modes freely — manual pagination + client-side sorting on the
37
- already-paged window is perfectly valid for medium datasets.
38
-
39
- | Flag | Meaning | What you must provide |
40
- | ------------------ | -------------------------------------------- | ----------------------------------------------------------- |
41
- | `manualPagination` | Server owns slicing; do not paginate locally | `rowCount` (or `pageCount`) |
42
- | `manualSorting` | Server owns ordering | Sort the server query by `sorting` state |
43
- | `manualFiltering` | Server owns row filtering | Filter the server query by `columnFilters` / `globalFilter` |
44
- | `manualGrouping` | Server returns already-grouped rows | Pre-shaped data |
45
- | `manualExpanding` | Server resolves sub-rows | Server-provided sub-row tree |
46
-
47
- When a stage is manual, you can drop its row-model factory. `manualPagination: true` does not
48
- need `paginatedRowModel: createPaginatedRowModel()`.
49
-
50
- ## Step 1 — Identify what's moving server-side
51
-
52
- For a typical "search and paginate against a database" screen:
53
-
54
- - Pagination → server
55
- - Filtering (column filter inputs + a global search box) → server
56
- - Sorting → server (usually, since a partial page can't be sorted client-side meaningfully)
57
- - Selection / visibility / column ordering → still client
58
-
59
- So the table keeps `rowSelectionFeature` etc., drops `columnFilteringFeature` /
60
- `rowPaginationFeature` / `rowSortingFeature` _row models_ but keeps the _features_ so the
61
- state slices and UI APIs still exist.
62
-
63
- > Subtle point: keep the **feature** even if you drop the row model. The feature is what gives
64
- > you `column.getCanSort()`, `table.setPageIndex()`, `column.setFilterValue()` — all the
65
- > control-surface APIs. Dropping it kills the UI.
66
-
67
- ## Step 2 — Own the relevant state with external atoms
68
-
69
- External atoms make state portable: the data layer (a fetch / query / store) can read the
70
- same atoms the table writes to. Use `@tanstack/svelte-store`:
71
-
72
- ```ts
73
- import { createAtom, useSelector } from '@tanstack/svelte-store'
74
- import type {
75
- ColumnFiltersState,
76
- PaginationState,
77
- SortingState,
78
- } from '@tanstack/svelte-table'
79
-
80
- const paginationAtom = createAtom<PaginationState>({
81
- pageIndex: 0,
82
- pageSize: 10,
83
- })
84
- const sortingAtom = createAtom<SortingState>([])
85
- const filtersAtom = createAtom<ColumnFiltersState>([])
86
-
87
- // For Svelte markup that should react to changes:
88
- const pagination = useSelector(paginationAtom)
89
- const sorting = useSelector(sortingAtom)
90
- const filters = useSelector(filtersAtom)
91
- ```
92
-
93
- ## Step 3 — Configure the table
94
-
95
- ```svelte
96
- <script lang="ts">
97
- import {
98
- columnFilteringFeature,
99
- createTable,
100
- rowPaginationFeature,
101
- rowSortingFeature,
102
- tableFeatures,
103
- } from '@tanstack/svelte-table'
104
-
105
- const features = tableFeatures({
106
- columnFilteringFeature,
107
- rowPaginationFeature,
108
- rowSortingFeature,
109
- })
110
-
111
- // No row-model factories for these — server owns them.
112
- const table = createTable({
113
- features,
114
- rowModels: {},
115
- columns,
116
- get data() {
117
- return query.data?.rows ?? []
118
- },
119
- get rowCount() {
120
- return query.data?.rowCount
121
- },
122
- atoms: {
123
- pagination: paginationAtom,
124
- sorting: sortingAtom,
125
- columnFilters: filtersAtom,
126
- },
127
- manualPagination: true,
128
- manualSorting: true,
129
- manualFiltering: true,
130
- })
131
- </script>
132
- ```
133
-
134
- `rowCount` is what makes `table.getPageCount()` / `table.getCanNextPage()` correct under
135
- manual pagination. Without it the pager has no idea how many pages exist.
136
-
137
- ## Step 4 — Drive the fetch from those atoms
138
-
139
- Wire whatever data layer you use (TanStack Query, a raw `fetch`, SvelteKit `load`, etc.) to
140
- read the atoms. With TanStack Query:
141
-
142
- ```ts
143
- import { createQuery, keepPreviousData } from '@tanstack/svelte-query'
144
-
145
- const dataQuery = createQuery<{ rows: Array<Person>; rowCount: number }>(
146
- () => ({
147
- queryKey: ['people', pagination.current, sorting.current, filters.current],
148
- queryFn: () =>
149
- fetch('/api/people', {
150
- method: 'POST',
151
- body: JSON.stringify({
152
- pageIndex: pagination.current.pageIndex,
153
- pageSize: pagination.current.pageSize,
154
- sorting: sorting.current,
155
- filters: filters.current,
156
- }),
157
- }).then((r) => r.json()),
158
- placeholderData: keepPreviousData,
159
- }),
160
- )
161
- ```
162
-
163
- `placeholderData: keepPreviousData` is what kills the "rows blank for one tick on every
164
- page change" flash.
165
-
166
- ## Step 5 — Reset behavior
167
-
168
- When the user changes a filter, you usually want to jump back to page 0. The table does this
169
- automatically when client-side filtering owns the data, but with manual mode the data layer
170
- controls it. Simplest fix: explicitly reset.
171
-
172
- ```ts
173
- $effect(() => {
174
- // re-runs whenever filters.current identity changes
175
- filters.current
176
- table.setPageIndex(0)
177
- })
178
- ```
179
-
180
- Or wrap your filter `onChange` handlers to also call `table.setPageIndex(0)`.
181
-
182
- ## Step 6 — A note on global filtering
183
-
184
- If you also support `globalFilterFeature`, debounce the input. `column.setFilterValue` and
185
- `table.setGlobalFilter` fire per keystroke; without debouncing you fire one request per typed
186
- character. See the `compose-with-tanstack-pacer` skill for the pattern.
187
-
188
- ## Hybrid example — manual pagination only
189
-
190
- Sometimes you only paginate server-side and let the page-sized window sort/filter on the
191
- client.
192
-
193
- ```ts
194
- const table = createTable({
195
- features: tableFeatures({
196
- columnFilteringFeature,
197
- rowPaginationFeature,
198
- rowSortingFeature,
199
- }),
200
- rowModels: {
201
- filteredRowModel: createFilteredRowModel(filterFns), // client filters the page
202
- sortedRowModel: createSortedRowModel(sortFns), // client sorts the page
203
- },
204
- columns,
205
- get data() {
206
- return query.data?.rows ?? []
207
- },
208
- get rowCount() {
209
- return query.data?.rowCount
210
- },
211
- atoms: { pagination: paginationAtom },
212
- manualPagination: true,
213
- })
214
- ```
215
-
216
- Only the manual flag for the stage you're moving server-side.
217
-
218
- ## Common failure modes
219
-
220
- - **Forgot `rowCount`.** `table.getPageCount()` returns `-1`, the pager looks broken.
221
- - **Dropped the feature, not just the row model.** Lost `column.getCanSort()` and friends.
222
- Keep the feature when you still need its UI APIs; only drop the row-model factory.
223
- - **Both `state.pagination` and `atoms.pagination`.** Atoms silently win; the `on*Change`
224
- callback never fires.
225
- - **Re-creating atoms inside reactive blocks.** Atoms must be stable across renders. Declare
226
- them at module / component-init scope, not inside `$derived` or `$effect`.
227
- - **Forgetting to reset page on filter change.** Stay on page 12 of a now-2-page result set.
228
- - **Plain `data: query.data?.rows`.** No getter, no reactivity. Use `get data()`.
229
- - **Reimplementing pagination math.** `table.setPageIndex / nextPage / previousPage /
230
- firstPage / lastPage / setPageSize / getCanNextPage / getCanPreviousPage / getPageCount`
231
- already exist and respect manual mode.
232
-
233
- ## Related skills
234
-
235
- - `tanstack-table/svelte/compose-with-tanstack-query` — the same flow with a Query data layer.
236
- - `tanstack-table/svelte/compose-with-tanstack-pacer` — debouncing filter inputs.
237
- - `tanstack-table/svelte/compose-with-tanstack-store` — atom interop and per-slice subscription.
238
- - `tanstack-table/core/pagination` / `filtering` / `sorting` — feature deep dives.