Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions beta-window-triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ All four implemented on beta in one coordinated change (`rowExpandingFeature.uti
- **[#5147](https://github.com/TanStack/table/issues/5147) + [#5832](https://github.com/TanStack/table/issues/5832) (high, same root cause)** — `column_getAutoSortDir` samples only `flatRows[0]` (lines 178-192); leading null or manual-sorting data swap flips the toggle cycle and silently drops a state. Fix: sample first N rows for first non-nullish value (match `column_getAutoSortFn`'s N=10). Close [#5832](https://github.com/TanStack/table/issues/5832) as duplicate when the fix lands.
- **[#5653](https://github.com/TanStack/table/issues/5653) (optional semantic half)** — default `sortUndefined: 1` short-circuits before the user's `sortFn` (`createSortedRowModel.ts:104-124`). Current behavior defensible; decide during beta whether to change. The docs half (sorting.md wrongly describes `sortUndefined: false`) is ANYTIME.

### Cluster 5: Filtering semantics
### Cluster 5: Filtering semantics — FIX IMPLEMENTED 2026-08-02 (pending PR)

- **[#6007](https://github.com/TanStack/table/issues/6007) (high)** — `filterFn_inNumberRange` has no numeric guard (`filterFns.ts:283-287`); `null`/`''`/`false` coerce to 0 and pass zero-spanning ranges on the auto-selected default number filter. **Vehicle: PR [#6313](https://github.com/TanStack/table/pull/6313) (needs rebase).**
- **[#5987](https://github.com/TanStack/table/issues/5987) (med)** — with `maxLeafRowFilterDepth: 0`, kept parents' visible descendants never enter `flatRows`/`rowsById` (`filterRowsUtils.ts:130-146`), under-counting facets. **Vehicle: PR [#6361](https://github.com/TanStack/table/pull/6361) (Test check failing, stale since June).**
- **[#6101](https://github.com/TanStack/table/issues/6101) (med)** — `shouldAutoRemoveFilter` ORs a hardcoded empty-string check over a custom `autoRemove` (`columnFilteringFeature.utils.ts:347-353`); redefine the contract so a provided `autoRemove` is authoritative. Built-ins unaffected (all already `testFalsy`). No PR yet (old v8 PRs [#6178](https://github.com/TanStack/table/pull/6178)/[#6195](https://github.com/TanStack/table/pull/6195)).
- **[#6081](https://github.com/TanStack/table/issues/6081) (med)** — custom `getFacetedUniqueValues` frozen by two cache layers (per-column factory cache `columnFacetingFeature.utils.ts:97-101` + stock `flatRows` memoDeps applied to custom impls `columnFacetingFeature.ts:42-52`). Fix requires deciding the caching contract for custom row-model factories — a public extension-point semantics decision. Old fix PR [#6417](https://github.com/TanStack/table/pull/6417) was closed by its author; needs fresh beta design.
All four implemented on beta in one coordinated change, with new tests, all-frameworks guide updates (column-filtering `autoRemove` contract + column-faceting custom-factory contract and reworked server-side examples), and changeset `filtering-semantics-fixes`:

- **[#6007](https://github.com/TanStack/table/issues/6007)** — `filterFn_inNumberRange.filter` now guards `typeof dataValue !== 'number' || Number.isNaN(dataValue)` (mirrors vehicle PR [#6313](https://github.com/TanStack/table/pull/6313), which can be closed as superseded). Note: numeric strings also stop matching, slightly beyond the PR's stated scope; called out in the changeset.
- **[#5987](https://github.com/TanStack/table/issues/5987)** — root-down filter path now pushes depth-truncated descendants of kept rows into `flatRows`/`rowsById` (ports vehicle PR [#6361](https://github.com/TanStack/table/pull/6361), which can be closed as superseded). Scoped to `filterRowModelFromRoot` like the PR; the leaf-up path drops truncated subRows entirely (pre-existing v8 behavior) and was left alone as a possible follow-up.
- **[#6101](https://github.com/TanStack/table/issues/6101)** — contract redefined: a provided `autoRemove` is authoritative for defined values (empty strings can now be kept); `undefined` always clears (universal sentinel). Built-ins unaffected.
- **[#6081](https://github.com/TanStack/table/issues/6081)** — caching contract decided: custom row-model factories resolve once per table/column (consistent with every other row model) and their returned functions run on every read; the redundant API-layer memoDeps in `columnFacetingFeature.ts` (which froze custom impls) are removed. Stock factories still memoize internally, so reference stability is preserved. Docs examples reworked to read live data via `table.options.meta` (react/vue/preact/solid/svelte were stale-closure patterns; angular/lit/alpine/ember/octane were already fine).

### Cluster 6: Group-column second-class citizenship

Expand Down Expand Up @@ -109,9 +111,9 @@ All four implemented on beta in one coordinated change (`rowExpandingFeature.uti
1. **[#5968](https://github.com/TanStack/table/issues/5968)** first-run auto-reset guard + `resetPageIndex` honoring `initialState` (unblocks [#6443](https://github.com/TanStack/table/pull/6443)).
2. **[#6442](https://github.com/TanStack/table/pull/6442)** undraft + merge (closes [#5770](https://github.com/TanStack/table/issues/5770)/[#5497](https://github.com/TanStack/table/issues/5497)); close [#4919](https://github.com/TanStack/table/issues/4919), [#5138](https://github.com/TanStack/table/issues/5138), [#5202](https://github.com/TanStack/table/issues/5202), and superseded PRs [#5823](https://github.com/TanStack/table/pull/5823)/[#6177](https://github.com/TanStack/table/pull/6177).
3. Row-selection coordinated PR ([#6049](https://github.com/TanStack/table/issues/6049) + [#5116](https://github.com/TanStack/table/issues/5116) + [#5398](https://github.com/TanStack/table/issues/5398)).
4. **[#6313](https://github.com/TanStack/table/pull/6313)** rebase + merge ([#6007](https://github.com/TanStack/table/issues/6007)); **[#6361](https://github.com/TanStack/table/pull/6361)** fix CI + merge ([#5987](https://github.com/TanStack/table/issues/5987)); **[#6443](https://github.com/TanStack/table/pull/6443)** rebase + merge ([#5801](https://github.com/TanStack/table/issues/5801)).
4. **[#6443](https://github.com/TanStack/table/pull/6443)** rebase + merge ([#5801](https://github.com/TanStack/table/issues/5801)). (Cluster 5 — [#6007](https://github.com/TanStack/table/issues/6007), [#5987](https://github.com/TanStack/table/issues/5987), [#6101](https://github.com/TanStack/table/issues/6101), [#6081](https://github.com/TanStack/table/issues/6081) — implemented 2026-08-02, pending PR; close [#6313](https://github.com/TanStack/table/pull/6313)/[#6361](https://github.com/TanStack/table/pull/6361) as superseded when it lands.)
5. `_valuesCache`/`defaultColumn` invalidation pair ([#5363](https://github.com/TanStack/table/issues/5363)/[#4485](https://github.com/TanStack/table/issues/4485) + [#5275](https://github.com/TanStack/table/issues/5275)).
6. Sorting defaults ([#4946](https://github.com/TanStack/table/issues/4946) one-liner; [#5147](https://github.com/TanStack/table/issues/5147)/[#5832](https://github.com/TanStack/table/issues/5832) auto-dir sampling).
7. Remaining semantics decisions: [#5909](https://github.com/TanStack/table/issues/5909) (undefined = uncontrolled), [#5778](https://github.com/TanStack/table/issues/5778), [#6101](https://github.com/TanStack/table/issues/6101), [#6081](https://github.com/TanStack/table/issues/6081). (Cluster 3 — [#6115](https://github.com/TanStack/table/issues/6115), [#5833](https://github.com/TanStack/table/issues/5833), [#4939](https://github.com/TanStack/table/issues/4939), [#6136](https://github.com/TanStack/table/issues/6136) — implemented 2026-08-02, pending PR.)
7. Remaining semantics decisions: [#5909](https://github.com/TanStack/table/issues/5909) (undefined = uncontrolled), [#5778](https://github.com/TanStack/table/issues/5778). (Cluster 3 — [#6115](https://github.com/TanStack/table/issues/6115), [#5833](https://github.com/TanStack/table/issues/5833), [#4939](https://github.com/TanStack/table/issues/4939), [#6136](https://github.com/TanStack/table/issues/6136) — implemented 2026-08-02, pending PR.)
Comment on lines +114 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file info =="
wc -l beta-window-triage.md
sed -n '1,140p' beta-window-triage.md | nl -ba

echo
echo "== occurrences of `#6443`, `#5801`, Cluster 5, step 4 =="
rg -n "(`#6443`|`#5801`|Cluster 5|step 4|filtering|defaultColumn|_valuesCache|sorting|Cluster 3)" beta-window-triage.md

Repository: TanStack/table

Length of output: 234


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file info =="
wc -l beta-window-triage.md

echo
echo "== file excerpt =="
python3 - <<'PY'
from pathlib import Path
p = Path("beta-window-triage.md")
for i, line in enumerate(p.read_text(encoding="utf-8").splitlines(), 1):
    if 1 <= i <= 140:
        print(f"{i:4d}: {line}")
PY

echo
echo "== occurrences of key references =="
rg -n "(`#6443`|`#5801`|Cluster 5|step 4|filtering|defaultColumn|_valuesCache|sorting|Cluster 3)" beta-window-triage.md || true

echo
echo "== parse references by line =="
python3 - <<'PY'
from pathlib import Path
import re
text = Path("beta-window-triage.md").read_text(encoding="utf-8")
for match in re.finditer(r"#(\d{3,4})", text):
    line = text.count("\n", 0, match.start()) + 1
    line_text = text.splitlines()[line-1]
    print(f"line {line}: #{match.group(1)}: {line_text[:120]}")
PY

Repository: TanStack/table

Length of output: 50370


Split or relabel the step-4 line.

Step 4 currently merges PR #6443 (#5801 expansion reset) and also describes Cluster 5 filtering fixes (#6007, #5987, #6101, #6081). The filtering fixes do not have a consolidated PR listed, so maintainers may treat the wrong PR as the vehicle for Cluster 5. Split this into separate steps, or replace the parenthetical with the actual Cluster 5 PR when available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@beta-window-triage.md` around lines 114 - 117, Split the step-4 entry so PR
`#6443` remains associated only with the `#5801` expansion-reset work, and move the
Cluster 5 filtering issues (`#6007`, `#5987`, `#6101`, and `#6081`) into a separate
step. If a consolidated Cluster 5 PR is known, reference it there; otherwise
state that its vehicle is pending.

8. Type changes batch: [#5908](https://github.com/TanStack/table/issues/5908), [#5971](https://github.com/TanStack/table/issues/5971), [#6302](https://github.com/TanStack/table/issues/6302) (TS-perf gate).
9. [#6078](https://github.com/TanStack/table/issues/6078) via corrected [#6445](https://github.com/TanStack/table/pull/6445) (urgent but non-breaking — do not let it slip just because it is "anytime").
24 changes: 16 additions & 8 deletions docs/framework/alpine/guide/column-faceting.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,25 +283,33 @@ When filtering is performed on the server, the rows loaded into the browser may

Each factory receives the table and a column ID, then returns a function that resolves the faceted result. The regular column APIs will return the server-provided values.

Factories are resolved once per table and column, but the function each factory returns runs on every read; the table does not cache its result. Read live values inside that returned function (from a signal, store, or `table.options.meta`) so updated server facets show up immediately, and memoize inside the factory if the calculation is expensive.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
```ts
const serverFacets = await fetch('/api/faceting').then((res) => res.json())
// `local` is your Alpine.reactive state, refreshed when facets arrive
async function loadFacets() {
local.serverFacets = await fetch('/api/faceting').then((res) => res.json())
}

const features = tableFeatures({
columnFacetingFeature,
facetedUniqueValues: (_table, columnId) => () => {
const uniqueValueMap = new Map<string, number>()
// Populate the map from serverFacets data for columnId.
return uniqueValueMap
// The returned functions run on every read and table.options stays in
// sync with the latest render, so read live data through options.meta
facetedUniqueValues: (table, columnId) => () => {
const serverFacets = table.options.meta?.serverFacets
return new Map<string, number>(serverFacets?.uniqueValues[columnId] ?? [])
},
facetedMinMaxValues: (_table, columnId) => () => {
// Read the range from serverFacets data for columnId.
return [min, max]
facetedMinMaxValues: (table, columnId) => () => {
return table.options.meta?.serverFacets?.minMaxValues[columnId]
},
})

const table = createTable({
features,
columns,
get meta() {
return { serverFacets: local.serverFacets }
},
get data() {
return local.data
},
Expand Down
2 changes: 1 addition & 1 deletion docs/framework/alpine/guide/column-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,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`.
- `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.

The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers:

Expand Down
2 changes: 2 additions & 0 deletions docs/framework/angular/guide/column-faceting.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ When filtering is performed on the server, the rows loaded into the browser may

Each factory receives the table and a column ID, then returns a function that resolves the faceted result. The regular column APIs will return the server-provided values.

Factories are resolved once per table and column, but the function each factory returns runs on every read; the table does not cache its result. Read live values inside that returned function (from a signal, store, or `table.options.meta`) so updated server facets show up immediately, and memoize inside the factory if the calculation is expensive.

```ts
const serverFacets = signal(initialServerFacets)

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/angular/guide/column-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,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`.
- `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.

The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers:

Expand Down
21 changes: 12 additions & 9 deletions docs/framework/ember/guide/column-faceting.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,26 +304,29 @@ When filtering is performed on the server, the rows loaded into the browser may

Each factory receives the table and a column ID, then returns a function that resolves the faceted result. The regular column APIs will return the server-provided values.

```ts
const serverFacets = await fetch('/api/faceting').then((res) => res.json())
Factories are resolved once per table and column, but the function each factory returns runs on every read; the table does not cache its result. Read live values inside that returned function (from a signal, store, or `table.options.meta`) so updated server facets show up immediately, and memoize inside the factory if the calculation is expensive.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
```ts
// `this.serverFacets` is a @tracked field on your component, set when
// the facet request resolves
const features = tableFeatures({
columnFacetingFeature,
facetedUniqueValues: (_table, columnId) => () => {
const uniqueValueMap = new Map<string, number>()
// Populate the map from serverFacets data for columnId.
return uniqueValueMap
// The returned functions run on every read and table.options stays in
// sync with the latest render, so read live data through options.meta
facetedUniqueValues: (table, columnId) => () => {
const serverFacets = table.options.meta?.serverFacets
return new Map<string, number>(serverFacets?.uniqueValues[columnId] ?? [])
},
facetedMinMaxValues: (_table, columnId) => () => {
// Read the range from serverFacets data for columnId.
return [min, max]
facetedMinMaxValues: (table, columnId) => () => {
return table.options.meta?.serverFacets?.minMaxValues[columnId]
},
})

// Inside your Glimmer component:
table = useTable(() => ({
features,
columns,
meta: { serverFacets: this.serverFacets },
data: this.data,
}))
```
Expand Down
2 changes: 1 addition & 1 deletion docs/framework/ember/guide/column-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,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`.
- `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.

The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers:

Expand Down
21 changes: 12 additions & 9 deletions docs/framework/lit/guide/column-faceting.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,26 +287,29 @@ When filtering is performed on the server, the rows loaded into the browser may

Each factory receives the table and a column ID, then returns a function that resolves the faceted result. The regular column APIs will return the server-provided values.

```ts
const serverFacets = await fetch('/api/faceting').then((res) => res.json())
Factories are resolved once per table and column, but the function each factory returns runs on every read; the table does not cache its result. Read live values inside that returned function (from a signal, store, or `table.options.meta`) so updated server facets show up immediately, and memoize inside the factory if the calculation is expensive.

```ts
// `this.serverFacets` is a reactive property on your Lit element, set
// when the facet request resolves
const features = tableFeatures({
columnFacetingFeature,
facetedUniqueValues: (_table, columnId) => () => {
const uniqueValueMap = new Map<string, number>()
// Populate the map from serverFacets data for columnId.
return uniqueValueMap
// The returned functions run on every read and table.options stays in
// sync with the latest render, so read live data through options.meta
facetedUniqueValues: (table, columnId) => () => {
const serverFacets = table.options.meta?.serverFacets
return new Map<string, number>(serverFacets?.uniqueValues[columnId] ?? [])
},
facetedMinMaxValues: (_table, columnId) => () => {
// Read the range from serverFacets data for columnId.
return [min, max]
facetedMinMaxValues: (table, columnId) => () => {
return table.options.meta?.serverFacets?.minMaxValues[columnId]
},
})

// Inside your Lit component's render method:
const table = this.tableController.table({
features,
columns,
meta: { serverFacets: this.serverFacets },
data: this.data,
})
```
Expand Down
2 changes: 1 addition & 1 deletion docs/framework/lit/guide/column-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,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`.
- `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.

The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers:

Expand Down
Loading
Loading