The release note says the table got smarter about global filters.
TanStack Table v9.0.0-beta.64, published 2026-07-31, includes one core fix that sounds small enough to merge while someone else is talking: table-core honors an explicit global-filter opt-in (#6439). Packages on that tag include @tanstack/[email protected] and @tanstack/[email protected]. [Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.64]
That line is useful. It is not a production decision.
On the same calendar day, npm dist-tags for both @tanstack/table-core and @tanstack/vue-table still showed:
| dist-tag | Version (verified 2026-08-01) |
|---|---|
latest | 8.21.3 |
beta | 9.0.0-beta.65 (already past .64) |
[Source: https://registry.npmjs.org/@tanstack/table-core] [Source: https://registry.npmjs.org/@tanstack/vue-table]
Read that table twice. Stable is still 8.21.3. The beta channel did not freeze on the opt-in fix you just bookmarked. beta.65 landed minutes later with another global-filter fix (undefined first values). If your spike branch says “install beta” without an exact pin, you are not testing the release note you read. You are testing whichever beta the registry served that hour.
The question is not whether beta.64 demos cleanly in a fresh sandbox. The question is whether a multi-surface admin table — filters, sort, pagination, selection, SSR hydrate, worker parity — still has a named owner, a fixture that would fail if opt-in is ignored, and a one-command rollback when the pilot goes sideways.

What beta.64 actually changed (primary sources only)
Do not upgrade table infrastructure from a social recap. Read the tag you will install, then confirm the npm tag you will resolve.
Release body — v9.0.0-beta.64
Published 2026-07-31T00:07:47Z, prerelease true.
Fix
table-core: honor explicit global filter opt-in (#6439)
Packages on the tag (partial list relevant to Vue admin stacks)
@tanstack/[email protected]@tanstack/[email protected]- (plus react/solid/svelte/angular/… adapters on the same beta line)
[Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.64]
Pull request rationale — #6439
PR #6439 (fix(table-core): honor explicit global filter opt-in) merged 2026-07-30T23:57:23Z. The summary is concrete:
- Let an explicit per-column global-filter opt-in bypass the default primitive-value heuristic
- Preserve table-wide filtering controls and custom column eligibility predicates
- Cover object-valued custom global filtering at capability and row-model levels
Rationale in the PR: by default, global filtering still only opts primitive string and number accessor values in automatically. When a column sets enableGlobalFilter: true, rejecting that column with the same primitive heuristic is surprising and blocks a custom globalFilterFn from handling object or array values.
[Source: https://github.com/TanStack/table/pull/6439]
Nearby betas you will confuse with “the” table beta
| Tag | Published (UTC) | What the body claims | Ops takeaway |
|---|---|---|---|
v9.0.0-beta.59 | 2026-07-29 | More adapter reactivity tests; react adapter sync state | Pilot hygiene / test depth — still not “unpin stable” |
v9.0.0-beta.63 | 2026-07-30 | More readonly columns internally | Internal core churn — pin exact |
v9.0.0-beta.64 | 2026-07-31 | Honor explicit global filter opt-in (#6439) | Filter fixture belongs on the pilot checklist |
v9.0.0-beta.65 | 2026-07-31 | Global filtering with undefined first values (#6438) | npm beta tag moved here after .64 |
v9.0.0-beta.67 | 2026-07-31 | Ship Octane Table source (#6480) | Package-scope change on octane line — do not generalize to vue-table |
[Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.59] [Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.63] [Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.65] [Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.67]
If your team says “we are on table v9 beta,” ask: which tag, which lockfile hash, which fixture file? Anything less is folklore.
Source TanStack Table v9.0.0-beta.64 GitHub release
Source PR #6439 — honor explicit global filter opt-in
Source npm @tanstack/vue-table
Stable is still the production default
This section exists so nobody “helpfully” edits the package.json to ^9.0.0-beta.0 after reading the opt-in fix.
Verified against the npm registry JSON on 2026-08-01:
@tanstack/table-core→dist-tags.latest = 8.21.3,dist-tags.beta = 9.0.0-beta.65@tanstack/vue-table→dist-tags.latest = 8.21.3,dist-tags.beta = 9.0.0-beta.65- Both
9.0.0-beta.64and9.0.0-beta.65exist as published versions; latest does not point at either
[Source: https://registry.npmjs.org/@tanstack/table-core] [Source: https://registry.npmjs.org/@tanstack/vue-table]
Production posture for a normal Laravel + Vue admin app:
| Lane | Package resolution | Allowed surfaces |
|---|---|---|
| Production / main | @tanstack/[email protected] (exact or locked latest) | Customer admin tables, SSR routes, billing grids |
| Pilot / spike branch | Exact beta pin, e.g. 9.0.0-beta.64 or the newer beta you deliberately chose | Feature-flagged route, internal tools, dogfood tenant |
| Broken middle | "@tanstack/vue-table": "beta" or floating range | Nowhere — this is how lockfiles lie on Monday |
I already wrote the general version of this argument in Before You Adopt a Beta Library, Prove the Exit Path. This post is the Saturday build-in-public instance: one fresh artifact (global-filter opt-in), one stable pin that still holds, one multi-surface gate list you can run before anyone calls the bump “ready to try.”

Why “more filter tests” is not a green light
beta.59-era notes leaned on adapter reactivity tests. beta.64 adds a behavioral filter fix. Both are healthy upstream signals. Neither answers:
- Who owns the pilot after merge?
- Which fixture fails if
enableGlobalFilter: trueis ignored again? - What is the named exit — pin stable forever, or graduate under a flag with a rollback command?
- Do sort, filter, pagination, and selection still agree across the main thread and any worker/row-model boundary you already built?
If you already invested in workerized row models, re-read TanStack Table v9 row-model workers. Parity before performance still applies. A global-filter opt-in fix that only passes on the main-thread happy path is not done.
The pilot gate list (copy into the PR)
Treat this as a release checklist, not inspiration.
Gate 0 — Classify the signal
1Signal: TanStack Table v9.0.0-beta.64 (#6439 global filter opt-in)
2Class: beta application library (spike / pilot only)
3Not: production default, security hotfix, or "just types"
4Stable: @tanstack/[email protected] remains production pin
If someone argues for production from this tag alone, the classification failed.
Gate 1 — Exact pins + lockfile
1{
2 "private": true,
3 "dependencies": {
4 "@tanstack/table-core": "9.0.0-beta.64",
5 "@tanstack/vue-table": "9.0.0-beta.64"
6 },
7 "scripts": {
8 "check:table-pins": "node scripts/check-table-pins.mjs",
9 "test:table-pilot": "vitest run tests/table-global-filter-pilot.test.ts"
10 }
11}
1// scripts/check-table-pins.mjs
2import fs from "node:fs";
3
4const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
5const lockOk = fs.existsSync("pnpm-lock.yaml") || fs.existsSync("package-lock.json");
6
7const required = {
8 "@tanstack/table-core": "9.0.0-beta.64",
9 "@tanstack/vue-table": "9.0.0-beta.64",
10};
11
12const deps = { ...pkg.dependencies, ...pkg.devDependencies };
13let failed = false;
14
15for (const [name, version] of Object.entries(required)) {
16 const actual = deps[name];
17 if (actual !== version) {
18 console.error(`pin mismatch: ${name} want ${version} got ${actual}`);
19 failed = true;
20 }
21 if (typeof actual === "string" && /^(beta|latest|\^|~|>=)/.test(actual)) {
22 console.error(`floating range forbidden: ${name}=${actual}`);
23 failed = true;
24 }
25}
26
27if (!lockOk) {
28 console.error("lockfile missing — pilot is not reproducible");
29 failed = true;
30}
31
32if (failed) process.exit(1);
33console.log("table pilot pins ok");
Swap 9.0.0-beta.64 for 9.0.0-beta.65 only if the PR description says you intentionally absorbed #6438 as well. Silent drift from .64 → .65 because someone ran bare npm install is a failed gate, not a free upgrade.
Gate 2 — Global-filter fixture that would have caught the bug class
You need at least one column that is not a primitive string/number accessor, with explicit global-filter opt-in, plus a custom filter function. If the suite only searches string names, you never exercised #6439.
V9 table construction is feature-registered (global filtering is not “all columns forever by default” the way many V8 apps behaved). Copy the invariant, then wire it with the exact helper imports from the beta tag you pinned — do not paste an old V8 createTable snippet into a V9 spike and call it green.
1// tests/table-global-filter-pilot.test.ts
2// Behavioral contract for PR #6439. Wire createTable/features from the
3// exact @tanstack/table-core version in package.json (beta pin).
4import { describe, expect, it } from "vitest";
5import { readFileSync } from "node:fs";
6
7type OrderRow = {
8 id: string;
9 customer: { name: string; code: string };
10 total: number;
11};
12
13const data: OrderRow[] = [
14 { id: "o1", customer: { name: "Ada", code: "A-100" }, total: 40 },
15 { id: "o2", customer: { name: "Ben", code: "B-200" }, total: 12 },
16];
17
18function customerMatches(row: OrderRow, filterValue: unknown): boolean {
19 const q = String(filterValue ?? "").toLowerCase();
20 if (!q) return true;
21 return (
22 row.customer.name.toLowerCase().includes(q) ||
23 row.customer.code.toLowerCase().includes(q)
24 );
25}
26
27describe("global filter opt-in pilot", () => {
28 it("pins the table packages named in the PR", () => {
29 const pkg = JSON.parse(readFileSync("package.json", "utf8"));
30 expect(pkg.dependencies["@tanstack/table-core"]).toBe("9.0.0-beta.64");
31 expect(pkg.dependencies["@tanstack/vue-table"]).toBe("9.0.0-beta.64");
32 });
33
34 it("documents the #6439 fixture rows the product table must satisfy", () => {
35 // Replace filterRows() with your real table.getFilteredRowModel() after
36 // registering global filtering + an object column with enableGlobalFilter.
37 const filterRows = (q: string) =>
38 data.filter((row) => customerMatches(row, q)).map((r) => r.id);
39
40 expect(filterRows("b-200")).toEqual(["o2"]);
41 expect(filterRows("Ada")).toEqual(["o1"]);
42 });
43});
When you wire the real table instance, the acceptance criteria stay fixed:
- Column accessor yields an object (not only
string/number). - That column explicitly opts into global filtering.
- A custom global filter function can read object fields.
- Assertions use the filtered row model, not a hand-rolled array filter left in the test forever.
Until step 4 uses the library row model, mark the test it.todo or fail CI — a pure array.filter only proves you understand the bug class, not that the pinned beta honors opt-in.
Gate 3 — Sort / filter / pagination parity matrix
| Surface | Fixture | Pass signal |
|---|---|---|
| Global filter | Object opt-in column (Gate 2) | Expected row ids only |
| Column filter | One discrete status filter combined with global filter | Intersection correct |
| Sort | Sort by total while filter active | Order stable and deterministic |
| Pagination | Page size 1 with two matches | Page count and page items agree with filter |
| Selection | Select filtered row, clear filter | Selection policy documented (keep vs clear) |
| SSR / hydrate | Dehydrate filtered state, hydrate client | No flicker to unfiltered full set |
| Worker path (if any) | Same fixture off main thread | Same row ids as main thread |
A green unit test on Gate 2 with a broken pagination footer is still a failed pilot.

Gate 4 — Feature flag and surface allow-list
1// config/tablePilot.ts
2export const TABLE_PILOT = {
3 enabled: process.env.TABLE_V9_PILOT === "1",
4 // Exact strings only — never "beta"
5 tableCore: "9.0.0-beta.64",
6 vueTable: "9.0.0-beta.64",
7 routes: ["/internal/ops/orders-pilot"],
8 exitDate: "2026-08-15",
9 owner: "orders-admin",
10} as const;
Production routes keep importing the stable-locked module path. Pilot routes import through a single factory that throws if the env flag is off. No shared “smart” resolver that picks beta because NODE_ENV !== "production" — staging surprises are still surprises.
Gate 5 — Named owner and exit
Write these four lines in the PR body before anyone merges:
1## Table pilot exit
2- Owner: @handle (orders-admin)
3- Success: Gate 2–3 green on CI + dogfood tenant for 5 business days
4- Graduate: only with stable pin plan or continued exact beta pin + flag
5- Rollback: `git revert <sha>` + `pnpm install` restores 8.21.3 lockfile entries
6- Kill date: 2026-08-15 (remove flag and spike deps if not graduated)
No owner means the beta becomes permanent platform drift with a cheerful name.
Gate 6 — Rollback command you have actually run
1#!/usr/bin/env bash
2# scripts/rollback-table-pilot.sh
3set -euo pipefail
4
5git checkout main -- package.json pnpm-lock.yaml
6pnpm install --frozen-lockfile
7pnpm run test:tables
8echo "table pilot rolled back to locked stable"
If rollback needs a hero and a spreadsheet, you do not have rollback. You have hope.
Green signals that still lie
| Green signal | What it usually proves | What beta.64 still needs |
|---|---|---|
pnpm install exit 0 | Something resolved | Exact 9.0.0-beta.64 (or chosen tag) on every install path |
| Storybook table renders | Chrome mounts | Object-column global search on a product route |
vite build exit 0 | Bundle compiles | Filter + sort + page footer agree after search |
| “Reactivity tests upstream” | Upstream cares | Your adapter version and your state wiring |
| Demo tenant “looks fine” | One happy path | Undefined values, empty filter, selection after clear |
| CI unit tests on strings only | Strings work | #6439 class never executed |
This is the same honesty pattern as UI kit lockfile bumps and runtime patch inventories: green is not done until the artifact that would catch this failure mode exists.
Build-in-public: how I would run the spike this weekend
Saturday rotation is build-in-public. Here is the boring sequence I want on a real orders grid, not a toy:
- Branch from main with production still on 8.21.3.
- Add exact beta.64 pins (or explicitly beta.65 if you want #6438 in the same spike — write it down).
- Land Gate 2 fixture first, before any visual polish.
- Wire one internal route behind
TABLE_V9_PILOT=1. - Run the parity matrix once on main thread; if you have a worker row-model path, run it twice.
- Paste Gate 5 exit block into the PR; set the kill date on a calendar, not in chat.
- Dogfood Monday with support/ops, not only engineering.
If step 3 is skipped because “we will add tests after the UI feels right,” stop. That ordering is how betas become load-bearing without evidence.
Optional: package identity assert at boot (pilot route only)
1// pilot/assertTablePilotVersion.ts
2import { TABLE_PILOT } from "../config/tablePilot";
3
4export function assertTablePilotVersion(actualCore: string, actualVue: string) {
5 if (actualCore !== TABLE_PILOT.tableCore || actualVue !== TABLE_PILOT.vueTable) {
6 throw new Error(
7 `table pilot version drift: core ${actualCore} vue ${actualVue} ` +
8 `expected ${TABLE_PILOT.tableCore} / ${TABLE_PILOT.vueTable}`,
9 );
10 }
11}
Resolve actual* from your lockfile audit at build time or from a tiny generated module — not from a hand-edited constant that can desync.
What this post is not claiming
- That npm
latesthas left 8.21.3 for a stable v9 line (it has not, as of this writing). - That beta.64 is safe for all Vue SSR apps.
- That you should jump from 8.21.3 on the strength of one filter fix.
- That upstream reactivity tests replace your product fixtures.
- Personal outage numbers, invented benchmarks, or unofficial performance wins.
If official docs later publish a migration guide section for global-filter opt-in defaults, update the fixture — do not replace the gate list with vibes.
What you should do Monday morning
- Open the npm package pages or registry JSON for
@tanstack/vue-tableand@tanstack/table-core. Write downlatestandbetadist-tags in the ticket. If they differ from this post, trust the registry, not the article date. - Confirm production lockfiles still resolve 8.21.3 (or your chosen stable pin). If anything floats on
beta, fix that before any feature work. - If you are not piloting v9, stop here. Add a calendar note to re-check monthly. Do not open a “quick bump” PR from a social link.
- If you are piloting, create a branch that pins one exact beta (document
.64vs.65) and commits the lockfile. - Add the object-column global-filter fixture (Gate 2). Make CI fail when it fails.
- Fill the parity matrix (Gate 3) for every table surface you already ship: filter, sort, page, selection, hydrate, worker.
- Put owner, kill date, and rollback command in the PR body (Gate 5–6). Merge only behind a flag.
- Link the PR to your internal developer tools change-control doc or the public hub so the next person does not rediscover the same gates from scratch.
Further reading
- — primary release body for the global-filter opt-in fix.
Source GitHub PR #6439
— rationale for explicit opt-in vs primitive heuristic.Source TanStack Table docs
— current documentation entry; verify beta vs latest paths before copying snippets.- Related on this site: Before You Adopt a Beta Library, Prove the Exit Path, TanStack Table v9 row-model workers, Vue Query cache invalidation contract, hubs: /developer-tools/, /laravel-vue-saas/, /start-here/.
A beta bump can be the right engineering move. Calling it done because upstream added filter tests is how quiet registry tags become loud Monday incidents. Pin the version, own the fixture, name the exit.
