From 4cca65ad3bf51ac4719f8f85cadf32182c6395c3 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 3 Aug 2026 21:01:09 -0500 Subject: [PATCH] fix: correct row behavior and expand feature guides --- docs/config.json | 19 +- docs/framework/alpine/guide/aggregation.md | 174 +++++++++-- .../alpine/guide/column-filtering.md | 16 +- .../framework/alpine/guide/custom-features.md | 2 +- docs/framework/alpine/guide/flex-render.md | 39 +++ .../alpine/guide/global-filtering.md | 14 +- docs/framework/alpine/guide/pagination.md | 12 +- docs/framework/alpine/guide/sorting.md | 6 +- docs/framework/angular/guide/aggregation.md | 174 +++++++++-- .../angular/guide/column-filtering.md | 16 +- .../angular/guide/custom-features.md | 2 +- .../guide/{rendering.md => flex-render.md} | 10 +- .../angular/guide/global-filtering.md | 14 +- docs/framework/angular/guide/migrating.md | 2 +- docs/framework/angular/guide/pagination.md | 16 +- docs/framework/angular/guide/sorting.md | 6 +- docs/framework/angular/quick-start.md | 2 +- docs/framework/ember/guide/aggregation.md | 174 +++++++++-- .../framework/ember/guide/column-filtering.md | 16 +- docs/framework/ember/guide/custom-features.md | 2 +- docs/framework/ember/guide/flex-render.md | 52 ++++ .../framework/ember/guide/global-filtering.md | 14 +- docs/framework/ember/guide/pagination.md | 16 +- docs/framework/ember/guide/sorting.md | 6 +- docs/framework/lit/guide/aggregation.md | 174 +++++++++-- docs/framework/lit/guide/column-filtering.md | 16 +- docs/framework/lit/guide/custom-features.md | 2 +- docs/framework/lit/guide/flex-render.md | 52 ++++ docs/framework/lit/guide/global-filtering.md | 14 +- docs/framework/lit/guide/pagination.md | 16 +- docs/framework/lit/guide/sorting.md | 6 +- docs/framework/octane/guide/aggregation.md | 182 ++++++++++-- .../octane/guide/column-filtering.md | 16 +- .../framework/octane/guide/custom-features.md | 2 +- docs/framework/octane/guide/flex-render.md | 31 ++ .../octane/guide/global-filtering.md | 14 +- docs/framework/octane/guide/pagination.md | 16 +- docs/framework/octane/guide/sorting.md | 6 +- docs/framework/preact/guide/aggregation.md | 182 ++++++++++-- .../preact/guide/column-filtering.md | 16 +- .../framework/preact/guide/custom-features.md | 2 +- docs/framework/preact/guide/flex-render.md | 59 ++++ .../preact/guide/global-filtering.md | 14 +- docs/framework/preact/guide/pagination.md | 16 +- docs/framework/preact/guide/sorting.md | 6 +- docs/framework/react/guide/aggregation.md | 182 ++++++++++-- .../framework/react/guide/column-filtering.md | 16 +- docs/framework/react/guide/custom-features.md | 2 +- docs/framework/react/guide/flex-render.md | 72 +++++ .../framework/react/guide/global-filtering.md | 14 +- docs/framework/react/guide/pagination.md | 16 +- docs/framework/react/guide/sorting.md | 6 +- docs/framework/solid/guide/aggregation.md | 182 ++++++++++-- .../framework/solid/guide/column-filtering.md | 16 +- docs/framework/solid/guide/custom-features.md | 2 +- docs/framework/solid/guide/flex-render.md | 53 ++++ .../framework/solid/guide/global-filtering.md | 14 +- docs/framework/solid/guide/pagination.md | 16 +- docs/framework/solid/guide/sorting.md | 6 +- docs/framework/svelte/guide/aggregation.md | 174 +++++++++-- .../svelte/guide/column-filtering.md | 16 +- .../framework/svelte/guide/custom-features.md | 2 +- docs/framework/svelte/guide/flex-render.md | 73 +++++ .../svelte/guide/global-filtering.md | 14 +- docs/framework/svelte/guide/pagination.md | 16 +- docs/framework/svelte/guide/sorting.md | 6 +- docs/framework/vanilla/guide/aggregation.md | 174 +++++++++-- docs/framework/vanilla/guide/flex-render.md | 35 +++ docs/framework/vue/guide/aggregation.md | 174 +++++++++-- docs/framework/vue/guide/column-filtering.md | 16 +- docs/framework/vue/guide/custom-features.md | 2 +- docs/framework/vue/guide/flex-render.md | 51 ++++ docs/framework/vue/guide/global-filtering.md | 14 +- docs/framework/vue/guide/pagination.md | 16 +- docs/framework/vue/guide/sorting.md | 6 +- docs/guide/aggregation.md | 277 ------------------ docs/guide/client-side-vs-server-side.md | 184 ++++++++++++ docs/guide/column-defs.md | 6 +- docs/guide/data.md | 8 +- docs/guide/features.md | 134 +++++++++ .../index/interfaces/ColumnDef_RowSorting.md | 2 +- .../index/interfaces/TableOptions_Core.md | 2 +- .../interfaces/TableOptions_RowPagination.md | 2 +- .../index/interfaces/TableOptions_Rows.md | 2 +- examples/react/kitchen-sink/package.json | 1 + examples/react/kitchen-sink/src/index.css | 41 +++ .../react/kitchen-sink/src/routes/index.tsx | 277 +++++++++++++++--- .../kitchen-sink/tests/e2e/smoke.spec.ts | 39 +++ .../src/core/rows/coreRowsFeature.types.ts | 2 +- .../cellSelectionFeature.utils.ts | 29 +- .../column-grouping/createGroupedRowModel.ts | 27 +- .../rowPaginationFeature.types.ts | 2 +- .../row-sorting/rowSortingFeature.types.ts | 2 +- .../cellSelectionFeature.test.ts | 39 +++ .../createGroupedRowModel.test.ts | 21 +- pnpm-lock.yaml | 3 + 96 files changed, 3146 insertions(+), 986 deletions(-) create mode 100644 docs/framework/alpine/guide/flex-render.md rename docs/framework/angular/guide/{rendering.md => flex-render.md} (97%) create mode 100644 docs/framework/ember/guide/flex-render.md create mode 100644 docs/framework/lit/guide/flex-render.md create mode 100644 docs/framework/octane/guide/flex-render.md create mode 100644 docs/framework/preact/guide/flex-render.md create mode 100644 docs/framework/react/guide/flex-render.md create mode 100644 docs/framework/solid/guide/flex-render.md create mode 100644 docs/framework/svelte/guide/flex-render.md create mode 100644 docs/framework/vanilla/guide/flex-render.md create mode 100644 docs/framework/vue/guide/flex-render.md delete mode 100644 docs/guide/aggregation.md create mode 100644 docs/guide/client-side-vs-server-side.md create mode 100644 docs/guide/features.md diff --git a/docs/config.json b/docs/config.json index 89e0d0161c..67200fbbd9 100644 --- a/docs/config.json +++ b/docs/config.json @@ -24,8 +24,7 @@ "label": "angular", "children": [ { "label": "Quick Start", "to": "framework/angular/quick-start" }, - { "label": "Migrating to V9", "to": "framework/angular/guide/migrating" }, - { "label": "Rendering components", "to": "framework/angular/guide/rendering" } + { "label": "Migrating to V9", "to": "framework/angular/guide/migrating" } ] }, { @@ -94,11 +93,12 @@ { "label": "Core Guides", "children": [ + { "label": "Features", "to": "guide/features" }, { "label": "Data", "to": "guide/data" }, + { "label": "Client-Side vs Server-Side", "to": "guide/client-side-vs-server-side" }, { "label": "Column Definitions", "to": "guide/column-defs" }, { "label": "Table Instance", "to": "guide/tables" }, { "label": "Row Models", "to": "guide/row-models" }, - { "label": "Aggregation", "to": "guide/aggregation" }, { "label": "Worker Row Models (Experimental)", "to": "guide/worker-row-models" }, { "label": "Rows", "to": "guide/rows" }, { "label": "Cells", "to": "guide/cells" }, @@ -120,6 +120,7 @@ "label": "Composable Tables (createTableHook)", "to": "framework/alpine/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/alpine/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/alpine/guide/custom-features" @@ -131,6 +132,7 @@ "children": [ { "label": "Table State", "to": "framework/angular/guide/table-state" }, { "label": "Composable Tables (createTableHook)", "to": "framework/angular/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/angular/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/angular/guide/custom-features" } ] }, @@ -139,6 +141,7 @@ "children": [ { "label": "Table State", "to": "framework/ember/guide/table-state" }, { "label": "Composable Tables (createTableHook)", "to": "framework/ember/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/ember/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/ember/guide/custom-features" } ] }, @@ -147,6 +150,7 @@ "children": [ { "label": "Table State", "to": "framework/lit/guide/table-state" }, { "label": "Composable Tables (createTableHook)", "to": "framework/lit/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/lit/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/lit/guide/custom-features" } ] }, @@ -156,6 +160,7 @@ { "label": "Table State", "to": "framework/react/guide/table-state" }, { "label": "Composable Tables (createTableHook)", "to": "framework/react/guide/composable-tables" }, { "label": "Table Context", "to": "framework/react/guide/table-context" }, + { "label": "FlexRender", "to": "framework/react/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/react/guide/custom-features" } ] }, @@ -165,6 +170,7 @@ { "label": "Table State", "to": "framework/preact/guide/table-state" }, { "label": "Composable Tables (createTableHook)", "to": "framework/preact/guide/composable-tables" }, { "label": "Table Context", "to": "framework/preact/guide/table-context" }, + { "label": "FlexRender", "to": "framework/preact/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/preact/guide/custom-features" } ] }, @@ -174,6 +180,7 @@ { "label": "Table State", "to": "framework/octane/guide/table-state" }, { "label": "Composable Tables (createTableHook)", "to": "framework/octane/guide/composable-tables" }, { "label": "Table Context", "to": "framework/octane/guide/table-context" }, + { "label": "FlexRender", "to": "framework/octane/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/octane/guide/custom-features" } ] }, @@ -182,6 +189,7 @@ "children": [ { "label": "Table State", "to": "framework/solid/guide/table-state" }, { "label": "Composable Tables (createTableHook)", "to": "framework/solid/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/solid/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/solid/guide/custom-features" } ] }, @@ -190,6 +198,7 @@ "children": [ { "label": "Table State", "to": "framework/svelte/guide/table-state" }, { "label": "Composable Tables (createTableHook)", "to": "framework/svelte/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/svelte/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/svelte/guide/custom-features" } ] }, @@ -198,13 +207,15 @@ "children": [ { "label": "Table State", "to": "framework/vue/guide/table-state" }, { "label": "Composable Tables (createTableHook)", "to": "framework/vue/guide/composable-tables" }, + { "label": "FlexRender", "to": "framework/vue/guide/flex-render" }, { "label": "Custom Plugins", "to": "framework/vue/guide/custom-features" } ] }, { "label": "vanilla", "children": [ - { "label": "Table State", "to": "framework/vanilla/guide/table-state" } + { "label": "Table State", "to": "framework/vanilla/guide/table-state" }, + { "label": "FlexRender", "to": "framework/vanilla/guide/flex-render" } ] } ] diff --git a/docs/framework/alpine/guide/aggregation.md b/docs/framework/alpine/guide/aggregation.md index 521e675ed5..0100de15cf 100644 --- a/docs/framework/alpine/guide/aggregation.md +++ b/docs/framework/alpine/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -50,6 +47,11 @@ const table = createTable({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; @@ -69,6 +71,28 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default @@ -90,13 +114,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -119,17 +156,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/alpine/guide/column-filtering.md b/docs/framework/alpine/guide/column-filtering.md index f0b0fad522..53d4ccf5b0 100644 --- a/docs/framework/alpine/guide/column-filtering.md +++ b/docs/framework/alpine/guide/column-filtering.md @@ -56,19 +56,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -343,7 +333,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/alpine/guide/custom-features.md b/docs/framework/alpine/guide/custom-features.md index 3b685efc5c..5fc3617c02 100644 --- a/docs/framework/alpine/guide/custom-features.md +++ b/docs/framework/alpine/guide/custom-features.md @@ -405,4 +405,4 @@ Alpine.data('table', () => { ### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in `Alpine.reactive`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in `Alpine.reactive`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/alpine/guide/flex-render.md b/docs/framework/alpine/guide/flex-render.md new file mode 100644 index 0000000000..fc9727a5fc --- /dev/null +++ b/docs/framework/alpine/guide/flex-render.md @@ -0,0 +1,39 @@ +--- +title: FlexRender (Alpine) Guide +--- + +Alpine column definitions commonly contain strings or functions that return HTML strings for `header`, `cell`, `footer`, and `aggregatedCell`. The rendering utilities resolve those definitions with the correct table context. + +## `FlexRender` vs `flexRender` + +`FlexRender` is the recommended table-aware wrapper. Pass exactly one `cell`, `header`, or `footer` object: + +```html + + + +``` + +Import `FlexRender` from `@tanstack/alpine-table` and expose it to the Alpine data scope, or use `table.FlexRender` on a table created by the adapter. For footer groups, call `FlexRender({ footer: header })`. + +For cells, `FlexRender` selects `aggregatedCell` for aggregated rows, falls back to `cell`, and returns `null` for grouping placeholders. + +`flexRender` is the lower-level function for a definition and context: + +```ts +import { flexRender } from '@tanstack/alpine-table' + +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` + +It invokes function renderers and passes non-functions through unchanged. It does not select grouped-cell renderers or suppress grouping placeholders. + +Because `x-html` inserts HTML, only render markup produced by code you trust. Escape or sanitize untrusted data before including it in a renderer result. Use `x-text` or normal DOM bindings instead when a renderer only needs to display text. + +Placeholder headers remain the template's layout decision. Check `header.isPlaceholder` unless a spanning-header layout intentionally renders that placeholder. diff --git a/docs/framework/alpine/guide/global-filtering.md b/docs/framework/alpine/guide/global-filtering.md index 81ec204372..4bd8ff27c6 100644 --- a/docs/framework/alpine/guide/global-filtering.md +++ b/docs/framework/alpine/guide/global-filtering.md @@ -51,19 +51,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/alpine/guide/pagination.md b/docs/framework/alpine/guide/pagination.md index 5196c7ff40..d766d126be 100644 --- a/docs/framework/alpine/guide/pagination.md +++ b/docs/framework/alpine/guide/pagination.md @@ -46,17 +46,9 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 100,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Pagination Row Model diff --git a/docs/framework/alpine/guide/sorting.md b/docs/framework/alpine/guide/sorting.md index 35c405965e..1953bc996e 100644 --- a/docs/framework/alpine/guide/sorting.md +++ b/docs/framework/alpine/guide/sorting.md @@ -157,7 +157,9 @@ const table = createTable({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -421,7 +423,7 @@ const table = createTable({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```ts const columns = [ diff --git a/docs/framework/angular/guide/aggregation.md b/docs/framework/angular/guide/aggregation.md index 4bd4ba097f..8b61a0797a 100644 --- a/docs/framework/angular/guide/aggregation.md +++ b/docs/framework/angular/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -50,6 +47,11 @@ const table = injectTable(() => ({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; @@ -69,6 +71,28 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default @@ -90,13 +114,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -119,17 +156,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/angular/guide/column-filtering.md b/docs/framework/angular/guide/column-filtering.md index 8d6c0880a9..32a9a09351 100644 --- a/docs/framework/angular/guide/column-filtering.md +++ b/docs/framework/angular/guide/column-filtering.md @@ -58,19 +58,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -330,7 +320,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/angular/guide/custom-features.md b/docs/framework/angular/guide/custom-features.md index 0fae885310..d88a63c9be 100644 --- a/docs/framework/angular/guide/custom-features.md +++ b/docs/framework/angular/guide/custom-features.md @@ -375,4 +375,4 @@ export class App { #### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `signal`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `signal`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/angular/guide/rendering.md b/docs/framework/angular/guide/flex-render.md similarity index 97% rename from docs/framework/angular/guide/rendering.md rename to docs/framework/angular/guide/flex-render.md index 673ed5db11..850d75f8fe 100644 --- a/docs/framework/angular/guide/rendering.md +++ b/docs/framework/angular/guide/flex-render.md @@ -1,10 +1,16 @@ --- -title: Rendering components +title: FlexRender (Angular) Guide --- The `@tanstack/angular-table` adapter provides structural directives and dependency injection primitives for rendering table content in Angular templates. -## FlexRender +## `FlexRender` vs `flexRender` + +Angular uses the names for two related template concepts: + +- `FlexRender` is the exported tuple of rendering directives. Add it to a component's `imports` array. +- `*flexRender` is the lower-level structural directive. +- `*flexRenderCell`, `*flexRenderHeader`, and `*flexRenderFooter` are table-aware shorthand selectors included in `FlexRender`. `FlexRender` is the rendering primitive. It is exported as a tuple of two directives: diff --git a/docs/framework/angular/guide/global-filtering.md b/docs/framework/angular/guide/global-filtering.md index ae01fd167e..7078218529 100644 --- a/docs/framework/angular/guide/global-filtering.md +++ b/docs/framework/angular/guide/global-filtering.md @@ -52,19 +52,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/angular/guide/migrating.md b/docs/framework/angular/guide/migrating.md index ba35523126..c394790f1b 100644 --- a/docs/framework/angular/guide/migrating.md +++ b/docs/framework/angular/guide/migrating.md @@ -726,7 +726,7 @@ When a component is rendered through the FlexRender directives, you can also acc If you need to render an Angular component with explicit configuration (custom `inputs`, `outputs`, `injector`, and Angular v20+ creation-time `bindings`/`directives`), return a `flexRenderComponent(Component, options)` wrapper from your column definition. -For complete rendering details (including component rendering, `TemplateRef`, `flexRenderComponent`, and context helpers), see the [Rendering components Guide](./rendering.md). +For complete rendering details (including component rendering, `TemplateRef`, `flexRenderComponent`, and context helpers), see the [FlexRender Guide](./flex-render). --- diff --git a/docs/framework/angular/guide/pagination.md b/docs/framework/angular/guide/pagination.md index 5d2eae8a7b..5d12b7f24d 100644 --- a/docs/framework/angular/guide/pagination.md +++ b/docs/framework/angular/guide/pagination.md @@ -47,21 +47,15 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 100,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Should You Use Virtualization Instead? -Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. #### Pagination Row Model diff --git a/docs/framework/angular/guide/sorting.md b/docs/framework/angular/guide/sorting.md index 4cd497e19b..f0191ca8ba 100644 --- a/docs/framework/angular/guide/sorting.md +++ b/docs/framework/angular/guide/sorting.md @@ -153,7 +153,9 @@ readonly table = injectTable(() => ({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -413,7 +415,7 @@ readonly table = injectTable(() => ({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```ts const columns = [ diff --git a/docs/framework/angular/quick-start.md b/docs/framework/angular/quick-start.md index eb5bb95245..010f70e2d2 100644 --- a/docs/framework/angular/quick-start.md +++ b/docs/framework/angular/quick-start.md @@ -116,7 +116,7 @@ A few things to note: - `tableFeatures({})` declares which optional features the table uses. Registering only what you need keeps bundles small and gives TypeScript accurate types for the table instance. - `injectTable` must be called in an injection context. Its initializer re-runs when Angular signals read inside it change (like `this.data()` here), and the adapter syncs the table options. -- The `FlexRender` directives (`*flexRenderHeader`, `*flexRenderCell`, `*flexRenderFooter`) render the `header`, `cell`, and `footer` definitions from your columns, whether they are plain values, templates, or components. See the [Rendering Guide](./guide/rendering.md) for `flexRenderComponent` and render context helpers. +- The `FlexRender` directives (`*flexRenderHeader`, `*flexRenderCell`, `*flexRenderFooter`) render the `header`, `cell`, and `footer` definitions from your columns, whether they are plain values, templates, or components. See the [FlexRender Guide](./guide/flex-render) for `flexRenderComponent` and render context helpers. - The `key` option is optional unless you use the [TanStack Table Devtools](../../devtools). The devtools identify tables by `key`, and you register a table with `injectTanStackTableDevtools` from `@tanstack/angular-table-devtools`. See the full [Basic injectTable example](./examples/basic-inject-table) for a runnable version with more columns and a footer. diff --git a/docs/framework/ember/guide/aggregation.md b/docs/framework/ember/guide/aggregation.md index af9324346d..7772dd4e91 100644 --- a/docs/framework/ember/guide/aggregation.md +++ b/docs/framework/ember/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -50,6 +47,11 @@ const table = useTable(() => ({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; @@ -69,6 +71,28 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default @@ -90,13 +114,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -119,17 +156,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/ember/guide/column-filtering.md b/docs/framework/ember/guide/column-filtering.md index c7bba3c24e..207bc29a49 100644 --- a/docs/framework/ember/guide/column-filtering.md +++ b/docs/framework/ember/guide/column-filtering.md @@ -54,19 +54,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -330,7 +320,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/ember/guide/custom-features.md b/docs/framework/ember/guide/custom-features.md index 3cf72393d1..8096e31ed0 100644 --- a/docs/framework/ember/guide/custom-features.md +++ b/docs/framework/ember/guide/custom-features.md @@ -425,4 +425,4 @@ export default class CustomPluginTable extends Component { #### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `@tracked` property, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `@tracked` property, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/ember/guide/flex-render.md b/docs/framework/ember/guide/flex-render.md new file mode 100644 index 0000000000..ff9ad1d1fe --- /dev/null +++ b/docs/framework/ember/guide/flex-render.md @@ -0,0 +1,52 @@ +--- +title: FlexRender (Ember) Guide +--- + +Ember uses dedicated Glimmer components to render column definitions with typed cell and header contexts. Renderer results can be primitive content or component configurations. + +## `FlexRender` vs `flexRender` + +Ember does not export one component named `FlexRender`. Instead it provides three table-aware components: + +- `FlexRenderCell` +- `FlexRenderHeader` +- `FlexRenderFooter` + +Use them in `.gts` templates: + +```ts +import { + FlexRenderCell, + FlexRenderFooter, + FlexRenderHeader, +} from '@tanstack/ember-table' +``` + +```hbs + + + +``` + +`FlexRenderCell` selects `aggregatedCell` for aggregated rows, falls back to `cell`, and renders nothing for grouping placeholders. + +The lowercase `flexRender(renderable, context)` export is the low-level function used underneath those components. It calls function renderers and passes primitive results through, but it cannot mount a Glimmer component by itself and does not select the correct grouped-cell renderer. + +## Rendering Glimmer Components + +Return `flexRenderComponent()` from a column definition when the renderer should mount a component: + +```gts +import { flexRenderComponent } from '@tanstack/ember-table' +import StatusCell from './StatusCell.gts' + +const columns = columnHelper.columns([ + columnHelper.accessor('status', { + cell: () => flexRenderComponent(StatusCell, { emphasis: 'strong' }), + }), +]) +``` + +The component receives `@ctx` with the cell or header context and `@options` with the optional configuration passed to `flexRenderComponent`. + +Placeholder headers and footers remain the template's layout decision. Wrap the header or footer component in `{{#unless header.isPlaceholder}}` in ordinary layouts, but render it when a spanning-header layout intentionally needs its content. When calling table methods from a template, wrap them in a locally bound helper as shown in the Ember examples. diff --git a/docs/framework/ember/guide/global-filtering.md b/docs/framework/ember/guide/global-filtering.md index 773f78b33e..3747ef7f12 100644 --- a/docs/framework/ember/guide/global-filtering.md +++ b/docs/framework/ember/guide/global-filtering.md @@ -48,19 +48,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/ember/guide/pagination.md b/docs/framework/ember/guide/pagination.md index 0179b94ac7..8b14cb1ce9 100644 --- a/docs/framework/ember/guide/pagination.md +++ b/docs/framework/ember/guide/pagination.md @@ -42,21 +42,15 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Should You Use Virtualization Instead? -Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. #### Pagination Row Model diff --git a/docs/framework/ember/guide/sorting.md b/docs/framework/ember/guide/sorting.md index a6cbecfd1c..2c72728ed9 100644 --- a/docs/framework/ember/guide/sorting.md +++ b/docs/framework/ember/guide/sorting.md @@ -151,7 +151,9 @@ table = useTable(() => ({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -406,7 +408,7 @@ table = useTable(() => ({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```ts const columns = columnHelper.columns([ diff --git a/docs/framework/lit/guide/aggregation.md b/docs/framework/lit/guide/aggregation.md index 7cc7cfddc3..70490ffdcd 100644 --- a/docs/framework/lit/guide/aggregation.md +++ b/docs/framework/lit/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -52,6 +49,11 @@ const table = tableController.table({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; @@ -71,6 +73,28 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default @@ -92,13 +116,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -121,17 +158,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/lit/guide/column-filtering.md b/docs/framework/lit/guide/column-filtering.md index 926827953b..1f449f6404 100644 --- a/docs/framework/lit/guide/column-filtering.md +++ b/docs/framework/lit/guide/column-filtering.md @@ -67,19 +67,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -342,7 +332,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/lit/guide/custom-features.md b/docs/framework/lit/guide/custom-features.md index 530181c2a5..5244d51761 100644 --- a/docs/framework/lit/guide/custom-features.md +++ b/docs/framework/lit/guide/custom-features.md @@ -346,4 +346,4 @@ return html` ### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `@state`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `@state`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/lit/guide/flex-render.md b/docs/framework/lit/guide/flex-render.md new file mode 100644 index 0000000000..03ce8db219 --- /dev/null +++ b/docs/framework/lit/guide/flex-render.md @@ -0,0 +1,52 @@ +--- +title: FlexRender (Lit) Guide +--- + +Column definitions can contain Lit template results, directives, nodes, primitive values, iterables, or renderer functions for `header`, `cell`, `footer`, and `aggregatedCell`. Use the adapter's rendering utilities inside Lit templates so those values receive their table context. + +## `FlexRender` vs `flexRender` + +In Lit, `FlexRender` is a convenience function rather than a custom element. Pass exactly one `cell`, `header`, or `footer` object and interpolate the result: + +```ts +html` + + ${table.getHeaderGroups().map( + (headerGroup) => html` + + ${headerGroup.headers.map( + (header) => html` + ${header.isPlaceholder ? null : FlexRender({ header })} + `, + )} + + `, + )} + + + ${table.getRowModel().rows.map( + (row) => html` + + ${row + .getVisibleCells() + .map((cell) => html`${FlexRender({ cell })}`)} + + `, + )} + +` +``` + +Import it from `@tanstack/lit-table`. Tables returned by `TableController` also expose it as `table.FlexRender`. + +For cells, `FlexRender` selects `aggregatedCell` for aggregated rows, falls back to `cell`, and returns `null` for grouping placeholders. Pass footer-group `Header` objects as `FlexRender({ footer: header })`. + +`flexRender` is the lower-level function for a renderable value and context: + +```ts +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` + +It calls function renderers and passes every other `LitRenderable` through unchanged. It does not choose the appropriate grouped-cell renderer or suppress placeholders, so prefer `FlexRender` for ordinary table markup. + +Placeholder headers remain the template's layout decision. Check `header.isPlaceholder` unless the placeholder intentionally provides content for a spanning header. diff --git a/docs/framework/lit/guide/global-filtering.md b/docs/framework/lit/guide/global-filtering.md index b7ca4fe7e1..339fcec5df 100644 --- a/docs/framework/lit/guide/global-filtering.md +++ b/docs/framework/lit/guide/global-filtering.md @@ -61,19 +61,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/lit/guide/pagination.md b/docs/framework/lit/guide/pagination.md index 645120cfcd..5119b3a845 100644 --- a/docs/framework/lit/guide/pagination.md +++ b/docs/framework/lit/guide/pagination.md @@ -56,21 +56,15 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Should You Use Virtualization Instead? -Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. #### Pagination Row Model diff --git a/docs/framework/lit/guide/sorting.md b/docs/framework/lit/guide/sorting.md index eb6c416520..be989cee67 100644 --- a/docs/framework/lit/guide/sorting.md +++ b/docs/framework/lit/guide/sorting.md @@ -174,7 +174,9 @@ const table = this.tableController.table({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -430,7 +432,7 @@ const table = this.tableController.table({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```ts const columns = [ diff --git a/docs/framework/octane/guide/aggregation.md b/docs/framework/octane/guide/aggregation.md index 14715dc392..5bf7e42aff 100644 --- a/docs/framework/octane/guide/aggregation.md +++ b/docs/framework/octane/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -50,13 +47,18 @@ const table = useTable({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; multiple entries return an object keyed by the aggregation name or descriptor `id`. -```tsx +```ts columnHelper.accessor('amount', { aggregationFn: 'sum', }) @@ -69,19 +71,41 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default pre-grouped row model. Filtering is included; grouping, sorting, expansion, and pagination do not change that default total. -```tsx +```ts footer: ({ column }) => column.getAggregationValue().toLocaleString() ``` Pass one options object with rows from any row model to choose a different set: -```tsx +```ts column.getAggregationValue({ rows: table.getCoreRowModel().rows }) column.getAggregationValue({ rows: table.getRowModel().rows }) column.getAggregationValue({ rows: table.getFilteredSelectedRowModel().rows }) @@ -90,13 +114,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -104,7 +141,7 @@ Grouped aggregation composes two independent features. Register both, add the grouped row-model slot, and configure aggregation functions on the columns that should produce grouped values. -```tsx +```ts const features = tableFeatures({ rowAggregationFeature, columnGroupingFeature, @@ -119,17 +156,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/octane/guide/column-filtering.md b/docs/framework/octane/guide/column-filtering.md index 97a3ac3b32..ba503c54b1 100644 --- a/docs/framework/octane/guide/column-filtering.md +++ b/docs/framework/octane/guide/column-filtering.md @@ -53,19 +53,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -326,7 +316,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/octane/guide/custom-features.md b/docs/framework/octane/guide/custom-features.md index 8c15c19177..c537312a7d 100644 --- a/docs/framework/octane/guide/custom-features.md +++ b/docs/framework/octane/guide/custom-features.md @@ -370,4 +370,4 @@ return ( #### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `useState`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `useState`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/octane/guide/flex-render.md b/docs/framework/octane/guide/flex-render.md new file mode 100644 index 0000000000..98ceb651cc --- /dev/null +++ b/docs/framework/octane/guide/flex-render.md @@ -0,0 +1,31 @@ +--- +title: FlexRender (Octane) Guide +--- + +Column definitions can contain Octane nodes or renderer components for `header`, `cell`, `footer`, and `aggregatedCell`. Use TanStack Table's rendering utilities so function components become Octane element descriptors with the correct typed context. + +## `FlexRender` vs `flexRender` + +`FlexRender` is the recommended TSRX component. Give it exactly one table object: `cell`, `header`, or `footer`. + +```tsx + + + +``` + +The table instance exposes `FlexRender`, and the component can also be imported directly from `@tanstack/octane-table`. Rendering it as a component gives Octane the correct component scope while preserving strings, numbers (including `0`), and pre-created descriptors. + +For cells, `FlexRender` selects `aggregatedCell` when appropriate, falls back to `cell`, and suppresses grouping placeholder cells. + +`flexRender` is the lower-level function for a renderable value and context: + +```tsx +import { flexRender } from '@tanstack/octane-table' + +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` + +Octane function components are converted with `createElement`; non-function nodes pass through unchanged. Unlike React, Octane has no class-component, `forwardRef`, or exotic-component branch. The lower-level function also does not select grouped-cell renderers or suppress grouping placeholders. + +Use `FlexRender` in normal TSRX table markup. Check `header.isPlaceholder` before rendering a header unless a spanning layout intentionally uses the placeholder, and pass footer-group `Header` objects through the `footer` prop. diff --git a/docs/framework/octane/guide/global-filtering.md b/docs/framework/octane/guide/global-filtering.md index 62972d7e5d..6de42c1575 100644 --- a/docs/framework/octane/guide/global-filtering.md +++ b/docs/framework/octane/guide/global-filtering.md @@ -47,19 +47,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/octane/guide/pagination.md b/docs/framework/octane/guide/pagination.md index 950a3d9f89..9f63a5be17 100644 --- a/docs/framework/octane/guide/pagination.md +++ b/docs/framework/octane/guide/pagination.md @@ -42,21 +42,15 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Should You Use Virtualization Instead? -Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. #### Pagination Row Model diff --git a/docs/framework/octane/guide/sorting.md b/docs/framework/octane/guide/sorting.md index 0d13172a98..159490bfc5 100644 --- a/docs/framework/octane/guide/sorting.md +++ b/docs/framework/octane/guide/sorting.md @@ -143,7 +143,9 @@ const table = useTable({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -406,7 +408,7 @@ const table = useTable({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```tsx const columns = [ diff --git a/docs/framework/preact/guide/aggregation.md b/docs/framework/preact/guide/aggregation.md index e9423a05cb..3f6c4227da 100644 --- a/docs/framework/preact/guide/aggregation.md +++ b/docs/framework/preact/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -50,13 +47,18 @@ const table = useTable({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; multiple entries return an object keyed by the aggregation name or descriptor `id`. -```tsx +```ts columnHelper.accessor('amount', { aggregationFn: 'sum', }) @@ -69,19 +71,41 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default pre-grouped row model. Filtering is included; grouping, sorting, expansion, and pagination do not change that default total. -```tsx +```ts footer: ({ column }) => column.getAggregationValue().toLocaleString() ``` Pass one options object with rows from any row model to choose a different set: -```tsx +```ts column.getAggregationValue({ rows: table.getCoreRowModel().rows }) column.getAggregationValue({ rows: table.getRowModel().rows }) column.getAggregationValue({ rows: table.getFilteredSelectedRowModel().rows }) @@ -90,13 +114,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -104,7 +141,7 @@ Grouped aggregation composes two independent features. Register both, add the grouped row-model slot, and configure aggregation functions on the columns that should produce grouped values. -```tsx +```ts const features = tableFeatures({ rowAggregationFeature, columnGroupingFeature, @@ -119,17 +156,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/preact/guide/column-filtering.md b/docs/framework/preact/guide/column-filtering.md index 572e80f7c7..3ac9675401 100644 --- a/docs/framework/preact/guide/column-filtering.md +++ b/docs/framework/preact/guide/column-filtering.md @@ -53,19 +53,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -326,7 +316,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/preact/guide/custom-features.md b/docs/framework/preact/guide/custom-features.md index 4b09aa8a45..47540ae503 100644 --- a/docs/framework/preact/guide/custom-features.md +++ b/docs/framework/preact/guide/custom-features.md @@ -370,4 +370,4 @@ return ( #### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `useState`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `useState`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/preact/guide/flex-render.md b/docs/framework/preact/guide/flex-render.md new file mode 100644 index 0000000000..3572acfd6c --- /dev/null +++ b/docs/framework/preact/guide/flex-render.md @@ -0,0 +1,59 @@ +--- +title: FlexRender (Preact) Guide +--- + +Column definitions can contain plain Preact children or renderer components for `header`, `cell`, `footer`, and `aggregatedCell`. Use TanStack Table's rendering utilities so each value is interpreted correctly and receives its typed table context. + +## `FlexRender` vs `flexRender` + +`FlexRender` is the recommended component wrapper. Give it exactly one table object: `cell`, `header`, or `footer`. It then selects the correct column definition and context: + +```tsx +{ + table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : } + + ))} + + )) +} + +{ + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + + + ))} + + )) +} +``` + +The adapter attaches `FlexRender` to the table instance, and you can also import it directly: + +```tsx +import { FlexRender } from '@tanstack/preact-table' + +const footerContent = +``` + +For cells, `FlexRender` renders `aggregatedCell` when the cell is aggregated, falls back to `cell` when needed, and renders nothing for grouping placeholder cells. + +`flexRender` is the lower-level function for a renderable value and its props: + +```tsx +import { flexRender } from '@tanstack/preact-table' + +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` + +It distinguishes Preact components and supported memo/forward-ref component objects from already-created children. It does not select grouped-cell renderers or suppress grouping placeholders; `FlexRender` handles those table-specific decisions. + +Use `cell.getValue()` or `cell.renderValue()` when you only need the accessor value. Use `FlexRender` to render column definitions and pass their complete contexts. + +Placeholder headers are a layout decision and are not automatically suppressed. Check `header.isPlaceholder` unless the placeholder intentionally supplies content for a spanning header. Pass footer-group `Header` objects through the `footer` prop. diff --git a/docs/framework/preact/guide/global-filtering.md b/docs/framework/preact/guide/global-filtering.md index 2df81292c5..5b91739d32 100644 --- a/docs/framework/preact/guide/global-filtering.md +++ b/docs/framework/preact/guide/global-filtering.md @@ -47,19 +47,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/preact/guide/pagination.md b/docs/framework/preact/guide/pagination.md index 2c4554bb70..b9826208f6 100644 --- a/docs/framework/preact/guide/pagination.md +++ b/docs/framework/preact/guide/pagination.md @@ -42,21 +42,15 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Should You Use Virtualization Instead? -Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. #### Pagination Row Model diff --git a/docs/framework/preact/guide/sorting.md b/docs/framework/preact/guide/sorting.md index 9b5b8957df..19d652ada0 100644 --- a/docs/framework/preact/guide/sorting.md +++ b/docs/framework/preact/guide/sorting.md @@ -143,7 +143,9 @@ const table = useTable({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -406,7 +408,7 @@ const table = useTable({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```tsx const columns = [ diff --git a/docs/framework/react/guide/aggregation.md b/docs/framework/react/guide/aggregation.md index bc31370e79..a257a12db5 100644 --- a/docs/framework/react/guide/aggregation.md +++ b/docs/framework/react/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -50,13 +47,18 @@ const table = useTable({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; multiple entries return an object keyed by the aggregation name or descriptor `id`. -```tsx +```ts columnHelper.accessor('amount', { aggregationFn: 'sum', }) @@ -69,19 +71,41 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default pre-grouped row model. Filtering is included; grouping, sorting, expansion, and pagination do not change that default total. -```tsx +```ts footer: ({ column }) => column.getAggregationValue().toLocaleString() ``` Pass one options object with rows from any row model to choose a different set: -```tsx +```ts column.getAggregationValue({ rows: table.getCoreRowModel().rows }) column.getAggregationValue({ rows: table.getRowModel().rows }) column.getAggregationValue({ rows: table.getFilteredSelectedRowModel().rows }) @@ -90,13 +114,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -104,7 +141,7 @@ Grouped aggregation composes two independent features. Register both, add the grouped row-model slot, and configure aggregation functions on the columns that should produce grouped values. -```tsx +```ts const features = tableFeatures({ rowAggregationFeature, columnGroupingFeature, @@ -119,17 +156,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/react/guide/column-filtering.md b/docs/framework/react/guide/column-filtering.md index 82c1f9c36e..bd72f77117 100644 --- a/docs/framework/react/guide/column-filtering.md +++ b/docs/framework/react/guide/column-filtering.md @@ -53,19 +53,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -326,7 +316,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/react/guide/custom-features.md b/docs/framework/react/guide/custom-features.md index e981306d85..58571cce81 100644 --- a/docs/framework/react/guide/custom-features.md +++ b/docs/framework/react/guide/custom-features.md @@ -370,4 +370,4 @@ return ( #### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `React.useState`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `React.useState`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/react/guide/flex-render.md b/docs/framework/react/guide/flex-render.md new file mode 100644 index 0000000000..5e4fdca294 --- /dev/null +++ b/docs/framework/react/guide/flex-render.md @@ -0,0 +1,72 @@ +--- +title: FlexRender (React) Guide +--- + +Column definitions can contain plain React nodes or renderer components for `header`, `cell`, `footer`, and `aggregatedCell`. Use TanStack Table's rendering utilities so each value is interpreted correctly and receives its typed table context. + +## `FlexRender` vs `flexRender` + +`FlexRender` is the recommended component wrapper. Give it exactly one table object: `cell`, `header`, or `footer`. It then selects the correct column definition and context: + +```tsx +{ + table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : } + + ))} + + )) +} + +{ + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + + + ))} + + )) +} +``` + +The adapter attaches `FlexRender` to the table instance, and you can also import it directly: + +```tsx +import { FlexRender } from '@tanstack/react-table' + +const footerContent = +``` + +For cells, `FlexRender` renders `aggregatedCell` when the cell is aggregated, falls back to `cell` when no aggregated renderer exists, and renders nothing for grouping placeholder cells. + +`flexRender` is the lower-level function. Use it when you already have a renderable value and its props: + +```tsx +import { flexRender } from '@tanstack/react-table' + +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` + +It distinguishes React components, including class components, `memo`, and `forwardRef`, from already-created React nodes. It does not select `cell` versus `aggregatedCell` or suppress grouping placeholders. The `FlexRender` wrapper handles those table-specific decisions. + +## Column Renderer Components + +Renderer functions are treated as React components and receive the appropriate context as props: + +```tsx +const columns = columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => , + cell: ({ getValue }) => {getValue()}, + }), +]) +``` + +Use `cell.getValue()` or `cell.renderValue()` when you only need the accessor value. Use `FlexRender` when rendering the column definition, because it supports static nodes and component renderers and passes the full context. + +Placeholder headers are a layout decision and are not automatically suppressed. Check `header.isPlaceholder` unless the placeholder intentionally supplies content for a spanning header. Footer groups contain `Header` objects too, so pass each footer header through the `footer` prop. diff --git a/docs/framework/react/guide/global-filtering.md b/docs/framework/react/guide/global-filtering.md index 2b9b6e08fb..a2c852c354 100644 --- a/docs/framework/react/guide/global-filtering.md +++ b/docs/framework/react/guide/global-filtering.md @@ -47,19 +47,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/react/guide/pagination.md b/docs/framework/react/guide/pagination.md index b92ef2020a..93cb500f97 100644 --- a/docs/framework/react/guide/pagination.md +++ b/docs/framework/react/guide/pagination.md @@ -42,21 +42,15 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Should You Use Virtualization Instead? -Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. #### Pagination Row Model diff --git a/docs/framework/react/guide/sorting.md b/docs/framework/react/guide/sorting.md index dcb5a30abf..9c4f331df0 100644 --- a/docs/framework/react/guide/sorting.md +++ b/docs/framework/react/guide/sorting.md @@ -143,7 +143,9 @@ const table = useTable({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -406,7 +408,7 @@ const table = useTable({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```tsx const columns = [ diff --git a/docs/framework/solid/guide/aggregation.md b/docs/framework/solid/guide/aggregation.md index 9d9f36baef..caf7ea347f 100644 --- a/docs/framework/solid/guide/aggregation.md +++ b/docs/framework/solid/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -50,13 +47,18 @@ const table = createTable({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; multiple entries return an object keyed by the aggregation name or descriptor `id`. -```tsx +```ts columnHelper.accessor('amount', { aggregationFn: 'sum', }) @@ -69,19 +71,41 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default pre-grouped row model. Filtering is included; grouping, sorting, expansion, and pagination do not change that default total. -```tsx +```ts footer: ({ column }) => column.getAggregationValue().toLocaleString() ``` Pass one options object with rows from any row model to choose a different set: -```tsx +```ts column.getAggregationValue({ rows: table.getCoreRowModel().rows }) column.getAggregationValue({ rows: table.getRowModel().rows }) column.getAggregationValue({ rows: table.getFilteredSelectedRowModel().rows }) @@ -90,13 +114,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -104,7 +141,7 @@ Grouped aggregation composes two independent features. Register both, add the grouped row-model slot, and configure aggregation functions on the columns that should produce grouped values. -```tsx +```ts const features = tableFeatures({ rowAggregationFeature, columnGroupingFeature, @@ -119,17 +156,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/solid/guide/column-filtering.md b/docs/framework/solid/guide/column-filtering.md index 3170a32509..c957a8e9b0 100644 --- a/docs/framework/solid/guide/column-filtering.md +++ b/docs/framework/solid/guide/column-filtering.md @@ -57,19 +57,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -325,7 +315,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/solid/guide/custom-features.md b/docs/framework/solid/guide/custom-features.md index 00d23f777e..6f405c910e 100644 --- a/docs/framework/solid/guide/custom-features.md +++ b/docs/framework/solid/guide/custom-features.md @@ -365,4 +365,4 @@ return ( ### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `createSignal`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `createSignal`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/solid/guide/flex-render.md b/docs/framework/solid/guide/flex-render.md new file mode 100644 index 0000000000..62e51b7927 --- /dev/null +++ b/docs/framework/solid/guide/flex-render.md @@ -0,0 +1,53 @@ +--- +title: FlexRender (Solid) Guide +--- + +Column definitions can contain Solid JSX values or renderer components for `header`, `cell`, `footer`, and `aggregatedCell`. Use TanStack Table's rendering utilities so each value is created in the correct reactive owner and receives its typed context. + +## `FlexRender` vs `flexRender` + +`FlexRender` is the recommended JSX component. Give it exactly one table object: `cell`, `header`, or `footer`. + +```tsx + + {(headerGroup) => ( + + + {(header) => ( + + + + + + )} + + + )} + + + + {(row) => ( + + + {(cell) => } + + + )} + +``` + +The table instance exposes `FlexRender`, or you can import the component directly from `@tanstack/solid-table`. Its keyed control flow recreates content when a persistent view starts representing a different cell or header. This behavior is important for virtualization and other reused views. + +For cells, it chooses `aggregatedCell` for aggregated rows, falls back to `cell`, and suppresses grouping placeholders. + +`flexRender` is the lower-level function for a renderable value and context: + +```tsx +import { flexRender } from '@tanstack/solid-table' + +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` + +Function renderers are created with Solid's component machinery. The lower-level function does not choose the correct grouped-cell renderer or key the result to a changing table object, so prefer `FlexRender` for normal table markup. + +Placeholder headers remain the caller's layout decision. Check `header.isPlaceholder` unless a spanning-header layout intentionally renders that placeholder. Pass footer-group `Header` objects with ``. diff --git a/docs/framework/solid/guide/global-filtering.md b/docs/framework/solid/guide/global-filtering.md index a8be7760c0..3b43ce8ec1 100644 --- a/docs/framework/solid/guide/global-filtering.md +++ b/docs/framework/solid/guide/global-filtering.md @@ -51,19 +51,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/solid/guide/pagination.md b/docs/framework/solid/guide/pagination.md index 400a98e7a5..dc05192789 100644 --- a/docs/framework/solid/guide/pagination.md +++ b/docs/framework/solid/guide/pagination.md @@ -46,21 +46,15 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Should You Use Virtualization Instead? -Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. #### Pagination Row Model diff --git a/docs/framework/solid/guide/sorting.md b/docs/framework/solid/guide/sorting.md index b811996cac..a6de9f8a28 100644 --- a/docs/framework/solid/guide/sorting.md +++ b/docs/framework/solid/guide/sorting.md @@ -148,7 +148,9 @@ const table = createTable({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -405,7 +407,7 @@ const table = createTable({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```tsx const columns = [ diff --git a/docs/framework/svelte/guide/aggregation.md b/docs/framework/svelte/guide/aggregation.md index 09a07ac282..aa4b8160c6 100644 --- a/docs/framework/svelte/guide/aggregation.md +++ b/docs/framework/svelte/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -50,6 +47,11 @@ const table = createTable({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; @@ -69,6 +71,28 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default @@ -90,13 +114,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -119,17 +156,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/svelte/guide/column-filtering.md b/docs/framework/svelte/guide/column-filtering.md index 62beb858c1..18fade4312 100644 --- a/docs/framework/svelte/guide/column-filtering.md +++ b/docs/framework/svelte/guide/column-filtering.md @@ -57,19 +57,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -337,7 +327,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/svelte/guide/custom-features.md b/docs/framework/svelte/guide/custom-features.md index 85380357eb..fc8fc4bc4d 100644 --- a/docs/framework/svelte/guide/custom-features.md +++ b/docs/framework/svelte/guide/custom-features.md @@ -344,4 +344,4 @@ Now that the feature is added to the table instance, you can use the new instanc ### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `$state`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `$state`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/svelte/guide/flex-render.md b/docs/framework/svelte/guide/flex-render.md new file mode 100644 index 0000000000..e339ed9533 --- /dev/null +++ b/docs/framework/svelte/guide/flex-render.md @@ -0,0 +1,73 @@ +--- +title: FlexRender (Svelte) Guide +--- + +Svelte column definitions can contain strings or renderer functions that return primitive values, component configurations, or snippet configurations. The `FlexRender` component resolves those forms reactively with the correct cell or header context. + +## `FlexRender` and the Lower-Level Form + +Svelte exports the `FlexRender` component, not a separate lowercase `flexRender` function. For normal table markup, import the component and pass exactly one `cell`, `header`, or `footer` object: + +```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} +``` + +For cells, `FlexRender` selects `aggregatedCell` for aggregated rows, falls back to `cell`, and renders nothing for grouping placeholders. Use `` for a footer-group header object. + +App tables created with `createTableHook` also expose `FlexRender` through the table and context-bound app components. The direct import shown above works for both ordinary and app tables. + +The older lower-level component form accepts `content` and `context` explicitly: + +```svelte + +``` + +Prefer the table-object shorthand because it makes the grouped-cell decisions for you. + +## Components and Snippets in Column Definitions + +Wrap Svelte components with `renderComponent()` and snippets with `renderSnippet()` before returning them from a column renderer: + +```svelte + +``` + +A snippet used with `renderSnippet` must accept one parameter. `FlexRender` mounts the resulting component or invokes the snippet with the supplied props or parameter. + +Placeholder headers remain the template's layout decision. Check `header.isPlaceholder` unless a spanning-header layout intentionally renders the placeholder. diff --git a/docs/framework/svelte/guide/global-filtering.md b/docs/framework/svelte/guide/global-filtering.md index 843f184c4a..a6d361254e 100644 --- a/docs/framework/svelte/guide/global-filtering.md +++ b/docs/framework/svelte/guide/global-filtering.md @@ -51,19 +51,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/svelte/guide/pagination.md b/docs/framework/svelte/guide/pagination.md index 0463d37c37..2ea77d6821 100644 --- a/docs/framework/svelte/guide/pagination.md +++ b/docs/framework/svelte/guide/pagination.md @@ -46,21 +46,15 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Should You Use Virtualization Instead? -Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. #### Pagination Row Model diff --git a/docs/framework/svelte/guide/sorting.md b/docs/framework/svelte/guide/sorting.md index 8a54e233bc..00b16a4f97 100644 --- a/docs/framework/svelte/guide/sorting.md +++ b/docs/framework/svelte/guide/sorting.md @@ -158,7 +158,9 @@ const table = createTable({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -419,7 +421,7 @@ const table = createTable({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```ts const columns = [ diff --git a/docs/framework/vanilla/guide/aggregation.md b/docs/framework/vanilla/guide/aggregation.md index 8020ddc5a7..45beb9d0c9 100644 --- a/docs/framework/vanilla/guide/aggregation.md +++ b/docs/framework/vanilla/guide/aggregation.md @@ -10,9 +10,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -49,6 +46,11 @@ const table = constructTable({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; @@ -68,6 +70,28 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default @@ -89,13 +113,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -118,17 +155,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/vanilla/guide/flex-render.md b/docs/framework/vanilla/guide/flex-render.md new file mode 100644 index 0000000000..bf6bc9c27f --- /dev/null +++ b/docs/framework/vanilla/guide/flex-render.md @@ -0,0 +1,35 @@ +--- +title: FlexRender (Vanilla) Guide +--- + +The Vanilla adapter does not own a rendering engine. Its rendering helpers resolve plain column-definition values and functions, while your application decides how to turn the returned value into DOM nodes or HTML. + +Import the helpers from the dedicated core entry point: + +```ts +import { FlexRender, flexRender } from '@tanstack/table-core/flex-render' +``` + +## `FlexRender` vs `flexRender` + +`FlexRender` is the table-aware wrapper. Pass exactly one `cell`, `header`, or `footer` object: + +```ts +const headerContent = header.isPlaceholder ? null : FlexRender({ header }) +const cellContent = FlexRender({ cell }) +const footerContent = FlexRender({ footer: header }) +``` + +For cells, it selects `aggregatedCell` for aggregated rows, falls back to `cell`, and returns `null` for grouping placeholders. + +`flexRender` is the lower-level function for a renderable definition and its context: + +```ts +const content = flexRender(cell.column.columnDef.cell, cell.getContext()) +``` + +It invokes a function definition with the supplied props and passes any other value through unchanged. It does not select grouped-cell definitions or suppress placeholders. + +Neither helper inserts content into the document. Handle the returned value according to your rendering system. For example, assign trusted text to `textContent`, append a returned node, or pass a framework-specific value to another renderer. Do not assign untrusted strings to `innerHTML` without sanitizing them. + +Placeholder headers and footers remain the caller's layout decision. Skip `header.isPlaceholder` in ordinary layouts unless a spanning-header layout intentionally needs that content. diff --git a/docs/framework/vue/guide/aggregation.md b/docs/framework/vue/guide/aggregation.md index 209fe417d4..d494544a0b 100644 --- a/docs/framework/vue/guide/aggregation.md +++ b/docs/framework/vue/guide/aggregation.md @@ -11,9 +11,6 @@ Aggregation is independent from column grouping. Register `rowAggregationFeature whenever columns calculate totals or aggregated values. Add `columnGroupingFeature` separately only when the table also groups rows. -For the complete behavior and type reference, see the core -[Aggregation Guide](../../../guide/aggregation). - ## Aggregation Setup Register only the built-in functions referenced by name. Passing a definition @@ -50,6 +47,11 @@ const table = useTable({ The aggregation feature does not require a grouped row model. This makes grand totals and custom row-subset totals available in otherwise ordinary tables. +The full `aggregationFns` registry remains available for compatibility, but it +bundles every built-in. Tables using `stockFeatures` already include +`rowAggregationFeature`; they still need the definitions that named column +options should resolve to. + ## Column Aggregations A column accepts one aggregation or an array. A single entry returns a scalar; @@ -69,6 +71,28 @@ columnHelper.accessor('score', { String values remain backward-compatible. Use descriptors when a result needs a stable custom key or options. +A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline +definition. Every entry in an aggregation array needs a unique stable id. +Duplicate ids, missing descriptor ids, and unregistered names warn in +development and preserve the affected key with an `undefined` value. + +Multiple aggregations can be read with a typed result: + +```ts +const scoreColumn = columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], + footer: ({ column }) => { + const result = column.getAggregationValue<{ + count: number + mean: number | undefined + range: [number | undefined, number | undefined] + }>() + + return `${result.count} values; mean ${result.mean}; range ${result.range}` + }, +}) +``` + ## Grand Totals and Row Subsets Call `column.getAggregationValue()` without arguments to aggregate the default @@ -90,13 +114,26 @@ column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) ``` Depth is relative to the supplied row array. `0` selects those roots, `1` -selects their direct sub-rows, and `Infinity` selects terminal rows. Configure -`maxAggregationDepth` on the column for cached default calls, or pass -`maxDepth` in the options object as an explicit override. -`table.getMaxSubRowDepth()` returns -the deepest structural depth in the core row model. Column option -`getAggregationValue(context)` can provide an external or server-computed -value; return `undefined` to fall back to the configured aggregation function. +selects their direct sub-rows, and so on. Selection returns a unique frontier: +a branch that ends before the maximum depth contributes its deepest available +row. `Infinity` selects terminal rows. + +Configure `maxAggregationDepth` on the column for cached default calls (it +defaults to `0`), or pass `maxDepth` in the options object as an explicit +override. Every aggregation configured on the column receives the same +selected rows. Explicit row calls are recomputed each time; the default call is +cached against its row model, depth, registry, and column aggregation option. + +`table.getMaxSubRowDepth()` returns the deepest structural depth in the core +row model. To stop one level before the deepest sub-row frontier: + +```ts +const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) +column.getAggregationValue({ + rows: table.getCoreRowModel().rows, + maxDepth, +}) +``` ## Grouped Aggregation @@ -119,17 +156,114 @@ columnHelper.accessor('visits', { }) ``` -Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer -rendering uses the adapter's normal footer renderer. +The `aggregatedCell` column option renders aggregate values on synthetic +grouped rows. Use `cell.getIsAggregated()` to identify a grouped aggregate +cell. Footer rendering uses the adapter's normal footer renderer. Grouping-only +tables do not expose `cell.getIsAggregated()`; it belongs to +`rowAggregationFeature`. ## Custom Aggregation Definitions -Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. -The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, -`column`, and `table`. Every aggregation configured on a column receives the -same row frontier. Grouped calls also include `groupingRow` and immediate -`subRows` for custom structural behavior. A `merge` implementation can more -efficiently combine already-computed sub-row results. +Custom aggregations are context-based definitions. `rows` contains the unique +frontier selected at `maxDepth`, and `getValue(row)` reads the current column's +value. + +```ts +const joined = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows + .map((row) => getValue(row)) + .filter(Boolean) + .join(', '), +}) +``` + +The context also includes `column`, `columnId`, `maxDepth`, and `table`. During +grouped aggregation it includes `groupingRow` and `subRows`; root and +caller-supplied-row aggregation omit those properties. The grouping depth is +`groupingRow.depth`. `subRows` contains the immediate rows at that grouping +level, so an aggregation can explicitly choose immediate sub-rows instead of +the depth-selected `rows`: + +```ts +const subRowCount = constructAggregationFn({ + aggregate: ({ subRows, rows }) => (subRows ?? rows).length, +}) +``` + +At the terminal grouping level, `subRows` contains direct data rows. At a +nested level, it contains the immediate synthetic sub-row groups. All built-in +aggregation definitions consume the same depth-selected `rows`; `subRows` +remains available when a custom definition intentionally needs the grouping +row's immediate structural children. + +For a result that can be combined more efficiently from already-computed +sub-row results, provide a `merge` function: + +```ts +const sum = constructAggregationFn({ + aggregate: ({ rows, getValue }) => + rows.reduce((total, row) => { + const value = getValue(row) + return total + (typeof value === 'number' ? value : 0) + }, 0), + merge: ({ subRowResults }) => + subRowResults.reduce((total, value) => total + value, 0), +}) +``` + +For `merge`, `subRowResults[i]` is the aggregation result previously computed +for `subRows[i]`. Without `merge`, nested grouping calls `aggregate` with both +the group's depth-selected `rows` and its immediate `subRows`. This +context-based form replaces the previous callable aggregation signature and its +`fromRows` and `resolveDataValue` properties while preserving access to both +row sets. + +## Providing Server or External Values + +A column can handle aggregation-value requests before local calculation: + +```ts +const amountColumn = columnHelper.accessor('amount', { + aggregationFn: 'sum', + getAggregationValue: ({ rows }) => { + if (rows !== undefined) return undefined // use local fallback for overrides + return { value: serverTotals.amount } + }, +}) +``` + +Returning `{ value }` marks the request as handled, including +`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the +same provider on `defaultColumn` to share it across columns. + +Set `manualAggregation: true` to disable the local fallback for +`column.getAggregationValue()`. This is separate from `manualGrouping`, which +controls whether the grouped row model runs. See the +[Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) +for guidance on choosing where the full data pipeline should run. + +## Built-in Definitions + +- `sum`: sums numeric values; non-numbers contribute zero. +- `count`: counts rows. +- `min` / `max`: find numeric or Date bounds. +- `extent`: returns `[min, max]`; an empty input returns + `[undefined, undefined]`. +- `mean`: averages numeric and number-like non-null values. +- `median`: requires every row value to be a number. +- `unique` / `uniqueCount`: use JavaScript `Set` semantics. +- `first` / `last`: return the positional value, including a nullish value. + +`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a +registered `sum`, Dates resolve to a registered `extent`, and other values do +not resolve an aggregation. + +## Web Workers -See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) -for the full contract, return typing, caching behavior, and worker limitations. +Worker-backed grouped row models eagerly compute explicitly configured grouped +aggregates in the worker. `column.getAggregationValue()` still executes its +final total on the main thread over the selected row model. Aggregation results +crossing the worker boundary must be structured-cloneable. See the +[Worker Row Models Guide](../../../guide/worker-row-models) for setup and +limitations. diff --git a/docs/framework/vue/guide/column-filtering.md b/docs/framework/vue/guide/column-filtering.md index bee8e42aba..4ab882da3f 100644 --- a/docs/framework/vue/guide/column-filtering.md +++ b/docs/framework/vue/guide/column-filtering.md @@ -55,19 +55,9 @@ TanStack table supports both client-side and manual server-side filtering. This ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Filtering @@ -330,7 +320,7 @@ You can attach a few other properties to filter functions to customize their beh - `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. -- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. e.g. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. When provided, this test is authoritative: values it keeps stay in filter state even when they are empty strings, which the default heuristic would otherwise remove. An `undefined` filter value always clears the filter regardless. The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: diff --git a/docs/framework/vue/guide/custom-features.md b/docs/framework/vue/guide/custom-features.md index e57a48145b..0d9842392e 100644 --- a/docs/framework/vue/guide/custom-features.md +++ b/docs/framework/vue/guide/custom-features.md @@ -376,4 +376,4 @@ Reads of `table.atoms.density.get()` are reactive inside Vue templates, so the c ### Do We Have to Do It This Way? -This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `ref`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. +This is just a new way to integrate custom code alongside the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in a `ref`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features alongside TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/vue/guide/flex-render.md b/docs/framework/vue/guide/flex-render.md new file mode 100644 index 0000000000..e5b4afd290 --- /dev/null +++ b/docs/framework/vue/guide/flex-render.md @@ -0,0 +1,51 @@ +--- +title: FlexRender (Vue) Guide +--- + +Column definitions can contain strings, VNodes, Vue components, or renderer functions for `header`, `cell`, `footer`, and `aggregatedCell`. Use the adapter's rendering utilities so each form is mounted correctly and receives its table context. + +## `FlexRender` vs `flexRender` + +`FlexRender` is the recommended Vue component. Its shorthand props accept exactly one `cell`, `header`, or `footer` object: + +```vue + + + +``` + +For cells, the component selects `aggregatedCell` for aggregated rows, falls back to `cell`, and renders nothing for grouping placeholders. Use `:footer="header"` for a header object from a footer group. + +Tables created by `createTableHook` also expose an app-aware component as `table.FlexRender`. Because it is stored as a value, render it with Vue's dynamic component syntax: + +```vue + +``` + +`flexRender` is the lower-level function for a renderable value and its props: + +```ts +import { flexRender } from '@tanstack/vue-table' + +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` + +It passes primitive values through, preserves returned VNodes, and creates VNodes for Vue component objects. It does not choose grouped-cell renderers or suppress grouping placeholders. + +The component still supports the older low-level `render`/`props` pair: + +```vue + +``` + +Prefer the `cell`, `header`, and `footer` shorthand for new code. Placeholder headers remain the caller's layout decision, so check `header.isPlaceholder` unless a spanning-header layout intentionally renders one. diff --git a/docs/framework/vue/guide/global-filtering.md b/docs/framework/vue/guide/global-filtering.md index 1827b6d089..e9efa35d80 100644 --- a/docs/framework/vue/guide/global-filtering.md +++ b/docs/framework/vue/guide/global-filtering.md @@ -49,19 +49,9 @@ This guide will focus on global filtering, which is a filter that is applied acr ### Client-Side vs Server-Side Filtering -If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. +Filtering should operate over the same dataset as sorting and pagination. Use client-side filtering when the browser has the complete dataset; use server-side filtering when it has only a page or another subset, unless filtering just the loaded rows is intentional. -However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. - -> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for combining data operations. ### Manual Server-Side Global Filtering diff --git a/docs/framework/vue/guide/pagination.md b/docs/framework/vue/guide/pagination.md index d96bda8d45..9487df9f53 100644 --- a/docs/framework/vue/guide/pagination.md +++ b/docs/framework/vue/guide/pagination.md @@ -44,21 +44,15 @@ Using client-side pagination means that the `data` that you fetch will contain * #### Should You Use Client-Side Pagination? -Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. +Client-side pagination is usually the simplest option when the browser can fetch and retain the complete dataset. Use server-side pagination when the full dataset would be too expensive to query, transfer, or store in the browser. -However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a handful of columns. - -Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: - -1. Can your server query all of the data in a reasonable amount of time (and cost)? -2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) -3. Is the client's browser using too much memory if all of the data is loaded at once? - -If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. +Row count alone does not decide the boundary. See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework, performance factors, and guidance for keeping filtering and sorting consistent with pagination. #### Should You Use Virtualization Instead? -Alternatively, instead of paginating the data, you can render all rows of a large dataset on the same page, but only use the browser's resources to render the rows that are visible in the viewport. This strategy is often called "virtualization" or "windowing". TanStack offers a virtualization library called [TanStack Virtual](https://tanstack.com/virtual/latest) that can work well with TanStack Table. The UI/UX of both virtualization and pagination have their own trade-offs, so see which one works best for your use-case. +Virtualization (or windowing) reduces rendering work by mounting only the visible rows, but the virtualized data still exists in the browser. It can complement client-side or server-side pagination, but it does not replace server-side processing when the complete dataset is too large to load. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side#rendering-is-a-separate-decision) for that distinction, or [TanStack Virtual](https://tanstack.com/virtual/latest) for virtualization APIs. #### Pagination Row Model diff --git a/docs/framework/vue/guide/sorting.md b/docs/framework/vue/guide/sorting.md index a73738dc1e..0583d0c471 100644 --- a/docs/framework/vue/guide/sorting.md +++ b/docs/framework/vue/guide/sorting.md @@ -149,7 +149,9 @@ const table = useTable({ ### Client-Side vs Server-Side Sorting -Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. +Sorting should operate over the same dataset as filtering and pagination. If the server returns only a page or filtered subset, client-side sorting sorts only those loaded rows, not the full dataset. + +See the [Client-Side vs Server-Side Guide](../../../guide/client-side-vs-server-side) for the full decision framework and the cases where mixing client-side and server-side operations is intentional. ### Manual Server-Side Sorting @@ -408,7 +410,7 @@ const table = useTable({ #### Invert Sorting -Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring. ```ts const columns = [ diff --git a/docs/guide/aggregation.md b/docs/guide/aggregation.md deleted file mode 100644 index 5f57d564e3..0000000000 --- a/docs/guide/aggregation.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -title: Aggregation Guide ---- - -Aggregation computes column values over sets of rows. It is independent from -column grouping: use it for table totals, filtered totals, selected-row totals, -or custom subsets without creating grouped rows. Register it alongside column -grouping when grouped rows should also expose aggregate values. - -## Setup - -Register `rowAggregationFeature` and the named definitions referenced by your -columns. Registering individual built-ins keeps unused functions out of the -bundle. - -```ts -import { - rowAggregationFeature, - aggregationFn_mean, - aggregationFn_sum, - tableFeatures, -} from '@tanstack/table-core' - -const features = tableFeatures({ - rowAggregationFeature, - aggregationFns: { - mean: aggregationFn_mean, - sum: aggregationFn_sum, - }, -}) -``` - -The full `aggregationFns` registry remains available for compatibility, but it -bundles every built-in. Tables using `stockFeatures` already include -`rowAggregationFeature`; they still need the definitions that named column options -should resolve to. - -To combine aggregation with grouped rows, register both independent features: - -```ts -const features = tableFeatures({ - rowAggregationFeature, - columnGroupingFeature, - groupedRowModel: createGroupedRowModel(), - aggregationFns: { sum: aggregationFn_sum }, -}) -``` - -## One Aggregation Per Column - -A scalar `aggregationFn` can be a registered name, `'auto'`, or an inline -definition. - -```ts -const amountColumn = columnHelper.accessor('amount', { - aggregationFn: 'sum', - footer: ({ column }) => column.getAggregationValue(), -}) -``` - -With no options argument, `column.getAggregationValue()` aggregates the table's -pre-grouped row model. In the normal client pipeline this includes filtering, -but precedes grouping, sorting, expansion, and pagination. It uses the column's -`maxAggregationDepth` (`0` by default). - -## Choosing Which Rows To Aggregate - -Pass rows in the options object to override the default: - -```ts -column.getAggregationValue({ rows: table.getCoreRowModel().rows }) // all core rows -column.getAggregationValue({ rows: table.getRowModel().rows }) // rendered model -column.getAggregationValue({ - rows: table.getFilteredSelectedRowModel().rows, -}) -column.getAggregationValue({ rows: customRows }) -column.getAggregationValue({ rows: customRows, maxDepth: 1 }) -``` - -Depth is relative to the supplied row array. `0` selects those root rows, `1` -selects their direct sub-rows, and so on. Selection returns a unique frontier: -a branch that ends before the maximum depth contributes its deepest available -row. `Infinity` selects terminal rows. - -Set the cached default depth on the column: - -```ts -const amountColumn = columnHelper.accessor('amount', { - aggregationFn: ['sum', 'mean', 'count'], - maxAggregationDepth: 0, -}) -``` - -Every aggregation configured on the column receives the same selected rows. -Explicit-row calls are recomputed each time; the default call is cached against -its row model, depth, registry, and column aggregation option. - -`table.getMaxSubRowDepth()` returns the deepest structural depth in the core row -model. To stop one level before the deepest sub-row frontier: - -```ts -const maxDepth = Math.max(0, table.getMaxSubRowDepth() - 1) -column.getAggregationValue({ - rows: table.getCoreRowModel().rows, - maxDepth, -}) -``` - -## Multiple Aggregations Per Column - -Use an array to return a keyed result object. Registered names become their own -keys. Use a descriptor when the result needs a different key or when an inline -definition appears in the array. - -```ts -const scoreColumn = columnHelper.accessor('score', { - aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], - footer: ({ column }) => { - const result = column.getAggregationValue<{ - count: number - mean: number | undefined - range: [number | undefined, number | undefined] - }>() - - return `${result.count} values; mean ${result.mean}; range ${result.range}` - }, -}) -``` - -Every entry needs a unique stable id. Duplicate ids, missing descriptor ids, -and unregistered names warn in development and preserve the affected key with -an `undefined` value. - -## Custom Aggregation Definitions - -Custom aggregations are context-based definitions. `rows` contains the unique -frontier selected at `maxDepth`, and `getValue(row)` reads the current column's -value. - -```ts -const joined = constructAggregationFn({ - aggregate: ({ rows, getValue }) => - rows - .map((row) => getValue(row)) - .filter(Boolean) - .join(', '), -}) -``` - -The context also includes `column`, `columnId`, `maxDepth`, and `table`. During grouped -aggregation it includes `groupingRow` and `subRows`; root and -caller-supplied-row aggregation omit those properties. The grouping depth is -`groupingRow.depth`. `subRows` contains the immediate rows at that grouping -level, so an aggregation can explicitly choose immediate sub-rows instead of -the depth-selected `rows`: - -```ts -const subRowCount = constructAggregationFn({ - aggregate: ({ subRows, rows }) => (subRows ?? rows).length, -}) -``` - -At the terminal grouping level, `subRows` contains direct data rows. At a -nested level, it contains the immediate synthetic sub-row groups. - -All built-in aggregation definitions consume the same depth-selected `rows`. -`subRows` remains available when a custom definition intentionally needs the -grouping row's immediate structural children. - -For a result that can be combined more efficiently from already-computed -sub-row results, provide a `merge` function: - -```ts -const sum = constructAggregationFn({ - aggregate: ({ rows, getValue }) => - rows.reduce((total, row) => { - const value = getValue(row) - return total + (typeof value === 'number' ? value : 0) - }, 0), - merge: ({ subRowResults }) => - subRowResults.reduce((total, value) => total + value, 0), -}) -``` - -For `merge`, `subRowResults[i]` is the aggregation result previously computed -for `subRows[i]`. - -Without `merge`, nested grouping calls `aggregate` with both the group's -depth-selected `rows` and its immediate `subRows`. This replaces the previous -callable aggregation signature and its `fromRows` and `resolveDataValue` -properties while preserving the ability to choose either row set. - -## Grouped Cell Rendering - -`aggregatedCell` renders aggregate values on synthetic grouped rows. - -```ts -const amountColumn = columnHelper.accessor('amount', { - aggregationFn: 'sum', - aggregatedCell: ({ getValue }) => getValue().toLocaleString(), -}) -``` - -Use `cell.getIsAggregated()` to distinguish these cells. Grouping-only tables -do not expose this method; it belongs to `rowAggregationFeature`. - -## Providing Server or External Values - -A column can handle aggregation-value requests before local calculation: - -```ts -const amountColumn = columnHelper.accessor('amount', { - aggregationFn: 'sum', - getAggregationValue: ({ rows }) => { - if (rows !== undefined) return undefined // use local fallback for overrides - return { value: serverTotals.amount } - }, -}) -``` - -Returning `{ value }` marks the request as handled, including -`{ value: undefined }`. Returning `undefined` uses the local fallback. Put the -same provider on `defaultColumn` to share it across columns. - -Set `manualAggregation: true` to disable the local fallback for -`column.getAggregationValue()`. This is separate from `manualGrouping`, which -controls whether the grouped row model runs. - -## Built-in Definitions - -- `sum`: sums numeric values; non-numbers contribute zero. -- `count`: counts rows. -- `min` / `max`: find numeric or Date bounds. -- `extent`: returns `[min, max]`; an empty input returns - `[undefined, undefined]`. -- `mean`: averages numeric and number-like non-null values. -- `median`: requires every row value to be a number. -- `unique` / `uniqueCount`: use JavaScript `Set` semantics. -- `first` / `last`: return the positional value, including a nullish value. - -`aggregationFn: 'auto'` inspects the first core row value. Numbers resolve to a -registered `sum`, Dates resolve to a registered `extent`, and other values do -not resolve an aggregation. - -## Web Workers - -Worker-backed grouped row models eagerly compute explicitly configured grouped -aggregates in the worker. `column.getAggregationValue()` still executes its -final total on the main thread over the selected row model. Aggregation results -crossing the worker boundary must be structured-cloneable. - -Every adapter has a standalone example showing grand totals, multiple -aggregations, and pagination: - -- [Alpine](../framework/alpine/examples/aggregation) -- [Angular](../framework/angular/examples/aggregation) -- [Ember](../framework/ember/examples/aggregation) -- [Lit](../framework/lit/examples/aggregation) -- [Preact](../framework/preact/examples/aggregation) -- [React](../framework/react/examples/aggregation) -- [Solid](../framework/solid/examples/aggregation) -- [Svelte](../framework/svelte/examples/aggregation) -- [Vue](../framework/vue/examples/aggregation) -- [Vanilla](../framework/vanilla/examples/aggregation) - -Every framework adapter also has a grouped aggregation example combining the -same API with grouped rows and grand-total footers: - -- [Alpine](../framework/alpine/examples/grouped-aggregation) -- [Angular](../framework/angular/examples/grouped-aggregation) -- [Ember](../framework/ember/examples/grouped-aggregation) -- [Lit](../framework/lit/examples/grouped-aggregation) -- [Preact](../framework/preact/examples/grouped-aggregation) -- [React](../framework/react/examples/grouped-aggregation) -- [Solid](../framework/solid/examples/grouped-aggregation) -- [Svelte](../framework/svelte/examples/grouped-aggregation) -- [Vue](../framework/vue/examples/grouped-aggregation) diff --git a/docs/guide/client-side-vs-server-side.md b/docs/guide/client-side-vs-server-side.md new file mode 100644 index 0000000000..29353c745b --- /dev/null +++ b/docs/guide/client-side-vs-server-side.md @@ -0,0 +1,184 @@ +--- +title: Client-Side vs Server-Side Guide +--- + +## Examples + + + +# React + +- [With TanStack Query](../framework/react/examples/with-tanstack-query) + +# Preact + +- [With TanStack Query](../framework/preact/examples/with-tanstack-query) + +# Vue + +- [With TanStack Query](../framework/vue/examples/with-tanstack-query) + +# Solid + +- [With TanStack Query](../framework/solid/examples/with-tanstack-query) + +# Svelte + +- [With TanStack Query](../framework/svelte/examples/with-tanstack-query) + +# Angular + +- [With TanStack Query](../framework/angular/examples/with-tanstack-query) + + + +> [!IMPORTANT] TanStack Table supports both client-side and server-side row processing! + +More accurately, TanStack Table lets you bring your own backend or other manual approach to processing data for features such as filtering, grouping, sorting, expanding, aggregating, faceting, and pagination. + +TanStack Table can filter, group, sort, expand, aggregate, facet, and paginate rows in the browser through client-side [row model](./row-models) processing. It can also just manage the state for those features while your server performs the actual data processing. The right approach depends on how much data the browser receives, how expensive that data is to transfer and process, and the experience you want to provide. + +Whichever option you choose, you pretty much have to choose just one or the other. You can't really mix and match client-side and server-side row processing for different features. This guide covers that shared decision. The individual feature guides explain the options and row models for implementing each feature. + +## Start With the Simplest Approach That Fits + +Client-side processing is usually the simplest approach. This is where you let TanStack Table do that processing for you. Fetch the complete dataset for the table, pass it to TanStack Table, and enable the row models you need. Filtering, sorting, and pagination can then update immediately without another request. + +You might be tempted to rule out client-side processing based on what you think is a large dataset, but tables with just a few thousand rows are often practical in the browser. TanStack Table examples stress-test much larger datasets. In fact, we stress-test all TanStack Table features with 1 million rows client-side and expect usable performance. Actual performance depends on the number of columns, the size and shape of each row, the work performed by accessors and feature functions, and the devices your users have. Test with representative data and target hardware. + +Older versions of TanStack Table used to start running into memory issues at about 1 million rows, but thanks to our [Object Prototypes Refactor](https://tanstack.com//blog/tanstack-table-v9-memory-performance) we can now claim to support up to 15 million rows client-side with ease. Is loading 15 million rows practical for your use-case? Probably not! But we hope you know that TanStack Table should give you headroom many client-side rows. + +Server-side processing is usually a better fit when: + +- fetching the complete dataset would be slow, expensive, or memory-intensive; +- the browser only receives a page or another subset of the rows; +- queries, permissions, or business rules must be enforced by the backend; +- data changes frequently enough that downloading it all would become stale; or +- the backend can perform indexed searching, sorting, grouping, or aggregation more efficiently. + +When evaluating the tradeoff, measure: + +1. How long and how much it costs the server to query the complete dataset. +2. The transferred payload size, not only the number of rows. +3. Browser memory usage and the time spent computing row models. +4. The interaction cost of waiting for a request after each state change. + +If both approaches fit today, starting on the client keeps the data flow smaller. Owning the relevant table state makes it easier to move processing to the server later. + +## Keep Dataset-Wide Operations Consistent + +Filtering, grouping, sorting, and pagination usually describe one pipeline over the same dataset. If the server sends only part of that dataset, a client-side operation can only process the loaded rows. + +For example, client-side sorting after server-side pagination sorts the current page, not every matching row. Client-side filtering after server-side pagination can hide rows on the current page without finding matches on other pages. These results are usually misleading. + +As a rule, when the server owns pagination, it should also own any filtering, grouping, sorting, or aggregation that must apply to the full result set. Mixing approaches is valid when the smaller scope is intentional. For example, you might rank the rows within each server-provided group. Make that scope clear in the UI. + +Facets need the same consideration. Facet counts calculated from a server-provided page describe only that page. Calculate facets on the server when they need to represent the full filtered dataset. + +## What “Manual” Means + +TanStack Table calls server-side data processing “manual” because the table does not perform that transformation. A `manual*` option does not fetch or transform data. It tells the table to use the data you provide as already processed for that feature. + +| Operation | Manual option | Client-side row model or implementation | +| --------------------------- | ------------------- | --------------------------------------- | +| Column and global filtering | `manualFiltering` | `filteredRowModel` | +| Grouping | `manualGrouping` | `groupedRowModel` | +| Aggregation values | `manualAggregation` | `aggregationFn` local fallback | +| Sorting | `manualSorting` | `sortedRowModel` | +| Expanding | `manualExpanding` | `expandedRowModel` | +| Pagination | `manualPagination` | `paginatedRowModel` | + +Faceting also supports server-provided results, but it uses custom factories rather than a `manual*` option. See the [Aggregation](../framework/react/guide/aggregation) and [Faceting](../framework/react/guide/column-faceting#custom-server-side-faceting) guides for those feature-specific details. + +You can omit an unused client-side row model. If a shared table configuration includes one, its matching `manual*` option tells the table to bypass it. The feature itself can remain enabled so the table still supplies its state and APIs. + +## A Typical Server-Side Data Flow + +TanStack Table does not include server-side row models. You write the filtering, grouping, sorting, aggregation, and pagination logic in the backend language, SQL query layer, database API, or service architecture that fits your application. This is intentional. TanStack Table does not prescribe how your backend stores, queries, or processes data. + +TanStack Table also does not fetch data. Your application is responsible for sending the table state to the backend and providing the returned rows to the table. You can use any data-fetching solution you prefer. [TanStack Query](https://tanstack.com/query/latest) is an excellent companion when you want declarative fetching, caching, loading states, and request lifecycle management, and it composes naturally with controlled TanStack Table state. + +For server-side processing: + +1. Own the relevant filter, grouping, sorting, and pagination state so your data-fetching code can read it. +2. Include every server-owned state value in the request or query key. +3. Enable the matching `manual*` options. +4. Pass the processed rows returned by the server to `data`. +5. For manual pagination, also provide `rowCount` or `pageCount` when known. +6. Reset or validate the page index when filters, grouping, sorting, or page size change. +7. Keep previous results or show loading state deliberately, and prevent slower stale responses from replacing newer results. + +The table continues to manage state and expose event handlers; your application connects that state to its data-fetching layer. See the framework-specific Table State guide and the With TanStack Query example for complete patterns: + + + +# React + +- [Table State](../framework/react/guide/table-state) +- [With TanStack Query](../framework/react/examples/with-tanstack-query) + +# Preact + +- [Table State](../framework/preact/guide/table-state) +- [With TanStack Query](../framework/preact/examples/with-tanstack-query) + +# Octane + +- [Table State](../framework/octane/guide/table-state) + +# Vue + +- [Table State](../framework/vue/guide/table-state) +- [With TanStack Query](../framework/vue/examples/with-tanstack-query) + +# Solid + +- [Table State](../framework/solid/guide/table-state) +- [With TanStack Query](../framework/solid/examples/with-tanstack-query) + +# Svelte + +- [Table State](../framework/svelte/guide/table-state) +- [With TanStack Query](../framework/svelte/examples/with-tanstack-query) + +# Angular + +- [Table State](../framework/angular/guide/table-state) +- [With TanStack Query](../framework/angular/examples/with-tanstack-query) + +# Ember + +- [Table State](../framework/ember/guide/table-state) + +# Lit + +- [Table State](../framework/lit/guide/table-state) + +# Alpine + +- [Table State](../framework/alpine/guide/table-state) + +# Vanilla + +- [Table State](../framework/vanilla/guide/table-state) + + + +Use a stable backend identifier with `getRowId` when selection, expansion, or other row state must survive requests. Page-relative row indexes do not identify the same record reliably across server responses. + +## Rendering Is a Separate Decision + +Data processing and rendering solve different problems: + +- Pagination limits how many rows appear at once and can be performed on either the client or server. +- Virtualization renders only the visible portion of the rows already loaded in the browser. + +Virtualization can make a large client-side dataset inexpensive to render, but it does not reduce the amount of data fetched or the work needed to filter and sort that dataset. If the complete dataset is too large to load, use server-side operations or incremental fetching; then add virtualization if the loaded result is still large enough to make rendering expensive. + +## Choosing an Approach + +Use client-side processing when the browser can reasonably fetch and retain the complete dataset and immediate local interactions are valuable. Use server-side processing when the complete dataset should not or cannot be loaded, or when the backend must define the authoritative result. + +Choosing server-side processing means bringing your own implementation. TanStack Table supplies the state and APIs that describe what the user wants, but your backend code or SQL must process that state, and your data-fetching code must transport the request and response. No TanStack Table server-side model or built-in fetching layer is involved. This separation keeps TanStack Table compatible with any backend. TanStack Query is an optional companion when you want help managing the fetching and caching layer. + +Whichever approach you choose, test the full pipeline with realistic data. Network transfer, browser memory, row-model computation, DOM rendering, and backend query cost are separate bottlenecks, and the best boundary is the one that keeps all of them acceptable for your users. diff --git a/docs/guide/column-defs.md b/docs/guide/column-defs.md index 1cba40852a..c7e0e8044a 100644 --- a/docs/guide/column-defs.md +++ b/docs/guide/column-defs.md @@ -9,7 +9,7 @@ Column defs are the single most important part of building a table. They are res - Building the underlying data model that will be used for everything including sorting, filtering, grouping, etc. - Formatting the data model into what will be displayed in the table - Creating [header groups](../reference/index/interfaces/HeaderGroup_Core), [headers](../reference/index/interfaces/Header_Core) and footers -- Creating columns for display-only purposes, eg. action buttons, checkboxes, expanders, sparklines, etc. +- Creating columns for display-only purposes, e.g. action buttons, checkboxes, expanders, sparklines, etc. ## Column Def Types @@ -18,7 +18,7 @@ The following "types" of column defs aren't actually TypeScript types, but more - `Accessor Columns` - Accessor columns have an underlying data model which means they can be sorted, filtered, grouped, etc. - `Display Columns` - - Display columns do **not** have a data model which means they cannot be sorted, filtered, etc, but they can be used to display arbitrary content in the table, eg. a row actions button, checkbox, expander, etc. + - Display columns do **not** have a data model which means they cannot be sorted, filtered, etc, but they can be used to display arbitrary content in the table, e.g. a row actions button, checkbox, expander, etc. - `Grouping Columns` - Group columns do **not** have a data model so they too cannot be sorted, filtered, etc, and are used to group other columns together. It's common to define a header or footer for a column group. @@ -275,7 +275,7 @@ See the [Dynamic Columns example](../framework/react/examples/basic-dynamic-colu ## Column Formatting & Rendering -By default, column cells will display their data model value as a string. You can override this behavior by providing custom rendering implementations. Each implementation is provided with relevant information about the cell, header or footer and returns something your framework adapter can render eg. JSX/Components/strings/etc. This will depend on which adapter you are using. +By default, column cells will display their data model value as a string. You can override this behavior by providing custom rendering implementations. Each implementation is provided with relevant information about the cell, header or footer and returns something your framework adapter can render e.g. JSX/Components/strings/etc. This will depend on which adapter you are using. There are a couple of formatters available to you: diff --git a/docs/guide/data.md b/docs/guide/data.md index 121b8db4e9..0ae4655e1e 100644 --- a/docs/guide/data.md +++ b/docs/guide/data.md @@ -316,10 +316,6 @@ Later, in other parts of these docs, you will see how TanStack Table processes t ## How Much Data Can TanStack Table Handle? -Believe it or not, TanStack Table was actually built to scale up to handle potentially hundreds of thousands of rows of data in the client. Many of the official examples (such as the [Sorting](../framework/react/examples/sorting) and [Filters](../framework/react/examples/filters) examples) include a "Stress Test" button that loads 1 million client-side rows. This is obviously not always possible, depending on the size of each column's data and the number of columns. However, the sorting, filtering, pagination, and grouping features are all built with performance in mind for large datasets. +TanStack Table's client-side row models are built to handle large datasets, and the official examples include much larger stress tests. The practical limit depends on the payload, table features, rendering strategy, and target hardware, so test with representative data instead of choosing from row count alone. -Keep in mind that with large datasets, _rendering_ is usually the bottleneck rather than data processing. Sorting a million rows is fast; putting a million `` elements in the DOM is not. Client-side pagination solves this by rendering one page at a time, or you can render all rows through virtualization: see the [Virtualized Rows example](../framework/react/examples/virtualized-rows). - -The default mindset of a developer building a data grid is to implement server-side pagination, sorting, and filtering for large datasets. This is still usually a good idea, but a lot of developers underestimate how much data can actually be handled in the client with modern browsers and the right optimizations. If your table will never have more than a few thousand rows, you can probably take advantage of the client-side features in TanStack Table instead of implementing them yourself on the server. Before committing to letting TanStack Table's client-side features handle your large dataset, you should test it with your actual data to see if it performs well enough for your needs, of course. - -This is discussed in more detail in the [Pagination Guide](../framework/react/guide/pagination#should-you-use-client-side-pagination). +See the [Client-Side vs Server-Side Guide](./client-side-vs-server-side) for the complete decision framework and the distinction between data processing, pagination, and virtualization. diff --git a/docs/guide/features.md b/docs/guide/features.md new file mode 100644 index 0000000000..8b21d0d9b5 --- /dev/null +++ b/docs/guide/features.md @@ -0,0 +1,134 @@ +--- +title: Features Guide +--- + +## Why the `features` Option Is Required + +TanStack Table v9 requires a `features` option so that every table explicitly declares the capabilities it uses. In previous versions, we were cautious about expanding TanStack Table because every new capability risked adding code for all users, even when only a small percentage of applications needed it. That constraint helped keep the library small, but it also limited how much existing features could grow and how many new features we could offer. + +The v9 plugin architecture removes that constraint. Each feature can now grow in capability, and TanStack Table can offer more features without including all of their code in every application. The total potential bundle size increased from about 14 kB in v8 to about 25 kB in v9, but most users should receive a smaller bundle when they register only the features they need. An application that uses roughly half of TanStack Table's available capabilities no longer has to ship the other half. + +TanStack Table v9 is a headless, tree-shakable library built around a plugin architecture. A table always includes the small set of core features needed to create tables, columns, headers, rows, and cells. Other capabilities, including sorting, filtering, pagination, selection, and sizing, are opt-in. + +Each feature contributes its own state, options, defaults, lifecycle hooks, and APIs to the table and its related objects. The feature object you register also controls the TypeScript surface: APIs for an omitted feature do not appear on the table, columns, rows, headers, or cells. + +## Choose your features with `tableFeatures` + +Every table declares its static feature set with `tableFeatures()`. A table that only needs core behavior uses an empty feature object: + +```ts +import { tableFeatures, useTable } from '@tanstack/react-table' + +const features = tableFeatures({}) + +function Table() { + const table = useTable({ + features, + columns, + data, + }) + + // render the table... +} +``` + +Import and register only the optional features that table needs: + +```ts +import { + columnFilteringFeature, + rowPaginationFeature, + rowSortingFeature, + tableFeatures, + useTable, +} from '@tanstack/react-table' + +const features = tableFeatures({ + columnFilteringFeature, + rowSortingFeature, + rowPaginationFeature, +}) +``` + +Define the feature object outside the component when possible. It is static configuration, and keeping it stable avoids rebuilding table structures and gives shared column definitions and helpers one reusable `typeof features` type. + +`tableFeatures()` also validates relationships between slots. For example, a `sortedRowModel` requires `rowSortingFeature`, while `globalFilteringFeature` requires `columnFilteringFeature`. Missing prerequisites produce a type error that names the required feature. + +## Features, Row Models, and Functions Are Separate + +A feature adds state and APIs, but it does not perform client-side data processing by itself. Client-side row models are optional. Add the corresponding row-model factory only when TanStack Table should process the data in the browser. If your server performs that work, keep the feature for its state and APIs, and omit the client-side row model. + +The following example enables client-side filtering, sorting, and pagination: + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + filterFn_includesString, + rowPaginationFeature, + rowSortingFeature, + sortFn_alphanumeric, + tableFeatures, +} from '@tanstack/react-table' + +const features = tableFeatures({ + columnFilteringFeature, + rowSortingFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns: { includesString: filterFn_includesString }, + sortFns: { alphanumeric: sortFn_alphanumeric }, +}) +``` + +The pieces have distinct jobs: + +- Feature objects provide state, options, event handlers, and instance APIs. +- Row models perform client-side filtering, grouping, sorting, expansion, pagination, and faceting. +- Function registries provide named filter, sort, and aggregation implementations. + +Register only the functions you use. Importing individual functions preserves tree-shaking; spreading a complete built-in function registry includes every implementation in that registry. A function passed directly to a column option does not need to be registered by name. + +For server-side processing, keep the feature for its state and APIs, omit the client-side row model, and configure the matching `manual*` option where applicable. See the [Row Models Guide](./row-models) and [Client-Side vs Server-Side Guide](./client-side-vs-server-side) for those data-flow choices. + +## Using Every Stock Feature + +`stockFeatures` is the complete collection of optional features. It is convenient for prototypes, shared compatibility layers, or tables that genuinely use most features: + +```ts +import { stockFeatures, tableFeatures } from '@tanstack/react-table' + +const features = tableFeatures({ + ...stockFeatures, +}) +``` + +Because this imports the complete collection, it includes all stock feature code. Prefer individual feature imports when bundle size and tree-shaking matter. `stockFeatures` also does not add client-side row models or function registries; add those separately when needed. + +## Stock Features + +- `cellSelectionFeature`: [Cell Selection](../framework/react/guide/cell-selection) adds cell-range selection state and APIs. +- `cellSpanningFeature`: [Cell Spanning](../framework/react/guide/cell-spanning) lets cells span rows or columns. +- `columnFacetingFeature`: [Faceting](../framework/react/guide/column-faceting) derives unique values, ranges, and counts for filtering UIs. +- `columnFilteringFeature`: [Column Filtering](../framework/react/guide/column-filtering) filters individual columns and provides column-filter state. +- `columnGroupingFeature`: [Grouping](../framework/react/guide/grouping) groups rows by column values. +- `columnOrderingFeature`: [Column Ordering](../framework/react/guide/column-ordering) controls the order of columns. +- `columnPinningFeature`: [Column Pinning](../framework/react/guide/column-pinning) pins columns to the left or right side of a table. +- `columnResizingFeature`: [Column Resizing](../framework/react/guide/column-resizing) adds resize interactions and builds on column sizing. +- `columnSizingFeature`: [Column Sizing](../framework/react/guide/column-sizing) stores and exposes column width information. +- `columnVisibilityFeature`: [Column Visibility](../framework/react/guide/column-visibility) shows and hides columns. +- `globalFilteringFeature`: [Global Filtering](../framework/react/guide/global-filtering) filters rows across multiple columns and builds on column filtering. +- `rowAggregationFeature`: [Aggregation](../framework/react/guide/aggregation) calculates totals and other aggregate values over row sets. +- `rowExpandingFeature`: [Expanding](../framework/react/guide/expanding) controls expanded rows and visible sub-rows. +- `rowPaginationFeature`: [Pagination](../framework/react/guide/pagination) provides page state, navigation APIs, and optional client-side pagination. +- `rowPinningFeature`: [Row Pinning](../framework/react/guide/row-pinning) pins rows to the top or bottom of the table. +- `rowSelectionFeature`: [Row Selection](../framework/react/guide/row-selection) manages selected-row state and APIs. +- `rowSortingFeature`: [Sorting](../framework/react/guide/sorting) manages sort state and optional client-side row ordering. + +These are the features included by `stockFeatures`. Fuzzy filtering is a filtering recipe built from the filtering and sorting features, while virtualization is provided by TanStack Virtual rather than a Table feature. + +To build and type your own plugin, see the framework-specific [Custom Plugins guide](../framework/react/guide/custom-features). diff --git a/docs/reference/index/interfaces/ColumnDef_RowSorting.md b/docs/reference/index/interfaces/ColumnDef_RowSorting.md index 6cef0a1341..9d01476fcd 100644 --- a/docs/reference/index/interfaces/ColumnDef_RowSorting.md +++ b/docs/reference/index/interfaces/ColumnDef_RowSorting.md @@ -51,7 +51,7 @@ optional invertSorting: boolean; Defined in: [features/row-sorting/rowSortingFeature.types.ts:133](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts#L133) -Inverts the order of the sorting for this column. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring +Inverts the order of the sorting for this column. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring *** diff --git a/docs/reference/index/interfaces/TableOptions_Core.md b/docs/reference/index/interfaces/TableOptions_Core.md index 6f881ae185..625f2de224 100644 --- a/docs/reference/index/interfaces/TableOptions_Core.md +++ b/docs/reference/index/interfaces/TableOptions_Core.md @@ -140,7 +140,7 @@ optional getRowId: (originalRow, index, parent?) => string; Defined in: [core/rows/coreRowsFeature.types.ts:115](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/rows/coreRowsFeature.types.ts#L115) -This optional function is used to derive a unique ID for any given row. If not provided the row's index is used (nested rows join together with `.` using their grandparents' index eg. `index.index.index`). If you need to identify individual rows that are originating from any server-side operations, it's suggested you use this function to return an ID that makes sense regardless of network IO/ambiguity eg. a userId, taskId, database ID field, etc. +This optional function is used to derive a unique ID for any given row. If not provided the row's index is used (nested rows join together with `.` using their grandparents' index e.g. `index.index.index`). If you need to identify individual rows that are originating from any server-side operations, it's suggested you use this function to return an ID that makes sense regardless of network IO/ambiguity e.g. a userId, taskId, database ID field, etc. #### Parameters diff --git a/docs/reference/index/interfaces/TableOptions_RowPagination.md b/docs/reference/index/interfaces/TableOptions_RowPagination.md index 3beba70b00..b9bba537ab 100644 --- a/docs/reference/index/interfaces/TableOptions_RowPagination.md +++ b/docs/reference/index/interfaces/TableOptions_RowPagination.md @@ -17,7 +17,7 @@ optional autoResetPageIndex: boolean; Defined in: [features/row-pagination/rowPaginationFeature.types.ts:18](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts#L18) -If set to `true`, pagination will be reset to the first page when page-altering state changes eg. `data` is updated, filters change, grouping changes, etc. +If set to `true`, pagination will be reset to the first page when page-altering state changes e.g. `data` is updated, filters change, grouping changes, etc. *** diff --git a/docs/reference/index/interfaces/TableOptions_Rows.md b/docs/reference/index/interfaces/TableOptions_Rows.md index 4a05f198c5..1bee0efe4b 100644 --- a/docs/reference/index/interfaces/TableOptions_Rows.md +++ b/docs/reference/index/interfaces/TableOptions_Rows.md @@ -31,7 +31,7 @@ optional getRowId: (originalRow, index, parent?) => string; Defined in: [core/rows/coreRowsFeature.types.ts:115](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/rows/coreRowsFeature.types.ts#L115) -This optional function is used to derive a unique ID for any given row. If not provided the row's index is used (nested rows join together with `.` using their grandparents' index eg. `index.index.index`). If you need to identify individual rows that are originating from any server-side operations, it's suggested you use this function to return an ID that makes sense regardless of network IO/ambiguity eg. a userId, taskId, database ID field, etc. +This optional function is used to derive a unique ID for any given row. If not provided the row's index is used (nested rows join together with `.` using their grandparents' index e.g. `index.index.index`). If you need to identify individual rows that are originating from any server-side operations, it's suggested you use this function to return an ID that makes sense regardless of network IO/ambiguity e.g. a userId, taskId, database ID field, etc. #### Parameters diff --git a/examples/react/kitchen-sink/package.json b/examples/react/kitchen-sink/package.json index 34db03e548..ecbc30b390 100644 --- a/examples/react/kitchen-sink/package.json +++ b/examples/react/kitchen-sink/package.json @@ -19,6 +19,7 @@ "@faker-js/faker": "^10.5.0", "@tanstack/match-sorter-utils": "^9.0.0-beta.53", "@tanstack/react-devtools": "^0.10.9", + "@tanstack/react-hotkeys": "^0.10.0", "@tanstack/react-pacer": "^0.22.1", "@tanstack/react-router": "^1.170.18", "@tanstack/react-start": "^1.168.27", diff --git a/examples/react/kitchen-sink/src/index.css b/examples/react/kitchen-sink/src/index.css index 66bd45a751..400a916d91 100644 --- a/examples/react/kitchen-sink/src/index.css +++ b/examples/react/kitchen-sink/src/index.css @@ -289,6 +289,47 @@ html { background: #ff000042; } +/* Cell selection uses inset shadows so selecting cells does not change the + * collapsed-border table's layout. Spanned cells participate as one unit. */ +.cell-selectable { + cursor: cell; + user-select: none; + --cell-edge-top: 0 0 0 0 transparent; + --cell-edge-right: 0 0 0 0 transparent; + --cell-edge-bottom: 0 0 0 0 transparent; + --cell-edge-left: 0 0 0 0 transparent; + box-shadow: + inset var(--cell-edge-top), + inset var(--cell-edge-right), + inset var(--cell-edge-bottom), + inset var(--cell-edge-left); +} + +.cell-selected { + background-color: #dbeafe !important; +} + +.cell-focused { + outline: 1px dashed #2563eb; + outline-offset: -3px; +} + +.cell-edge-top { + --cell-edge-top: 0 2px 0 0 #2563eb; +} + +.cell-edge-right { + --cell-edge-right: -2px 0 0 0 #2563eb; +} + +.cell-edge-bottom { + --cell-edge-bottom: 0 -2px 0 0 #2563eb; +} + +.cell-edge-left { + --cell-edge-left: 2px 0 0 0 #2563eb; +} + /* Row pinning — light blue tint + outline so pinned rows are obvious * against sticky positioning. Outline applied via inset box-shadow on each * cell (outline on doesn't render well with border-collapse: collapse). */ diff --git a/examples/react/kitchen-sink/src/routes/index.tsx b/examples/react/kitchen-sink/src/routes/index.tsx index f2f5798ae4..2150bc014b 100644 --- a/examples/react/kitchen-sink/src/routes/index.tsx +++ b/examples/react/kitchen-sink/src/routes/index.tsx @@ -23,6 +23,8 @@ import { import { useTanStackTableDevtools } from '@tanstack/react-table-devtools' import { compareItems, rankItem } from '@tanstack/match-sorter-utils' import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer' +import { useHotkeys } from '@tanstack/react-hotkeys' +import { useCreateAtom } from '@tanstack/react-store' import { DndContext, KeyboardSensor, @@ -47,6 +49,8 @@ import type { RankingInfo } from '@tanstack/match-sorter-utils' import type { Person } from '../makeData' import type { Cell, + CellSelectionBounds, + CellSelectionState, Column, FilterFn, Header, @@ -54,6 +58,7 @@ import type { Row, SortFn, TableFeatures, + TableState, } from '@tanstack/react-table' // ===================================================================== @@ -132,6 +137,11 @@ const features = tableFeatures({ aggregationFns, }) +type KitchenSinkSelectedState = Omit< + TableState, + 'cellSelection' +> + // ===================================================================== // Custom status sort (from sorting example) // ===================================================================== @@ -310,7 +320,7 @@ function DraggableTableHeader({ table, }: { header: Header - table: ReactTable + table: ReactTable }) { const { attributes, isDragging, listeners, setNodeRef, transform } = useSortable({ id: header.column.id }) @@ -455,13 +465,21 @@ function DragAlongCell({ table, }: { cell: Cell - table: ReactTable + table: ReactTable }) { const { isDragging, setNodeRef, transform } = useSortable({ id: cell.column.id, }) const pinningStyles = getCommonPinningStyles(cell.column) + const rowSpan = cell.getRowSpan() + const colSpan = cell.getColSpan() + + // A span of 0 marks a cell covered by another cell. Rendering rowSpan={0} + // would mean "span to the end of the row group" in HTML, so covered cells + // must be omitted entirely. + if (rowSpan === 0 || colSpan === 0) return null + const style: CSSProperties = { ...pinningStyles, opacity: isDragging ? 0.8 : (pinningStyles.opacity ?? 1), @@ -478,7 +496,7 @@ function DragAlongCell({ // aggregated highlight from showing on plain nested data. const groupingActive = table.state.grouping.length > 0 const hasAggregation = !!cell.column.columnDef.aggregationFn - const className = !groupingActive + const groupingClassName = !groupingActive ? undefined : cell.getIsGrouped() ? 'cell-grouped' @@ -488,12 +506,46 @@ function DragAlongCell({ ? 'cell-placeholder' : undefined + const selectionClassNames = cell.getCanSelect() + ? (() => { + const isSelected = cell.getIsSelected() + const edges = isSelected ? cell.getSelectionEdges() : undefined + + return [ + 'cell-selectable', + isSelected && 'cell-selected', + cell.getIsFocused() && 'cell-focused', + edges?.top && 'cell-edge-top', + edges?.right && 'cell-edge-right', + edges?.bottom && 'cell-edge-bottom', + edges?.left && 'cell-edge-left', + ] + })() + : [] + + const className = [groupingClassName, ...selectionClassNames] + .filter(Boolean) + .join(' ') + // FlexRender now handles aggregatedCell / placeholder dispatch internally // (returns null for placeholders, picks aggregatedCell when available). // The only state we still wrap manually is the grouped-cell expander — // that's UI specific to this example. return ( - + {cell.getIsGrouped() ? ( +
@@ -831,7 +1005,7 @@ function App() { {/* Table */} -
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -856,17 +1030,36 @@ function App() { ))} {table.getCenterRows().map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - - - ))} - + + rowSelectionKey( + ranges, + table.getCellSelectionBounds(), + row.getDisplayIndex(), + row.id, + ) + } + > + {() => ( + + {row.getVisibleCells().map((cell) => ( + + + + ))} + + )} + ))} {table.getBottomRows().map((row) => ( diff --git a/examples/react/kitchen-sink/tests/e2e/smoke.spec.ts b/examples/react/kitchen-sink/tests/e2e/smoke.spec.ts index 72f3570583..e067db8e46 100644 --- a/examples/react/kitchen-sink/tests/e2e/smoke.spec.ts +++ b/examples/react/kitchen-sink/tests/e2e/smoke.spec.ts @@ -53,3 +53,42 @@ test('renders the table without crashing', async ({ page }) => { await server.close() } }) + +test('exposes cell selection and spans adjacent status values', async ({ + page, +}) => { + const { errors, server } = await openExample(page) + + try { + const table = page.locator('table').first() + await expect( + table.locator('tbody td.cell-selectable').first(), + ).toBeVisible() + await expect( + page.getByRole('button', { name: 'Select all cells' }), + ).toBeVisible() + + const statusHeader = table.locator('th').filter({ hasText: 'Status' }) + await statusHeader.locator('.sortable-header').click() + await expect(table.locator('tbody td[rowspan="2"]').first()).toBeVisible() + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) + +test('does not render undefined names on grouped rows', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = page.locator('table').first() + const visitsHeader = table.locator('th').filter({ hasText: 'Visits' }) + + await visitsHeader.getByTitle('Group by this column').click() + + await expect(table.locator('tbody')).not.toContainText('undefined') + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/packages/table-core/src/core/rows/coreRowsFeature.types.ts b/packages/table-core/src/core/rows/coreRowsFeature.types.ts index 52997c915d..8a38bed8c9 100644 --- a/packages/table-core/src/core/rows/coreRowsFeature.types.ts +++ b/packages/table-core/src/core/rows/coreRowsFeature.types.ts @@ -109,7 +109,7 @@ export interface TableOptions_Rows< in out TData extends RowData, > { /** - * This optional function is used to derive a unique ID for any given row. If not provided the row's index is used (nested rows join together with `.` using their grandparents' index eg. `index.index.index`). If you need to identify individual rows that are originating from any server-side operations, it's suggested you use this function to return an ID that makes sense regardless of network IO/ambiguity eg. a userId, taskId, database ID field, etc. + * This optional function is used to derive a unique ID for any given row. If not provided the row's index is used (nested rows join together with `.` using their grandparents' index e.g. `index.index.index`). If you need to identify individual rows that are originating from any server-side operations, it's suggested you use this function to return an ID that makes sense regardless of network IO/ambiguity e.g. a userId, taskId, database ID field, etc. * @example getRowId: row => row.userId */ getRowId?: ( diff --git a/packages/table-core/src/features/cell-selection/cellSelectionFeature.utils.ts b/packages/table-core/src/features/cell-selection/cellSelectionFeature.utils.ts index 410a006e93..e7c404dd35 100644 --- a/packages/table-core/src/features/cell-selection/cellSelectionFeature.utils.ts +++ b/packages/table-core/src/features/cell-selection/cellSelectionFeature.utils.ts @@ -896,13 +896,16 @@ function stepCoordinate( columnId: string, direction: CellSelectionDirection, ): { rowId: string; columnId: string } | null { - const rows = table.getRowsInDisplayOrder() + // Navigation is constrained to the final row model. In particular, the + // pre-pagination display-order model contains rows from every page and + // would let ArrowDown move focus into a row that is not rendered. + const rows = table.getRowModel().rows const columns = getDisplayOrderedColumns(table) if (!rows.length || !columns.length) return null const { rowDelta, columnDelta } = getDirectionDelta(direction) - const rowIndex = resolveRowIndex(table, rows, rowId) + const rowIndex = rows.findIndex((row) => row.id === rowId) const columnIndex = columns.findIndex((column) => column.id === columnId) if (rowIndex < 0 || columnIndex < 0) return null @@ -915,7 +918,7 @@ function stepCoordinate( 'getCellSelectionMergeBounds', table_getCellSelectionMergeBounds, ) - let fromRowIndex = rowIndex + let fromRowIndex = rows[rowIndex]!.getDisplayIndex() let fromColumnIndex = columnIndex if (merges.length) { @@ -928,7 +931,15 @@ function stepCoordinate( } } - const nextRowIndex = fromRowIndex + rowDelta + let nextRowIndex = rowIndex + rowDelta + + if (rowDelta && fromRowIndex !== rows[rowIndex]!.getDisplayIndex()) { + const edgeRowIndex = rows.findIndex( + (row) => row.getDisplayIndex() === fromRowIndex, + ) + if (edgeRowIndex < 0) return null + nextRowIndex = edgeRowIndex + rowDelta + } if (nextRowIndex < 0 || nextRowIndex >= rows.length) { return null @@ -980,13 +991,17 @@ function stepCoordinate( let landingColumnIndex = nextColumnIndex if (merges.length) { + const landingDisplayRowIndex = rows[nextRowIndex]!.getDisplayIndex() const landingMerge = findMergeBoundsAt( merges, - nextRowIndex, + landingDisplayRowIndex, nextColumnIndex, ) if (landingMerge) { - landingRowIndex = landingMerge.minRowIndex + landingRowIndex = rows.findIndex( + (row) => row.getDisplayIndex() === landingMerge.minRowIndex, + ) + if (landingRowIndex < 0) return null landingColumnIndex = landingMerge.minColumnIndex } } @@ -1021,7 +1036,7 @@ export function table_moveCellSelection< const active = ranges?.[ranges.length - 1] if (!active) { - const rows = table.getRowsInDisplayOrder() + const rows = table.getRowModel().rows const columns = getSelectableColumns(table) if (!rows.length || !columns.length) return diff --git a/packages/table-core/src/features/column-grouping/createGroupedRowModel.ts b/packages/table-core/src/features/column-grouping/createGroupedRowModel.ts index 25ee2e7054..718fcc8a26 100644 --- a/packages/table-core/src/features/column-grouping/createGroupedRowModel.ts +++ b/packages/table-core/src/features/column-grouping/createGroupedRowModel.ts @@ -78,10 +78,11 @@ function _createGroupedRowModel< const grouping = table.atoms.grouping?.get() if (!rowModel.rows.length || !grouping?.length) { - rowModel.rows.forEach((row) => { - row.depth = 0 - row.parentId = undefined - }) + // A previous grouped pass rewrote depth/parentId on these shared row + // objects, shifting the whole tree down by the number of grouping levels. + // Restore the natural relationships all the way down, not just at the + // top level. + resetRowRelationships(rowModel.rows, 0, undefined) return rowModel } @@ -231,6 +232,24 @@ function _createGroupedRowModel< } } +function resetRowRelationships< + TFeatures extends TableFeatures, + TData extends RowData, +>( + rows: Array>, + depth: number, + parentId: string | undefined, +) { + for (let i = 0; i < rows.length; i++) { + const row = rows[i]! + row.depth = depth + row.parentId = parentId + if (row.subRows.length) { + resetRowRelationships(row.subRows, depth + 1, row.id) + } + } +} + function groupBy( table: Table_Internal, rows: Array>, diff --git a/packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts b/packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts index 45456281e9..4446c03864 100644 --- a/packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts +++ b/packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts @@ -13,7 +13,7 @@ export interface TableState_RowPagination { export interface TableOptions_RowPagination { /** - * If set to `true`, pagination will be reset to the first page when page-altering state changes eg. `data` is updated, filters change, grouping changes, etc. + * If set to `true`, pagination will be reset to the first page when page-altering state changes e.g. `data` is updated, filters change, grouping changes, etc. */ autoResetPageIndex?: boolean /** diff --git a/packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts b/packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts index 443e3ae6b5..264e14aed3 100644 --- a/packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts +++ b/packages/table-core/src/features/row-sorting/rowSortingFeature.types.ts @@ -128,7 +128,7 @@ export interface ColumnDef_RowSorting< */ enableSorting?: boolean /** - * Inverts the order of the sorting for this column. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring + * Inverts the order of the sorting for this column. This is useful for values that have an inverted best/worst scale where lower numbers are better, e.g. a ranking (1st, 2nd, 3rd) or golf-like scoring */ invertSorting?: boolean /** diff --git a/packages/table-core/tests/implementation/features/cell-selection/cellSelectionFeature.test.ts b/packages/table-core/tests/implementation/features/cell-selection/cellSelectionFeature.test.ts index 3ee4cd0c9d..2187c2f9d5 100644 --- a/packages/table-core/tests/implementation/features/cell-selection/cellSelectionFeature.test.ts +++ b/packages/table-core/tests/implementation/features/cell-selection/cellSelectionFeature.test.ts @@ -3,6 +3,8 @@ import { cellSelectionFeature, columnVisibilityFeature, constructTable, + createPaginatedRowModel, + rowPaginationFeature, } from '../../../../src' import { testFeatures } from '../../../fixtures/features' import type { CellSelectionState, ColumnDef, Table } from '../../../../src' @@ -10,6 +12,8 @@ import type { CellSelectionState, ColumnDef, Table } from '../../../../src' const features = testFeatures({ cellSelectionFeature, columnVisibilityFeature, + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), }) interface TestRow { @@ -473,6 +477,41 @@ describe('cellSelectionFeature', () => { expect(table.getSelectedCellCount()).toBe(1) }) + + it('keeps movement within the current pagination page', () => { + const table = makeTable({ + data: makeData(6), + initialState: { + pagination: { pageIndex: 1, pageSize: 2 }, + }, + }) + + table.moveCellSelection('down') + expect(table.getSelectedCellIds()).toEqual(['r2_a']) + + table.moveCellSelection('down') + expect(table.getSelectedCellIds()).toEqual(['r3_a']) + + table.moveCellSelection('down') + expect(table.getSelectedCellIds()).toEqual(['r3_a']) + }) + + it('keeps range extension within the current pagination page', () => { + const table = makeTable({ + data: makeData(6), + initialState: { + pagination: { pageIndex: 1, pageSize: 2 }, + }, + }) + + table.setFocusedCell('r2', 'a') + table.extendCellSelection('down') + table.extendCellSelection('down') + + expect(table.atoms.cellSelection.get()).toEqual([ + rangeOf('r2', 'a', 'r3', 'a'), + ]) + }) }) describe('derived data', () => { diff --git a/packages/table-core/tests/implementation/features/column-grouping/createGroupedRowModel.test.ts b/packages/table-core/tests/implementation/features/column-grouping/createGroupedRowModel.test.ts index 34714395ad..da511b8d92 100644 --- a/packages/table-core/tests/implementation/features/column-grouping/createGroupedRowModel.test.ts +++ b/packages/table-core/tests/implementation/features/column-grouping/createGroupedRowModel.test.ts @@ -436,12 +436,18 @@ describe('createGroupedRowModel ungrouping reset', () => { }) }) - it('should reset depth and parentId on top-level tree rows after setGrouping([])', () => { + it('should reset depth and parentId on nested tree rows after setGrouping([])', () => { const data: Array = [ { status: 'x', firstName: 'parent1', - subRows: [{ status: 'x', firstName: 'child1' }], + subRows: [ + { + status: 'x', + firstName: 'child1', + subRows: [{ status: 'x', firstName: 'grandchild1' }], + }, + ], }, { status: 'y', firstName: 'parent2' }, ] @@ -451,6 +457,7 @@ describe('createGroupedRowModel ungrouping reset', () => { expect(grouped.rowsById['0']!.depth).toBe(1) expect(grouped.rowsById['0']!.parentId).toBe('status:x') expect(grouped.rowsById['0.0']!.depth).toBe(2) + expect(grouped.rowsById['0.0.0']!.depth).toBe(3) table.setGrouping([]) const ungrouped = table.getGroupedRowModel() @@ -459,12 +466,12 @@ describe('createGroupedRowModel ungrouping reset', () => { expect(row.depth).toBe(0) expect(row.parentId).toBeUndefined() }) - // Pinning current behavior (possible bug): the ungrouping reset branch - // only rewrites top-level rows, so nested tree descendants keep the - // shifted depth (2) from the previous grouped pass instead of returning - // to their natural tree depth (1). - expect(ungrouped.rowsById['0.0']!.depth).toBe(2) + // Nested descendants return to their natural tree depth instead of + // keeping the shifted depth from the previous grouped pass. + expect(ungrouped.rowsById['0.0']!.depth).toBe(1) expect(ungrouped.rowsById['0.0']!.parentId).toBe('0') + expect(ungrouped.rowsById['0.0.0']!.depth).toBe(2) + expect(ungrouped.rowsById['0.0.0']!.parentId).toBe('0.0') }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d51f89bbb..e509884478 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8983,6 +8983,9 @@ importers: '@tanstack/react-devtools': specifier: ^0.10.9 version: 0.10.9(@types/react-dom@19.2.4(@types/react@19.2.16))(@types/react@19.2.16)(csstype@3.2.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.14) + '@tanstack/react-hotkeys': + specifier: ^0.10.0 + version: 0.10.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tanstack/react-pacer': specifier: ^0.22.1 version: 0.22.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)