diff --git a/beta-window-triage.md b/beta-window-triage.md index 1b4eecfeaf..c988cc675a 100644 --- a/beta-window-triage.md +++ b/beta-window-triage.md @@ -1,119 +1,132 @@ -# v9 beta-window triage — what must land before stable +# v9 beta-window triage — open items only -Generated 2026-08-02 from the 42 keep-open issues in `remaining-issues.md` and the 6 open beta PRs in `prs-gpt-.md`. Seven investigation agents verified every item against current `beta` source (`packages/table-core/src`, post [#6491](https://github.com/TanStack/table/pull/6491)/[#6493](https://github.com/TanStack/table/pull/6493)). +Fresh scan completed 2026-08-02 against local `beta` at `164ef450a` and the live GitHub open-item lists. -**Sorting rule:** an item is **BETA-NOW** if the natural fix changes observable default behavior, public API semantics, or TypeScript types — i.e. shipping it in a stable 9.x release would be semver-breaking or would confuse users who adapted to current behavior. **ANYTIME** items are purely additive, crash fixes, or unambiguous bugfixes that can ship in any 9.x release. +This file intentionally lists only issues and PRs that were still open at the snapshot. Merged replacement work is identified by beta commit hash rather than kept as a closed triage entry. -## Headline counts +**Scope:** all 60 open issues and all 64 open PRs were screened. The detailed sections retain only beta API/default decisions, current implementation vehicles, urgent valid bugs, and open items that can already be closed as duplicates or superseded work. -- **3 issues can close today** (already fixed on beta, still open on GitHub): [#4919](https://github.com/TanStack/table/issues/4919), [#5138](https://github.com/TanStack/table/issues/5138) (both via merged [#6438](https://github.com/TanStack/table/pull/6438)/[#6439](https://github.com/TanStack/table/pull/6439)), [#5202](https://github.com/TanStack/table/issues/5202) (via [#6485](https://github.com/TanStack/table/pull/6485)/[#6487](https://github.com/TanStack/table/pull/6487) header rowSpan + cell spanning). -- **26 issues are BETA-NOW** (several are pairs/duplicates; roughly 18 distinct fixes). -- **11 issues are ANYTIME** (including top-urgent [#6078](https://github.com/TanStack/table/issues/6078), which is a crash fix, not a breaking change). -- **All 6 beta PRs assessed**; [#6442](https://github.com/TanStack/table/pull/6442) is mergeable today, [#6445](https://github.com/TanStack/table/pull/6445) has a defect (does not fix [#6078](https://github.com/TanStack/table/issues/6078) as written). +**BETA-NOW rule:** resolve before stable when the likely fix changes default behavior, state meaning, callback precedence, cache invalidation semantics, or a public type/API shape. Additive helpers, crash fixes, framework fixes, examples, and performance work may ship later, though urgent items should not be allowed to drift. -## Close now (fixed on beta, still open on GitHub) +## Headline -| Issue | Fixed by | Note | -| --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| [#4919](https://github.com/TanStack/table/issues/4919) — global filter fails on null accessor values | [#6438](https://github.com/TanStack/table/pull/6438) + [#6439](https://github.com/TanStack/table/pull/6439) (merged) | Both halves fixed: `column_getAutoFilterFn` scans past nullish; default `getColumnCanGlobalFilter` finds first non-null row | -| [#5138](https://github.com/TanStack/table/issues/5138) — `enableGlobalFilter` ignored by default `getColumnCanGlobalFilter` | [#6439](https://github.com/TanStack/table/pull/6439) (merged) | Explicit `true` now early-returns; explicit `false` still wins upstream | -| [#5202](https://github.com/TanStack/table/issues/5202) — `Header.rowSpan` always 0 | [#6485](https://github.com/TanStack/table/pull/6485) + [#6487](https://github.com/TanStack/table/pull/6487) (merged) | Real rowSpans computed in `buildHeaderGroups.ts:201-225`; covered placeholders deliberately report 0 | +- **Five core beta decisions remain:** column/default-column cache coherence, persisted group-column visibility, aggregated-cell fallback precedence, custom sorting of `undefined`, and the options-store architecture. +- **One additional API-shape decision is open:** whether manual pagination capability flags belong in pagination state. +- **Seven PRs target `beta`:** two are already superseded and can close; one is clean and fully green; four need a decision, rebase, fix, or full CI. +- **Eight open issues can close now** because current beta already resolves them or because a canonical duplicate remains open. +- **Eight open PRs can close now** because equivalent fixes are already merged into beta. -Also superseded PRs to close: [#5823](https://github.com/TanStack/table/pull/5823) (issue fixed by [#6493](https://github.com/TanStack/table/pull/6493)), [#6177](https://github.com/TanStack/table/pull/6177) (issue fixed by [#6491](https://github.com/TanStack/table/pull/6491)) — already recategorized in `prs-gpt-.md`. +## BETA-NOW — resolve before stable -## BETA-NOW — address during beta +### 1. Column construction and value-cache coherence -### Cluster 1: Row selection reconciliation — FIX IMPLEMENTED 2026-08-02 (pending PR) +- **Canonical: [#5363](https://github.com/TanStack/table/issues/5363)** — rows retain `_valuesCache` when `columns` or an `accessorFn` changes, so rendering, sorting, filtering, and grouping can read stale values. +- **Duplicate: [#4485](https://github.com/TanStack/table/issues/4485)** — the same defect observed through a replaced columns array. Close this issue into #5363 rather than track a second fix. +- **Coupled default behavior: [#5275](https://github.com/TanStack/table/issues/5275)** — `defaultColumn` is baked into constructed columns while `getAllColumns` only depends on the columns array. +- **Open prior-art PR: [#5582](https://github.com/TanStack/table/pull/5582)** — targets `main`, conflicts with beta, and should not be merged as-is. Preserve its regression idea in a fresh beta implementation. -Implemented on beta per maintainer decisions (opt-in flag, prune-only, select-all-paths-only guard), with 19 new tests plus guide/SKILL updates: +Resolve these together. Prefer invalidating/sweeping affected caches and reconstructed column definitions without needlessly changing row identity. The stable contract must be explicit about which option-reference changes recompute columns and accessor values. -- **[#6049](https://github.com/TanStack/table/issues/6049)** — resolved as opt-in, NOT a default change: new `deselectParents` flag on `ToggleSelectedOptions` prunes ancestor ids on deselect (`row_toggleSelected` + Shift-range paths). Scenario 1 (bottom-up select) was already covered by the recommended checked expression, which the guide's canonical snippet now teaches. Close #4878/#5416 as duplicates when this lands. -- **[#5116](https://github.com/TanStack/table/issues/5116)** — new `isRowSelectableInSelectAll` helper (selectable AND no ancestor blocks `enableSubRowSelection`) applied to `table_toggleAllRowsSelected` and symmetrically to `getIsAllRowsSelected`/`getIsAllPageRowsSelected` (+ memoDeps). All four select-all paths now agree with the page-toggle cascade. -- **[#5398](https://github.com/TanStack/table/issues/5398)** — deselect branches of the two table-level select-all paths now guard on `row_getCanSelect`; disabled-but-selected rows survive deselect-all, cleared only via `{ deselectAll: true }`/`resetRowSelection(true)`. Individual `row.toggleSelected(false)` and cascades still clear any id. Note: the thread's regression claim was misattributed; the lost #5061 guard was on `getIsSomeRowsSelected` (moot in v9). +### 2. Persisted group-column visibility semantics -### Cluster 2: Auto-reset first-run guard (strict landing order) +- **[#5770](https://github.com/TanStack/table/issues/5770)** remains open and remains valid. Current beta still ignores `columnVisibility: { groupId: false }` because group visibility is derived from descendant leaves. -1. **[#5968](https://github.com/TanStack/table/issues/5968) (high) — land FIRST.** `memo` (`utils.ts:176-201`) has no first-run suppression, so the first `getRowModel()` schedules `table_autoResetExpanded`/`table_autoResetPageIndex` on mount, wiping controlled `expanded` and initial `pageIndex`. Also `table_autoResetPageIndex` calls `table_resetPageIndex(table, true)` (`rowPaginationFeature.utils.ts:49`), hard-coding 0 and ignoring `initialState`. Fix: restore first-run guard + drop the `true`. -2. **[#5801](https://github.com/TanStack/table/issues/5801) (med-high) — land SECOND, via PR [#6443](https://github.com/TanStack/table/pull/6443).** `table_autoResetExpanded` is only wired from the grouped row model; wire into `createCoreRowModel`. Landing this before the [#5968](https://github.com/TanStack/table/issues/5968) guard would extend the mount-wipe bug to all non-grouped tables. +Important correction from the previous audit: beta commit `164ef450a` fixed `groupColumn.toggleVisibility()` by fanning out to leaf IDs, but it did **not** make a persisted group-ID key meaningful. Before stable, choose one contract: -### Cluster 3: Expanded/pagination state contents — FIX IMPLEMENTED 2026-08-02 (pending PR) +1. Accept group IDs in `ColumnVisibilityState` and define precedence against descendant keys; or +2. Declare visibility state leaf-ID-only, document it, and close #5770 as unsupported state shape. -All four implemented on beta in one coordinated change (`rowExpandingFeature.utils.ts` + `createPaginatedRowModel.ts`), with new unit/implementation tests, all-frameworks expanding-guide note, and changeset `expanding-pagination-state-contents`: +Leaving the setter accepting group columns while persisted state silently ignores group IDs is the ambiguous outcome to avoid. -- **[#6115](https://github.com/TanStack/table/issues/6115)** — expand-all materialization (`row_toggleExpanded` old===true branch) now only writes ids where `row_getCanExpand` is true (chosen over `subRows.length` so `getRowCanExpand` lazy-load overrides stay expandable). `table_getExpandedDepth` filters the same way for expanded-all. Companion: `table_getIsAllRowsExpanded` now only considers expandable rows so the materialized map still round-trips as "all expanded" (stale-ids-with-no-expandable-rows returns false). -- **[#5833](https://github.com/TanStack/table/issues/5833)** — paginated `flatRows` rebuild dedupes via seen-id set. Semantic decision: collapsed descendants of page rows stay included (consistent with every other row model where `flatRows` ignores expansion state); only the duplication is removed. -- **[#6136](https://github.com/TanStack/table/issues/6136)** — `row_toggleExpanded` early-returns (no `onExpandedChange`) when the target state matches current atom state; `table_toggleAllRowsExpanded` gets symmetric no-op guards (already `true`, or collapse with nothing expanded). -- **[#4939](https://github.com/TanStack/table/issues/4939)** — expand direction of `row_toggleExpanded` now guarded on `row_getCanExpand`; `table_toggleAllRowsExpanded` expand branch guarded on `table_getCanSomeRowsExpand`. Collapse always allowed (stale-id cleanup). Angular/ember flex-render tests that drove expansion on flat rows updated with `getRowCanExpand: () => true`. +### 3. Aggregated-cell renderer precedence -### Cluster 4: Sorting defaults +- **[#5778](https://github.com/TanStack/table/issues/5778)** — the aggregation feature injects a default `aggregatedCell`, so a consumer's `cell` formatter never wins for aggregated cells unless they explicitly set `aggregatedCell: null`. -- **[#4946](https://github.com/TanStack/table/issues/4946) (high, one-liner)** — `column_toggleSorting` calls `column_getNextSortingOrder(column)` with no `multi` arg (`rowSortingFeature.utils.ts:255`), so `enableMultiRemove` is dead on every path. Also widen the public `getNextSortingOrder` type to accept `multi`. -- **[#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. +Changing the fallback order later would alter rendering for existing consumers. Decide during beta whether aggregation supplies a default renderer or whether `aggregatedCell` should lazily fall back to the consumer's `cell` renderer. -### Cluster 5: Filtering semantics — FIX IMPLEMENTED 2026-08-02 (pending PR) +### 4. Custom sorting and `undefined` -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`: +- **[#5653](https://github.com/TanStack/table/issues/5653)** — the remaining semantic question is whether default `sortUndefined: 1` may bypass a custom `sortingFn`. -- **[#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). +The documentation defect is already fixed in current beta. Either preserve the current short-circuit and close the issue with `sortUndefined: false` as the opt-in pass-through, or change the default/precedence now. Do not change this sorting default after stable. -### Cluster 6: Group-column second-class citizenship +### 5. Options-store architecture -- **[#5770](https://github.com/TanStack/table/issues/5770) + [#5497](https://github.com/TanStack/table/issues/5497) (high)** — explicit `columnVisibility: { groupId: false }` ignored (`columnVisibilityFeature.utils.ts:78-83` short-circuits to `childColumns.some`); `column_toggleVisibility` writes dead group-id state keys; `getCanHide` advertises hideability it can't deliver. Kevin already approved the beta-only fix in-thread. **Vehicle: PR [#6442](https://github.com/TanStack/table/pull/6442) (clean, CI fully green, just needs undrafting + sign-off on the leaf-ID fan-out convention). Closes both.** -- **[#5397](https://github.com/TanStack/table/issues/5397) (med-high)** — `table_getColumnOffsets` built from leaf columns only (`columnSizingFeature.utils.ts:136-155`); group-column `getStart` falls to `?? 0`, so sticky pinned group headers stack at offset 0. Fix: delegate group start/after to first/last visible leaf descendant. -- **[#5778](https://github.com/TanStack/table/issues/5778) (med)** — aggregation feature unconditionally injects default `aggregatedCell` (`rowAggregationFeature.ts:15-20`), so user's `cell` formatter never wins on aggregated cells without `aggregatedCell: null`. Resolve the fallback lazily instead. +- **[#6475](https://github.com/TanStack/table/pull/6475)** — draft POC refactoring the options store into atoms. Its previous full test/build run passed, but it now conflicts with current beta. -### Cluster 7: Core caching & memoization semantics +This touches exported/internal table structure, reactivity, adapters, devtools, and generated API docs. Decide before stable whether this architecture is part of v9; if yes, rebase and re-run the full suite, and if no, close the POC rather than carry it across the stable boundary. -- **[#5363](https://github.com/TanStack/table/issues/5363) + [#4485](https://github.com/TanStack/table/issues/4485) (high)** — `row._valuesCache` never invalidated when `columns`/`accessorFn` change (`createCoreRowModel.ts:28` memoizes on `[options.data]` only; `constructRow.ts:53`; `coreRowsFeature.utils.ts:82-94`). Stale values feed rendering, sorting, filtering, grouping. Fix choice: add columns to memoDeps (row identity churn) vs sweep caches on column-defs invalidation (keeps identity — preferred). -- **[#5275](https://github.com/TanStack/table/issues/5275) (med)** — `defaultColumn` baked in at column construction (`constructColumn.ts:45-48`); `table_getAllColumns` memoizes on `[options.columns]` only. Fix needs a documented memoize-`defaultColumn` contract; couple with the [#5363](https://github.com/TanStack/table/issues/5363) fix. -- **[#5909](https://github.com/TanStack/table/issues/5909) (high, trivial fix + semantics decision)** — `table_syncExternalStateToBaseAtoms` writes explicit `undefined` slices into base atoms (`coreTablesFeature.utils.ts:50-54`), violating non-optional `TableState`. One-line guard, but it changes controlled-state semantics: `undefined` flips from "clobber" to "uncontrolled, keep internal". Settle in beta. +### 6. Pagination capability API shape — decide now, implementation may wait -### Cluster 8: Public types +- **[#6385](https://github.com/TanStack/table/pull/6385)** — draft proposal adding `canNextPage` and `canPreviousPage` to `PaginationState`. -- **[#5908](https://github.com/TanStack/table/issues/5908) (med)** — `globalFilter: any` (`globalFilteringFeature.types.ts:15`) poisons `TableState`; widen to `unknown` (+ matching updater/option types). Consumers reading it as string stop compiling — type-breaking, beta only. -- **[#5971](https://github.com/TanStack/table/issues/5971) (med)** — `Table` not assignable to `Table`; `in out` annotations on `Table_Core` (`types/Table.ts:41-44`) + writable `optionsStore` atom make it genuinely invariant. Full covariance is unsound; realistic fix is a blessed `AnyTable` alias and/or bivariance on callback members. Decide during beta. -- **[#6302](https://github.com/TanStack/table/issues/6302) (med-low)** — `accessorKey: (string & {}) | keyof TData` (`ColumnDef.ts:230`) vs runtime deep-path support. Prefer the additive `DeepKeys | (string & {})` union; must clear a TS-perf benchmark against wide `TData` first. Either way the decision belongs in beta. +The feature is additive, so it is not a semver blocker. The durable question is whether server capability belongs in controlled state or in table options. Settle that shape during beta; merge only after full CI and state-update preservation tests are current. -## ANYTIME — valid, can land in any 9.x release +## Open beta PRs -| Issue | Priority | What/vehicle | -| ------------------------------------------------------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [#6078](https://github.com/TanStack/table/issues/6078) — `process is not defined` crash in bundler-less envs | **HIGH — do now anyway** | ~15 raw `process.env.NODE_ENV` reads survive into dist; `tsdown.config.ts` has no define. Pure crash fix. Combine with a corrected [#6445](https://github.com/TanStack/table/pull/6445): `typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'` | -| [#6074](https://github.com/TanStack/table/issues/6074) — `filterFromLeafRows` wipes `columnFiltersMeta` | med | One-line copy at `filterRowsUtils.ts:65`; port [#6075](https://github.com/TanStack/table/pull/6075) | -| [#6077](https://github.com/TanStack/table/issues/6077) — Vue SSR hydration mismatch on empty-string cells | high-med | Normalize `''` → `null` inside v9 `flexRender()` (`packages/vue-table/src/FlexRender.ts:50,57,143`), not the v8 component patch | -| [#5850](https://github.com/TanStack/table/issues/5850) — `table.removeRowSelection(ids)` helper | low-med | Additive method Kevin agreed to; referential no-op when ids absent | -| [#5008](https://github.com/TanStack/table/issues/5008) — median aggregation bails on null | med | Skip nullish like `mean` does (`aggregationFns.ts:274` vs `:247`); leaning land-in-beta for tidiness but non-breaking | -| [#5864](https://github.com/TanStack/table/issues/5864) — `mode()` aggregation fn | low | Additive, tree-shakeable; reporter supplied implementation | -| [#5653](https://github.com/TanStack/table/issues/5653) (docs half) — `sortUndefined: false` misdocumented | med | All framework sorting guides describe `false` incorrectly ("considered tied" vs passes undefined through) | -| [#4825](https://github.com/TanStack/table/issues/4825) — full-width resize redistribute mode | med | Additive opt-in mode; pure `ColumnSizingState` math | -| [#4512](https://github.com/TanStack/table/issues/4512) — resizing under `transform: scale(k)` | low-med | Additive scale option/arg to `getResizeHandler` | -| [#6230](https://github.com/TanStack/table/issues/6230) — `CellContext.table` missing React members | med | Types-only: add phantom `Table_Plugins` interface in core (mirror `TableMeta` pattern), react adapter merges `Subscribe`/`FlexRender` via `declare module` | -| [#5696](https://github.com/TanStack/table/issues/5696) — cell materialization memory ceiling | med | Partially mitigated in v9 (WeakMap cache, shared prototypes); remaining work is additive lazy `row.getCell(column)` | -| [#6011](https://github.com/TanStack/table/issues/6011) — virtualized-columns example end-of-scroll jank | low-med | Example-only: missing `box-sizing: border-box` + `flex-shrink: 0` makes scrollWidth overshoot `getTotalSize()`, creating a clamp feedback loop. Not a TanStack Virtual bug | +| PR | Live state | Recommendation | +| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [#6475 — options store into atoms](https://github.com/TanStack/table/pull/6475) | Draft, conflicting; prior full CI green | **Beta decision.** Rebase and land before stable, or close the POC. | +| [#6474 — improve table devtools performance](https://github.com/TanStack/table/pull/6474) | Non-draft, clean, full CI green | **Mergeable now.** Devtools-only; not a core API blocker. | +| [#6472 — align grouped sticky headers](https://github.com/TanStack/table/pull/6472) | Non-draft, mergeable, only security/review checks reported | **Re-run full CI, then merge.** It fixes the example for [#5397](https://github.com/TanStack/table/issues/5397) using existing header-segment leaf APIs; no core API change is required. | +| [#6445 — hoist NODE_ENV checks](https://github.com/TanStack/table/pull/6445) | Draft, mergeable, incomplete CI | **Fix before merge.** Module-scope `process.env` reads still crash the bundler-less environment from [#6078](https://github.com/TanStack/table/issues/6078). Guard `process` (or replace it at build time), then decide `!== 'production'` warning semantics and run full CI. | +| [#6385 — pagination capability overrides](https://github.com/TanStack/table/pull/6385) | Draft, mergeable, incomplete CI | **API-shape decision.** State versus options; additive after that decision. | +| [#6361 — filter-depth flatRows](https://github.com/TanStack/table/pull/6361) | Conflicting | **Close now.** Equivalent beta implementation is merged in `8fcfd3453`. | +| [#6313 — numeric range guard](https://github.com/TanStack/table/pull/6313) | Conflicting | **Close now.** Equivalent beta implementation is merged in `8fcfd3453`. | -## Open beta PRs — status & recommendation +## Open issues that can close now -| PR | State | CI | Behavior-changing | Recommendation | -| -------------------------------------------------------------------------------------------------- | ------------------ | ---------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [#6442](https://github.com/TanStack/table/pull/6442) — group column visibility | draft, **clean** | **fully green** | Yes (visibility outcome + persisted state shape) | **Merge during beta.** Healthiest PR; undraft + sign off leaf-ID fan-out. Closes [#5770](https://github.com/TanStack/table/issues/5770) + [#5497](https://github.com/TanStack/table/issues/5497) | -| [#6313](https://github.com/TanStack/table/pull/6313) — inNumberRange numeric guard | conflicting | stale | Yes (default filter output) | **Merge during beta** after rebase | -| [#6361](https://github.com/TanStack/table/pull/6361) — flatRows under maxLeafRowFilterDepth | mergeable | **Test FAILING** | Yes (flatRows/facet counts) | **Merge during beta** after fixing types check; stale since June | -| [#6443](https://github.com/TanStack/table/pull/6443) — reset expansion on data change | draft, conflicting | Test failing | Yes (autoResetExpanded default alignment) | **Merge during beta**, but only AFTER the [#5968](https://github.com/TanStack/table/issues/5968) first-run guard lands | -| [#6445](https://github.com/TanStack/table/pull/6445) — NODE_ENV hoist + `!== 'production'` flip | draft | partial | Partially (dev-warning visibility) | **Needs decision + fix.** As written it does NOT fix [#6078](https://github.com/TanStack/table/issues/6078) — hoisting the unguarded read makes bundler-less envs crash at import time. Add `typeof process` guard, then it can close [#6078](https://github.com/TanStack/table/issues/6078) | -| [#6385](https://github.com/TanStack/table/pull/6385) — manual canNextPage/canPreviousPage override | draft | partial | No (additive) | **Needs API-shape decision** (state vs options); once decided, can ship anytime | +These are still open on GitHub, but no additional implementation is needed for the reported behavior. -## Suggested beta execution order +| Open issue | Why it can close | +| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [#4485 — accessor function does not update](https://github.com/TanStack/table/issues/4485) | Duplicate of canonical [#5363](https://github.com/TanStack/table/issues/5363); keep one cache-invalidation issue. | +| [#4759 — cannot deselect children through a grouped row](https://github.com/TanStack/table/issues/4759) | Current beta's row-selection implementation recursively toggles selectable descendants even when the synthetic group row itself is not selectable. The v8 early-return behavior reported here is gone. | +| [#4879 — selection on grouped flat data](https://github.com/TanStack/table/issues/4879) | Current beta keeps synthetic group IDs out of selection and derives group checkbox state from descendants; the reported v8 flows are resolved. | +| [#4880 — full-width column sizing state mismatch](https://github.com/TanStack/table/issues/4880) | Duplicate rendering request; consolidate into canonical [#4825](https://github.com/TanStack/table/issues/4825). Core state stores logical widths and does not mirror browser-distributed table widths. | +| [#5605 — aggregated cells with zero subrows](https://github.com/TanStack/table/issues/5605) | Current `cell_getIsAggregated` uses grouping identity and resolved aggregation functions; the reported `subRows.length` condition no longer exists. | +| [#5617 — array filter functions accept scalar strings](https://github.com/TanStack/table/issues/5617) | Current `arrIncludesAll`/`arrIncludesSome` explicitly reject non-array cell values, removing the reported accidental string-substring behavior. Use the scalar/equality filter for scalar cells. | +| [#6010 — full-width resize moves preceding columns](https://github.com/TanStack/table/issues/6010) | Same browser-layout/redistribution request as canonical [#4825](https://github.com/TanStack/table/issues/4825); keep one issue. | +| [#6158 — pagination callback loop on RSC navigation](https://github.com/TanStack/table/issues/6158) | Current beta's page-index reset returns before `onPaginationChange` when the target index already equals the current index, removing the reported v8 URL-sync loop. | -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. **[#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). (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.) -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"). +## Open PRs that can close now + +All of these remain open, but their equivalent fixes are already present on beta. + +| Open PR | Superseding beta implementation | +| ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| [#5978 — duplicate subrows in paginated flatRows](https://github.com/TanStack/table/pull/5978) | `aad2c292c` deduplicates paginated `flatRows`. | +| [#6116 — ignore non-expandable rows in expanded state](https://github.com/TanStack/table/pull/6116) | `aad2c292c` filters expand-all materialization and depth calculations by `getCanExpand`. | +| [#6184 — skip unchanged toggleExpanded](https://github.com/TanStack/table/pull/6184) | `aad2c292c` adds row-level and table-level no-op guards. | +| [#6194 — skip unchanged toggleExpanded](https://github.com/TanStack/table/pull/6194) | Duplicate of #6184 and superseded by `aad2c292c`. | +| [#6178 — respect custom filterFn.autoRemove](https://github.com/TanStack/table/pull/6178) | `8fcfd3453` makes a supplied `autoRemove` authoritative for defined values. | +| [#6195 — respect custom filterFn.autoRemove](https://github.com/TanStack/table/pull/6195) | Duplicate of #6178 and superseded by `8fcfd3453`. | +| [#6313 — exclude non-numeric range values](https://github.com/TanStack/table/pull/6313) | `8fcfd3453` contains the beta implementation and regression coverage. | +| [#6361 — preserve depth-truncated descendants in flatRows](https://github.com/TanStack/table/pull/6361) | `8fcfd3453` contains the beta implementation and regression coverage. | + +## Valid open work that can land anytime + +These are worth doing, but their natural fixes are additive, unambiguous bug fixes, adapter fixes, examples, or performance work rather than stable-API blockers. + +| Open item | Priority and next step | +| ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [#6078 — `process is not defined`](https://github.com/TanStack/table/issues/6078) | **Urgent release-quality bug.** Correct [#6445](https://github.com/TanStack/table/pull/6445) or add a build-time replacement plus a direct ESM/import-map regression. | +| [#6074 — leaf-up filtering drops `columnFiltersMeta`](https://github.com/TanStack/table/issues/6074) | Port the focused fix from open [#6075](https://github.com/TanStack/table/pull/6075) to beta and add a fuzzy-filter/rank-sort regression. | +| [#6077 — Vue empty-string hydration mismatch](https://github.com/TanStack/table/issues/6077) | Rework open [#6186](https://github.com/TanStack/table/pull/6186) against the v9 Vue adapter. | +| [#5397 — grouped sticky headers](https://github.com/TanStack/table/issues/5397) | Land open [#6472](https://github.com/TanStack/table/pull/6472) after full CI; its example-side solution shows no new core API is necessary. | +| [#5008 — median aggregation with nulls](https://github.com/TanStack/table/issues/5008) | Skip nullish values consistently with the other numeric aggregation functions. | +| [#5850 — remove selected row IDs helper](https://github.com/TanStack/table/issues/5850) | Additive helper already accepted in the issue thread. | +| [#5864 — `mode()` aggregation](https://github.com/TanStack/table/issues/5864) | Additive, tree-shakeable aggregation function. | +| [#4825 — full-width resize redistribution](https://github.com/TanStack/table/issues/4825) | Additive opt-in resize mode; canonical issue for the full-width sizing cluster. | +| [#4512 — resizing under CSS scale](https://github.com/TanStack/table/issues/4512) | Additive scale input/option for resize deltas. | +| [#6230 — React members missing from cell/header context types](https://github.com/TanStack/table/issues/6230) | Adapter typing improvement; do not merge the conflicting alpha approach in open [#6240](https://github.com/TanStack/table/pull/6240) as-is. | +| [#6474 — table devtools performance](https://github.com/TanStack/table/pull/6474) | Clean and fully green; merge when reviewed. | + +## Suggested order + +1. Close the eight superseded PRs and the eight resolved/duplicate issues above; this removes noise from the remaining beta view. +2. Decide the options-store POC ([#6475](https://github.com/TanStack/table/pull/6475)) and group-ID visibility contract ([#5770](https://github.com/TanStack/table/issues/5770)). +3. Implement the cache/default-column cluster ([#5363](https://github.com/TanStack/table/issues/5363) + [#5275](https://github.com/TanStack/table/issues/5275)); close [#4485](https://github.com/TanStack/table/issues/4485) as the duplicate. +4. Resolve the two remaining default-render/sort decisions ([#5778](https://github.com/TanStack/table/issues/5778), [#5653](https://github.com/TanStack/table/issues/5653)). +5. Decide state versus options for [#6385](https://github.com/TanStack/table/pull/6385). +6. Merge the clean devtools PR, re-run the grouped-header example PR, and fix the urgent `process` crash vehicle. diff --git a/examples/react/kitchen-sink/src/index.css b/examples/react/kitchen-sink/src/index.css index 9fba258414..66bd45a751 100644 --- a/examples/react/kitchen-sink/src/index.css +++ b/examples/react/kitchen-sink/src/index.css @@ -10,18 +10,18 @@ html { border: 1px solid lightgray; } -table { +.table-container table { /* box-shadow / borders + position: sticky don't play well otherwise */ border-collapse: collapse; border-spacing: 0; table-layout: fixed; } -thead { +.table-container thead { position: relative; } -th { +.table-container th { background-color: #f3f4f6; border-bottom: 1px solid lightgray; border-right: 1px solid lightgray; @@ -32,18 +32,18 @@ th { vertical-align: top; } -td { +.table-container td { background-color: white; border-bottom: 1px solid #eee; padding: 2px 4px; vertical-align: middle; } -tfoot { +.table-container tfoot { color: gray; } -tfoot th { +.table-container tfoot th { font-weight: normal; } diff --git a/packages/angular-table-devtools/src/TableDevtools.ts b/packages/angular-table-devtools/src/TableDevtools.ts index b88c366710..0ea790a3b4 100644 --- a/packages/angular-table-devtools/src/TableDevtools.ts +++ b/packages/angular-table-devtools/src/TableDevtools.ts @@ -1,12 +1,9 @@ import { TableDevtoolsCore } from '@tanstack/table-devtools' -import { createAngularPanel } from '@tanstack/devtools-utils/angular' +import { computed, effect } from '@angular/core' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/angular' export interface TableDevtoolsAngularInit extends Partial {} -const [TableDevtoolsPanelBase, TableDevtoolsPanelNoOpBase] = - createAngularPanel(TableDevtoolsCore) - function resolvePanelProps( props?: TableDevtoolsAngularInit, ): DevtoolsPanelProps { @@ -23,12 +20,26 @@ type TableDevtoolsPanelComponent = () => ( export const TableDevtoolsPanel: TableDevtoolsPanelComponent = () => (props, host) => { - const panel = TableDevtoolsPanelBase() - return panel(() => resolvePanelProps(props()), host) + const panel = host.ownerDocument.createElement('div') + panel.style.height = '100%' + host.appendChild(panel) + + const panelProps = computed(() => resolvePanelProps(props()), { + equal: (previous, next) => + previous.theme === next.theme && + previous.devtoolsOpen === next.devtoolsOpen, + }) + const panelEffect = effect((onCleanup) => { + const instance = new TableDevtoolsCore() + void instance.mount(panel, panelProps()) + onCleanup(() => instance.unmount()) + }) + + return () => { + panelEffect.destroy() + panel.remove() + } } export const TableDevtoolsPanelNoOp: TableDevtoolsPanelComponent = - () => (_props, _host) => { - const panel = TableDevtoolsPanelNoOpBase() - return () => panel - } + () => (_props, _host) => () => {} diff --git a/packages/angular-table-devtools/src/injectTanStackTableDevtools.ts b/packages/angular-table-devtools/src/injectTanStackTableDevtools.ts index eabca429b5..e8ca71c6e0 100644 --- a/packages/angular-table-devtools/src/injectTanStackTableDevtools.ts +++ b/packages/angular-table-devtools/src/injectTanStackTableDevtools.ts @@ -1,5 +1,6 @@ -import { upsertTableDevtoolsTarget } from '@tanstack/table-devtools' +import { createTableDevtoolsRegistrationManager } from '@tanstack/table-devtools' import { + DestroyRef, Injector, assertInInjectionContext, effect, @@ -30,17 +31,15 @@ export function injectTanStackTableDevtools< injector = inject(Injector) } + const destroyRef = injector.get(DestroyRef) + const registration = createTableDevtoolsRegistrationManager() + destroyRef.onDestroy(() => registration.dispose()) + effect( - (onCleanup) => { + () => { const { table } = options() const enabledValue = enabled() - if (!enabledValue || !table) { - return - } - const cleanup = untracked(() => upsertTableDevtoolsTarget({ table })) - onCleanup(() => { - cleanup?.() - }) + untracked(() => registration.update(table, enabledValue)) }, { injector }, ) diff --git a/packages/angular-table-devtools/src/production.ts b/packages/angular-table-devtools/src/production.ts index f3e96534ef..1256c41c5a 100644 --- a/packages/angular-table-devtools/src/production.ts +++ b/packages/angular-table-devtools/src/production.ts @@ -1,5 +1,5 @@ -export { TableDevtoolsPanel } from './TableDevtools' +export { TableDevtoolsPanel } from './production/TableDevtools' export type { TableDevtoolsAngularInit } from './TableDevtools' -export { tableDevtoolsPlugin } from './plugin' +export { tableDevtoolsPlugin } from './production/plugin' export { injectTanStackTableDevtools } from './injectTanStackTableDevtools' export type { InjectTanStackTableDevtoolsOptions } from './injectTanStackTableDevtools' diff --git a/packages/angular-table-devtools/src/production/TableDevtools.ts b/packages/angular-table-devtools/src/production/TableDevtools.ts new file mode 100644 index 0000000000..c9061540c8 --- /dev/null +++ b/packages/angular-table-devtools/src/production/TableDevtools.ts @@ -0,0 +1,41 @@ +import { computed, effect } from '@angular/core' +import { TableDevtoolsCore } from '@tanstack/table-devtools/production' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/angular' +import type { TableDevtoolsAngularInit } from '../TableDevtools' + +function resolvePanelProps( + props?: TableDevtoolsAngularInit, +): DevtoolsPanelProps { + return { + theme: props?.theme ?? 'dark', + devtoolsOpen: props?.devtoolsOpen ?? false, + } +} + +type TableDevtoolsPanelComponent = () => ( + inputs: () => TableDevtoolsAngularInit, + hostElement: HTMLElement, +) => () => void + +export const TableDevtoolsPanel: TableDevtoolsPanelComponent = + () => (props, host) => { + const panel = host.ownerDocument.createElement('div') + panel.style.height = '100%' + host.appendChild(panel) + + const panelProps = computed(() => resolvePanelProps(props()), { + equal: (previous, next) => + previous.theme === next.theme && + previous.devtoolsOpen === next.devtoolsOpen, + }) + const panelEffect = effect((onCleanup) => { + const instance = new TableDevtoolsCore() + void instance.mount(panel, panelProps()) + onCleanup(() => instance.unmount()) + }) + + return () => { + panelEffect.destroy() + panel.remove() + } + } diff --git a/packages/angular-table-devtools/src/production/plugin.ts b/packages/angular-table-devtools/src/production/plugin.ts new file mode 100644 index 0000000000..73c8683158 --- /dev/null +++ b/packages/angular-table-devtools/src/production/plugin.ts @@ -0,0 +1,11 @@ +import { createAngularPlugin } from '@tanstack/devtools-utils/angular' +import { TableDevtoolsPanel } from './TableDevtools' + +type TableDevtoolsPluginFactory = ReturnType[0] + +const [plugin] = createAngularPlugin({ + name: 'TanStack Table', + render: TableDevtoolsPanel, +}) + +export const tableDevtoolsPlugin: TableDevtoolsPluginFactory = plugin diff --git a/packages/preact-table-devtools/src/PreactTableDevtools.tsx b/packages/preact-table-devtools/src/PreactTableDevtools.tsx index b84cae7ced..9e0f7e681b 100644 --- a/packages/preact-table-devtools/src/PreactTableDevtools.tsx +++ b/packages/preact-table-devtools/src/PreactTableDevtools.tsx @@ -1,4 +1,5 @@ import { h } from 'preact' +import { useMemo } from 'preact/hooks' import { createPreactPanel } from '@tanstack/devtools-utils/preact' import { TableDevtoolsCore } from '@tanstack/table-devtools' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact' @@ -22,8 +23,22 @@ function resolvePanelProps( } } -export const TableDevtoolsPanel: TableDevtoolsPanelComponent = (props) => - h(TableDevtoolsPanelBase, resolvePanelProps(props)) +export const TableDevtoolsPanel: TableDevtoolsPanelComponent = (props) => { + const theme = props?.theme + const devtoolsOpen = props?.devtoolsOpen + const panelProps = useMemo( + () => resolvePanelProps({ theme, devtoolsOpen }), + [devtoolsOpen, theme], + ) + return h(TableDevtoolsPanelBase, panelProps) +} -export const TableDevtoolsPanelNoOp: TableDevtoolsPanelComponent = (props) => - h(TableDevtoolsPanelNoOpBase, resolvePanelProps(props)) +export const TableDevtoolsPanelNoOp: TableDevtoolsPanelComponent = (props) => { + const theme = props?.theme + const devtoolsOpen = props?.devtoolsOpen + const panelProps = useMemo( + () => resolvePanelProps({ theme, devtoolsOpen }), + [devtoolsOpen, theme], + ) + return h(TableDevtoolsPanelNoOpBase, panelProps) +} diff --git a/packages/preact-table-devtools/src/production.ts b/packages/preact-table-devtools/src/production.ts index 2212d72fce..9d7e6de391 100644 --- a/packages/preact-table-devtools/src/production.ts +++ b/packages/preact-table-devtools/src/production.ts @@ -1,7 +1,7 @@ 'use client' -export { TableDevtoolsPanel } from './PreactTableDevtools' +export { TableDevtoolsPanel } from './production/PreactTableDevtools' export type { TableDevtoolsPreactInit } from './PreactTableDevtools' -export { tableDevtoolsPlugin } from './plugin' +export { tableDevtoolsPlugin } from './production/plugin' export { useTanStackTableDevtools } from './useTanStackTableDevtools' export type { UseTanStackTableDevtoolsOptions } from './useTanStackTableDevtools' diff --git a/packages/preact-table-devtools/src/production/PreactTableDevtools.tsx b/packages/preact-table-devtools/src/production/PreactTableDevtools.tsx new file mode 100644 index 0000000000..7606a20f5c --- /dev/null +++ b/packages/preact-table-devtools/src/production/PreactTableDevtools.tsx @@ -0,0 +1,32 @@ +import { h } from 'preact' +import { useMemo } from 'preact/hooks' +import { createPreactPanel } from '@tanstack/devtools-utils/preact' +import { TableDevtoolsCore } from '@tanstack/table-devtools/production' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact' +import type { JSX } from 'preact' +import type { TableDevtoolsPreactInit } from '../PreactTableDevtools' + +type TableDevtoolsPanelComponent = ( + props?: TableDevtoolsPreactInit, +) => JSX.Element + +const [TableDevtoolsPanelBase] = createPreactPanel(TableDevtoolsCore) + +function resolvePanelProps( + props?: TableDevtoolsPreactInit, +): DevtoolsPanelProps { + return { + theme: props?.theme ?? 'dark', + devtoolsOpen: props?.devtoolsOpen ?? false, + } +} + +export const TableDevtoolsPanel: TableDevtoolsPanelComponent = (props) => { + const theme = props?.theme + const devtoolsOpen = props?.devtoolsOpen + const panelProps = useMemo( + () => resolvePanelProps({ theme, devtoolsOpen }), + [devtoolsOpen, theme], + ) + return h(TableDevtoolsPanelBase, panelProps) +} diff --git a/packages/preact-table-devtools/src/production/plugin.tsx b/packages/preact-table-devtools/src/production/plugin.tsx new file mode 100644 index 0000000000..a75f39c20d --- /dev/null +++ b/packages/preact-table-devtools/src/production/plugin.tsx @@ -0,0 +1,13 @@ +import { createPreactPlugin } from '@tanstack/devtools-utils/preact' +import { TableDevtoolsPanel } from './PreactTableDevtools' + +type PreactTableDevtoolsPlugin = ReturnType< + ReturnType[0] +> + +const [plugin] = createPreactPlugin({ + name: 'TanStack Table', + Component: TableDevtoolsPanel, +}) + +export const tableDevtoolsPlugin: () => PreactTableDevtoolsPlugin = plugin diff --git a/packages/preact-table-devtools/src/useTanStackTableDevtools.ts b/packages/preact-table-devtools/src/useTanStackTableDevtools.ts index 80b51f5ca4..52682f8044 100644 --- a/packages/preact-table-devtools/src/useTanStackTableDevtools.ts +++ b/packages/preact-table-devtools/src/useTanStackTableDevtools.ts @@ -1,7 +1,7 @@ 'use client' -import { useEffect } from 'preact/hooks' -import { upsertTableDevtoolsTarget } from '@tanstack/table-devtools' +import { useEffect, useRef } from 'preact/hooks' +import { createTableDevtoolsRegistrationManager } from '@tanstack/table-devtools' import type { RowData, Table, TableFeatures } from '@tanstack/table-core' export interface UseTanStackTableDevtoolsOptions { @@ -16,18 +16,18 @@ export function useTanStackTableDevtools< options?: UseTanStackTableDevtoolsOptions, ): void { const enabled = options?.enabled ?? true + const registrationRef = + useRef>() + registrationRef.current ??= createTableDevtoolsRegistrationManager() + const registration = registrationRef.current useEffect(() => { - if (!enabled || !table) { - return - } + return () => registration.dispose() + }, [registration]) - const cleanup = upsertTableDevtoolsTarget({ table }) - - return () => { - cleanup?.() - } - }, [enabled, table, table?.options.key]) + useEffect(() => { + registration.update(table, enabled) + }, [enabled, registration, table, table?.options.key]) } export function useTanStackTableDevtoolsNoOp< diff --git a/packages/react-table-devtools/src/ReactTableDevtools.tsx b/packages/react-table-devtools/src/ReactTableDevtools.tsx index 898c95f67d..4fbd67f137 100644 --- a/packages/react-table-devtools/src/ReactTableDevtools.tsx +++ b/packages/react-table-devtools/src/ReactTableDevtools.tsx @@ -1,4 +1,4 @@ -import { createElement } from 'react' +import { createElement, useMemo } from 'react' import { createReactPanel } from '@tanstack/devtools-utils/react' import { TableDevtoolsCore } from '@tanstack/table-devtools' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react' @@ -20,8 +20,22 @@ function resolvePanelProps(props?: TableDevtoolsReactInit): DevtoolsPanelProps { } } -export const TableDevtoolsPanel: TableDevtoolsPanelComponent = (props) => - createElement(TableDevtoolsPanelBase, resolvePanelProps(props)) +export const TableDevtoolsPanel: TableDevtoolsPanelComponent = (props) => { + const theme = props?.theme + const devtoolsOpen = props?.devtoolsOpen + const panelProps = useMemo( + () => resolvePanelProps({ theme, devtoolsOpen }), + [devtoolsOpen, theme], + ) + return createElement(TableDevtoolsPanelBase, panelProps) +} -export const TableDevtoolsPanelNoOp: TableDevtoolsPanelComponent = (props) => - createElement(TableDevtoolsPanelNoOpBase, resolvePanelProps(props)) +export const TableDevtoolsPanelNoOp: TableDevtoolsPanelComponent = (props) => { + const theme = props?.theme + const devtoolsOpen = props?.devtoolsOpen + const panelProps = useMemo( + () => resolvePanelProps({ theme, devtoolsOpen }), + [devtoolsOpen, theme], + ) + return createElement(TableDevtoolsPanelNoOpBase, panelProps) +} diff --git a/packages/react-table-devtools/src/production.ts b/packages/react-table-devtools/src/production.ts index d31f8bce12..17fbb08574 100644 --- a/packages/react-table-devtools/src/production.ts +++ b/packages/react-table-devtools/src/production.ts @@ -1,7 +1,7 @@ 'use client' -export { TableDevtoolsPanel } from './ReactTableDevtools' +export { TableDevtoolsPanel } from './production/ReactTableDevtools' export type { TableDevtoolsReactInit } from './ReactTableDevtools' -export { tableDevtoolsPlugin } from './plugin' +export { tableDevtoolsPlugin } from './production/plugin' export { useTanStackTableDevtools } from './useTanStackTableDevtools' export type { UseTanStackTableDevtoolsOptions } from './useTanStackTableDevtools' diff --git a/packages/react-table-devtools/src/production/ReactTableDevtools.tsx b/packages/react-table-devtools/src/production/ReactTableDevtools.tsx new file mode 100644 index 0000000000..5d2f4e5dd8 --- /dev/null +++ b/packages/react-table-devtools/src/production/ReactTableDevtools.tsx @@ -0,0 +1,29 @@ +import { createElement, useMemo } from 'react' +import { createReactPanel } from '@tanstack/devtools-utils/react' +import { TableDevtoolsCore } from '@tanstack/table-devtools/production' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react' +import type { JSX } from 'react' +import type { TableDevtoolsReactInit } from '../ReactTableDevtools' + +type TableDevtoolsPanelComponent = ( + props?: TableDevtoolsReactInit, +) => JSX.Element + +const [TableDevtoolsPanelBase] = createReactPanel(TableDevtoolsCore) + +function resolvePanelProps(props?: TableDevtoolsReactInit): DevtoolsPanelProps { + return { + theme: props?.theme ?? 'dark', + devtoolsOpen: props?.devtoolsOpen ?? false, + } +} + +export const TableDevtoolsPanel: TableDevtoolsPanelComponent = (props) => { + const theme = props?.theme + const devtoolsOpen = props?.devtoolsOpen + const panelProps = useMemo( + () => resolvePanelProps({ theme, devtoolsOpen }), + [devtoolsOpen, theme], + ) + return createElement(TableDevtoolsPanelBase, panelProps) +} diff --git a/packages/react-table-devtools/src/production/plugin.tsx b/packages/react-table-devtools/src/production/plugin.tsx new file mode 100644 index 0000000000..af649b07db --- /dev/null +++ b/packages/react-table-devtools/src/production/plugin.tsx @@ -0,0 +1,11 @@ +import { createReactPlugin } from '@tanstack/devtools-utils/react' +import { TableDevtoolsPanel } from './ReactTableDevtools' + +type TableDevtoolsPluginFactory = ReturnType[0] + +const [plugin] = createReactPlugin({ + name: 'TanStack Table', + Component: TableDevtoolsPanel, +}) + +export const tableDevtoolsPlugin: TableDevtoolsPluginFactory = plugin diff --git a/packages/react-table-devtools/src/table-devtools.d.ts b/packages/react-table-devtools/src/table-devtools.d.ts index e8b6b1d122..e8db884dc5 100644 --- a/packages/react-table-devtools/src/table-devtools.d.ts +++ b/packages/react-table-devtools/src/table-devtools.d.ts @@ -31,4 +31,14 @@ declare module '@tanstack/table-devtools' { ): (() => void) | undefined export function removeTableDevtoolsTarget(id: string): void + + export interface TableDevtoolsRegistrationManager { + update: ( + table: Table | undefined, + enabled?: boolean, + ) => void + dispose: () => void + } + + export function createTableDevtoolsRegistrationManager(): TableDevtoolsRegistrationManager } diff --git a/packages/react-table-devtools/src/useTanStackTableDevtools.ts b/packages/react-table-devtools/src/useTanStackTableDevtools.ts index fed82bad9b..1375f57be2 100644 --- a/packages/react-table-devtools/src/useTanStackTableDevtools.ts +++ b/packages/react-table-devtools/src/useTanStackTableDevtools.ts @@ -1,7 +1,7 @@ 'use client' -import { upsertTableDevtoolsTarget } from '@tanstack/table-devtools' -import { useEffect } from 'react' +import { createTableDevtoolsRegistrationManager } from '@tanstack/table-devtools' +import { useEffect, useState } from 'react' import type { RowData, Table, TableFeatures } from '@tanstack/table-core' export interface UseTanStackTableDevtoolsOptions { @@ -16,18 +16,15 @@ export function useTanStackTableDevtools< options?: UseTanStackTableDevtoolsOptions, ): void { const enabled = options?.enabled ?? true + const [registration] = useState(createTableDevtoolsRegistrationManager) useEffect(() => { - if (!enabled || !table) { - return - } + return () => registration.dispose() + }, [registration]) - const cleanup = upsertTableDevtoolsTarget({ table }) - - return () => { - cleanup?.() - } - }, [enabled, table, table?.options.key]) + useEffect(() => { + registration.update(table, enabled) + }, [enabled, registration, table, table?.options.key]) } export function useTanStackTableDevtoolsNoOp< diff --git a/packages/solid-table-devtools/src/TableDevtools.tsx b/packages/solid-table-devtools/src/TableDevtools.tsx index 75b8f100ce..b20fea09f4 100644 --- a/packages/solid-table-devtools/src/TableDevtools.tsx +++ b/packages/solid-table-devtools/src/TableDevtools.tsx @@ -14,8 +14,12 @@ const [TableDevtoolsPanelBase, TableDevtoolsPanelNoOpBase] = function resolvePanelProps(props?: TableDevtoolsSolidInit): DevtoolsPanelProps { return { - theme: props?.theme ?? 'dark', - devtoolsOpen: props?.devtoolsOpen ?? false, + get theme() { + return props?.theme ?? 'dark' + }, + get devtoolsOpen() { + return props?.devtoolsOpen ?? false + }, } } diff --git a/packages/solid-table-devtools/src/production.ts b/packages/solid-table-devtools/src/production.ts index 010ae2139e..eddeb12413 100644 --- a/packages/solid-table-devtools/src/production.ts +++ b/packages/solid-table-devtools/src/production.ts @@ -1,4 +1,4 @@ -export { TableDevtoolsPanel } from './TableDevtools' +export { TableDevtoolsPanel } from './production/TableDevtools' export type { TableDevtoolsSolidInit } from './production/TableDevtools' diff --git a/packages/solid-table-devtools/src/production/TableDevtools.tsx b/packages/solid-table-devtools/src/production/TableDevtools.tsx index c6670acf6f..ec38ac5ae5 100644 --- a/packages/solid-table-devtools/src/production/TableDevtools.tsx +++ b/packages/solid-table-devtools/src/production/TableDevtools.tsx @@ -14,8 +14,12 @@ type TableDevtoolsPanelComponent = ( function resolvePanelProps(props?: TableDevtoolsSolidInit): DevtoolsPanelProps { return { - theme: props?.theme ?? 'dark', - devtoolsOpen: props?.devtoolsOpen ?? false, + get theme() { + return props?.theme ?? 'dark' + }, + get devtoolsOpen() { + return props?.devtoolsOpen ?? false + }, } } diff --git a/packages/solid-table-devtools/src/useTanStackTableDevtools.ts b/packages/solid-table-devtools/src/useTanStackTableDevtools.ts index 5c5fc60b40..160116b207 100644 --- a/packages/solid-table-devtools/src/useTanStackTableDevtools.ts +++ b/packages/solid-table-devtools/src/useTanStackTableDevtools.ts @@ -1,5 +1,5 @@ import { createRenderEffect, onCleanup } from 'solid-js' -import { upsertTableDevtoolsTarget } from '@tanstack/table-devtools' +import { createTableDevtoolsRegistrationManager } from '@tanstack/table-devtools' import type { RowData, Table, TableFeatures } from '@tanstack/table-core' export interface UseTanStackTableDevtoolsOptions { @@ -13,16 +13,11 @@ export function useTanStackTableDevtools< table: Table | undefined, options?: UseTanStackTableDevtoolsOptions, ): void { - createRenderEffect(() => { - if (!(options?.enabled ?? true) || !table) { - return - } - - const cleanup = upsertTableDevtoolsTarget({ table }) + const registration = createTableDevtoolsRegistrationManager() + onCleanup(() => registration.dispose()) - onCleanup(() => { - cleanup?.() - }) + createRenderEffect(() => { + registration.update(table, options?.enabled ?? true) }) } diff --git a/packages/table-devtools/src/TableDevtools.tsx b/packages/table-devtools/src/TableDevtools.tsx index a6e96ecec2..0f687b3557 100644 --- a/packages/table-devtools/src/TableDevtools.tsx +++ b/packages/table-devtools/src/TableDevtools.tsx @@ -1,3 +1,4 @@ +import { Show } from 'solid-js' import { ThemeContextProvider } from '@tanstack/devtools-ui' import { TableContextProvider } from './TableContextProvider' import { Shell } from './components/Shell' @@ -9,9 +10,11 @@ export default function TableDevtools(props: { }) { return ( - - - + + + + + ) } diff --git a/packages/table-devtools/src/components/ColumnsPanel.tsx b/packages/table-devtools/src/components/ColumnsPanel.tsx index 500e33a639..7434d6ad73 100644 --- a/packages/table-devtools/src/components/ColumnsPanel.tsx +++ b/packages/table-devtools/src/components/ColumnsPanel.tsx @@ -32,9 +32,9 @@ export function ColumnsPanel() { const styles = useStyles() const { table } = useTableDevtoolsContext() - const tableState = useTableStore( - () => table()?.store, - (state) => state, + const optionsStoreValue = useTableStore( + () => table()?.optionsStore, + (options) => options, ) const columns = createMemo>(() => { @@ -43,7 +43,7 @@ export function ColumnsPanel() { return [] } - tableState() + optionsStoreValue() const tableWithColumnFns = tableInstance as unknown as { getAllFlatColumns?: () => Array diff --git a/packages/table-devtools/src/components/FeaturesPanel.tsx b/packages/table-devtools/src/components/FeaturesPanel.tsx index 6c839ff0ae..49e2bfca38 100644 --- a/packages/table-devtools/src/components/FeaturesPanel.tsx +++ b/packages/table-devtools/src/components/FeaturesPanel.tsx @@ -157,19 +157,15 @@ export function FeaturesPanel() { () => table()?.store, (state) => state, ) - const tableOptions = useTableStore( - () => { - const tableInstance = table() - return tableInstance?.optionsStore ?? tableInstance?.store - }, - () => table()?.options as unknown, + const optionsStoreValue = useTableStore( + () => table()?.optionsStore, + (options) => options, ) const tableFeatures = createMemo((): Set => { const tableInstance = table() if (!tableInstance) return new Set() - tableState() return new Set(Object.keys(tableInstance._features)) }) @@ -177,8 +173,7 @@ export function FeaturesPanel() { const tableInstance = table() if (!tableInstance) return [] - tableState() - tableOptions() + optionsStoreValue() return Object.keys(tableInstance.options.features ?? {}).filter((key) => ROW_MODEL_FEATURE_SLOTS.includes(key), @@ -191,8 +186,7 @@ export function FeaturesPanel() { const tableInstance = table() if (!tableInstance) return [] - tableState() - tableOptions() + optionsStoreValue() const rowModelFns = toFnBuckets(tableInstance._rowModelFns) const optionFns = toFnBuckets(tableInstance.options) diff --git a/packages/table-devtools/src/components/OptionsPanel.tsx b/packages/table-devtools/src/components/OptionsPanel.tsx index e0753c4d0f..d9ef754c2a 100644 --- a/packages/table-devtools/src/components/OptionsPanel.tsx +++ b/packages/table-devtools/src/components/OptionsPanel.tsx @@ -20,17 +20,9 @@ export function OptionsPanel() { const styles = useStyles() const { table } = useTableDevtoolsContext() - const tableOptions = useTableStore( - () => { - const tableInstance = table() - return tableInstance?.optionsStore ?? tableInstance?.store - }, - () => { - const tableInstance = table() - return tableInstance - ? projectOptionsForTree(tableInstance.options) - : undefined - }, + const optionsStoreValue = useTableStore( + () => table()?.optionsStore, + (options) => options, ) const options = createMemo(() => { @@ -39,8 +31,7 @@ export function OptionsPanel() { return undefined } - tableOptions() - return projectOptionsForTree(tableInstance.options) + return projectOptionsForTree(optionsStoreValue() ?? tableInstance.options) }) return ( diff --git a/packages/table-devtools/src/components/ResizableSplit.tsx b/packages/table-devtools/src/components/ResizableSplit.tsx index c8abfa661c..a19a411e0a 100644 --- a/packages/table-devtools/src/components/ResizableSplit.tsx +++ b/packages/table-devtools/src/components/ResizableSplit.tsx @@ -1,4 +1,4 @@ -import { createSignal } from 'solid-js' +import { createSignal, onCleanup } from 'solid-js' import { useStyles } from '../styles/use-styles' import type { JSX } from 'solid-js' @@ -14,14 +14,22 @@ interface ResizableSplitProps { export function ResizableSplit(props: ResizableSplitProps) { const styles = useStyles() const [leftPercent, setLeftPercent] = createSignal(DEFAULT_LEFT_PERCENT) + let cleanupDrag: (() => void) | undefined + + onCleanup(() => { + cleanupDrag?.() + }) const handleMouseDown = (e: MouseEvent) => { e.preventDefault() + cleanupDrag?.() - const onMouseMove = (moveEvent: MouseEvent) => { - const container = (e.target as HTMLElement).parentElement - if (!container) return + const container = (e.currentTarget as HTMLElement).parentElement + if (!container) return + const previousCursor = document.body.style.cursor + const previousUserSelect = document.body.style.userSelect + const onMouseMove = (moveEvent: MouseEvent) => { const rect = container.getBoundingClientRect() const x = moveEvent.clientX - rect.left const percent = Math.max( @@ -31,12 +39,14 @@ export function ResizableSplit(props: ResizableSplitProps) { setLeftPercent(percent) } - const onMouseUp = () => { + cleanupDrag = () => { document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) - document.body.style.cursor = '' - document.body.style.userSelect = '' + document.body.style.cursor = previousCursor + document.body.style.userSelect = previousUserSelect + cleanupDrag = undefined } + const onMouseUp = () => cleanupDrag?.() document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) diff --git a/packages/table-devtools/src/components/RowsPanel.tsx b/packages/table-devtools/src/components/RowsPanel.tsx index feb9f39f1c..519e147f55 100644 --- a/packages/table-devtools/src/components/RowsPanel.tsx +++ b/packages/table-devtools/src/components/RowsPanel.tsx @@ -51,12 +51,9 @@ export function RowsPanel() { () => table()?.store, (state) => state, ) - const tableOptions = useTableStore( - () => { - const tableInstance = table() - return tableInstance?.optionsStore ?? tableInstance?.store - }, - () => table()?.options as unknown, + const optionsStoreValue = useTableStore( + () => table()?.optionsStore, + (options) => options, ) const [selectedRowModel, setSelectedRowModel] = @@ -66,8 +63,7 @@ export function RowsPanel() { const tableInstance = table() if (!tableInstance) return undefined - tableState() - tableOptions() + optionsStoreValue() const data = tableInstance.options.data as ReadonlyArray if (!Array.isArray(data)) return data @@ -79,8 +75,7 @@ export function RowsPanel() { const tableInstance = table() if (!tableInstance) return 0 - tableState() - tableOptions() + optionsStoreValue() const data = tableInstance.options.data as ReadonlyArray return Array.isArray(data) ? data.length : 0 @@ -91,7 +86,7 @@ export function RowsPanel() { if (!tableInstance) return [] tableState() - tableOptions() + optionsStoreValue() const tableWithColumnFns = tableInstance as unknown as { getVisibleLeafColumns?: () => Array @@ -220,18 +215,25 @@ export function RowsPanel() { - {(row) => ( - - {row.id} - - {(cell) => ( - - {stringifyValue(cell.getValue())} - - )} - - - )} + {(row) => { + const cells = createMemo(() => { + tableState() + return getCells(row) + }) + + return ( + + {row.id} + + {(cell) => ( + + {stringifyValue(cell.getValue())} + + )} + + + ) + }} diff --git a/packages/table-devtools/src/components/Shell.tsx b/packages/table-devtools/src/components/Shell.tsx index 3da49071db..56ecbdb32c 100644 --- a/packages/table-devtools/src/components/Shell.tsx +++ b/packages/table-devtools/src/components/Shell.tsx @@ -25,7 +25,6 @@ export function Shell() { setActiveTab, selectedTargetId, setSelectedTargetId, - table, targets, } = useTableDevtoolsContext() @@ -81,8 +80,8 @@ export function Shell() {
- - {(_table) => ( + + {(_selectedTargetId) => ( diff --git a/packages/table-devtools/src/components/StatePanel.tsx b/packages/table-devtools/src/components/StatePanel.tsx index 0095b61075..8e6c4efefb 100644 --- a/packages/table-devtools/src/components/StatePanel.tsx +++ b/packages/table-devtools/src/components/StatePanel.tsx @@ -6,15 +6,11 @@ import { useTableStore } from '../useTableStore' import { useStyles } from '../styles/use-styles' import { NoTableConnected } from './NoTableConnected' import { ThreeWayResizableSplit } from './ThreeWayResizableSplit' +import type { Accessor } from 'solid-js' +import type { TableDevtoolsStyles } from '../styles/use-styles' type AtomSource = 'external-atom' | 'external-state' | 'internal' -interface AtomSlice { - key: string - value: unknown - source: AtomSource -} - export function StatePanel() { const styles = useStyles() const { table } = useTableDevtoolsContext() @@ -22,75 +18,62 @@ export function StatePanel() { const [storeCopied, setStoreCopied] = createSignal(false) const [pasteError, setPasteError] = createSignal(null) - // Subscribe to both stores so the panel re-renders when either the table - // state or the options (e.g. options.atoms / options.state) change. const tableState = useTableStore( () => table()?.store, (state) => state, ) - const tableOptions = useTableStore( - () => { - const tableInstance = table() - return tableInstance?.optionsStore ?? tableInstance?.store - }, - () => table()?.options as unknown, + const optionsStoreValue = useTableStore( + () => table()?.optionsStore, + (options) => options, ) const initialState = createMemo((): unknown => { const tableInstance = table() if (!tableInstance) return undefined - tableState() - tableOptions() - return tableInstance.initialState }) - const storeState = createMemo((): unknown => { + const storeState = createMemo((): Record | undefined => { const tableInstance = table() if (!tableInstance) return undefined - tableState() - tableOptions() - - return tableInstance.store.state + return (tableState() ?? tableInstance.store.get()) as Record< + string, + unknown + > }) - const atomSlices = createMemo((): Array => { + const tableOptions = createMemo | undefined>(() => { const tableInstance = table() - if (!tableInstance) return [] + if (!tableInstance) return undefined + + return (optionsStoreValue() ?? tableInstance.options) as Record< + string, + unknown + > + }) - // Touch subscriptions so this recomputes on state or option change. - tableState() - tableOptions() + const atomKeys = createMemo(() => Object.keys(storeState() ?? {})) - const options = tableInstance.options as unknown as Record + const getAtomSource = (key: string): AtomSource => { + const options = tableOptions() ?? {} const externalAtoms = (options.atoms as Record | undefined) ?? {} const externalState = (options.state as Record | undefined) ?? {} - const storeState = tableInstance.store.state as Record - - return Object.keys(storeState).map((key) => { - const hasExternalAtom = externalAtoms[key] != null - const hasExternalState = - !hasExternalAtom && - key in externalState && - externalState[key] !== undefined - - const source: AtomSource = hasExternalAtom - ? 'external-atom' - : hasExternalState - ? 'external-state' - : 'internal' - - return { - key, - value: storeState[key], - source, - } - }) - }) + const hasExternalAtom = externalAtoms[key] != null + const hasExternalState = + !hasExternalAtom && + key in externalState && + externalState[key] !== undefined + + return hasExternalAtom + ? 'external-atom' + : hasExternalState + ? 'external-state' + : 'internal' + } const copyToClipboard = async ( value: unknown, @@ -177,8 +160,15 @@ export function StatePanel() { Reset to initialState
- - {(slice) => } + + {(key) => ( + getAtomSource(key)} + styles={styles} + value={() => storeState()?.[key]} + /> + )} } @@ -213,11 +203,14 @@ export function StatePanel() { ) } -function AtomRow(props: { slice: AtomSlice }) { - const styles = useStyles() - +function AtomRow(props: { + atomKey: string + source: Accessor + styles: Accessor + value: Accessor +}) { const badgeLabel = () => { - switch (props.slice.source) { + switch (props.source()) { case 'external-atom': return 'External Atom' case 'external-state': @@ -228,25 +221,25 @@ function AtomRow(props: { slice: AtomSlice }) { } const badgeClass = () => { - const base = styles().atomBadge - switch (props.slice.source) { + const base = props.styles().atomBadge + switch (props.source()) { case 'external-atom': - return `${base} ${styles().atomBadgeExternalAtom}` + return `${base} ${props.styles().atomBadgeExternalAtom}` case 'external-state': - return `${base} ${styles().atomBadgeExternalState}` + return `${base} ${props.styles().atomBadgeExternalState}` case 'internal': - return `${base} ${styles().atomBadgeInternal}` + return `${base} ${props.styles().atomBadgeInternal}` } } return ( -
-
- {props.slice.key} +
+
+ {props.atomKey} {badgeLabel()}
-
- +
+
) diff --git a/packages/table-devtools/src/components/ThreeWayResizableSplit.tsx b/packages/table-devtools/src/components/ThreeWayResizableSplit.tsx index 3a5803172e..098752c91b 100644 --- a/packages/table-devtools/src/components/ThreeWayResizableSplit.tsx +++ b/packages/table-devtools/src/components/ThreeWayResizableSplit.tsx @@ -1,4 +1,4 @@ -import { createSignal } from 'solid-js' +import { createSignal, onCleanup } from 'solid-js' import { useStyles } from '../styles/use-styles' import type { JSX } from 'solid-js' @@ -17,15 +17,24 @@ export function ThreeWayResizableSplit(props: ThreeWayResizableSplitProps) { const styles = useStyles() const [leftPercent, setLeftPercent] = createSignal(DEFAULT_LEFT_PERCENT) const [middlePercent, setMiddlePercent] = createSignal(DEFAULT_MIDDLE_PERCENT) + let cleanupDrag: (() => void) | undefined + + onCleanup(() => { + cleanupDrag?.() + }) const makeDragHandler = (which: 'left' | 'right'): ((e: MouseEvent) => void) => // eslint-disable-next-line solid/reactivity (e) => { e.preventDefault() + cleanupDrag?.() + const handleEl = e.currentTarget as HTMLElement const container = handleEl.parentElement if (!container) return + const previousCursor = document.body.style.cursor + const previousUserSelect = document.body.style.userSelect const startLeft = leftPercent() const startMiddle = middlePercent() @@ -56,12 +65,14 @@ export function ThreeWayResizableSplit(props: ThreeWayResizableSplitProps) { } } - const onMouseUp = () => { + cleanupDrag = () => { document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) - document.body.style.cursor = '' - document.body.style.userSelect = '' + document.body.style.cursor = previousCursor + document.body.style.userSelect = previousUserSelect + cleanupDrag = undefined } + const onMouseUp = () => cleanupDrag?.() document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) diff --git a/packages/table-devtools/src/devtoolsUpdateScheduler.ts b/packages/table-devtools/src/devtoolsUpdateScheduler.ts new file mode 100644 index 0000000000..25550b71bc --- /dev/null +++ b/packages/table-devtools/src/devtoolsUpdateScheduler.ts @@ -0,0 +1,77 @@ +import { batch } from 'solid-js' + +const UPDATE_DELAY_MS = 32 +const IDLE_TIMEOUT_MS = 100 + +type ScheduledUpdate = () => void + +const pendingUpdates = new Set() + +let delayHandle: ReturnType | undefined +let idleHandle: number | undefined + +function flushUpdates() { + delayHandle = undefined + idleHandle = undefined + + const updates = Array.from(pendingUpdates) + pendingUpdates.clear() + + batch(() => { + for (const update of updates) { + update() + } + }) +} + +function cancelScheduledFlush() { + if (delayHandle !== undefined) { + clearTimeout(delayHandle) + delayHandle = undefined + } + + if ( + idleHandle !== undefined && + typeof globalThis.cancelIdleCallback === 'function' + ) { + globalThis.cancelIdleCallback(idleHandle) + idleHandle = undefined + } +} + +function requestFlush() { + if (delayHandle !== undefined || idleHandle !== undefined) { + return + } + + delayHandle = setTimeout(() => { + delayHandle = undefined + + if (typeof globalThis.requestIdleCallback === 'function') { + idleHandle = globalThis.requestIdleCallback(flushUpdates, { + timeout: IDLE_TIMEOUT_MS, + }) + return + } + + delayHandle = setTimeout(flushUpdates, 0) + }, UPDATE_DELAY_MS) +} + +/** + * Coalesces devtools-only reactive work and yields to the inspected app before + * flushing it. The returned function removes this update if its owner unmounts + * or its source changes before the queue is flushed. + */ +export function scheduleDevtoolsUpdate(update: ScheduledUpdate) { + pendingUpdates.add(update) + requestFlush() + + return () => { + pendingUpdates.delete(update) + + if (pendingUpdates.size === 0) { + cancelScheduledFlush() + } + } +} diff --git a/packages/table-devtools/src/index.ts b/packages/table-devtools/src/index.ts index 1c5d82106e..a1ff6af3e8 100644 --- a/packages/table-devtools/src/index.ts +++ b/packages/table-devtools/src/index.ts @@ -10,6 +10,7 @@ export const TableDevtoolsCore: ClassType = export type { TableDevtoolsInit } from './core' export { + createTableDevtoolsRegistrationManager, getTableDevtoolsTargets, removeTableDevtoolsTarget, setTableDevtoolsTarget, @@ -18,6 +19,7 @@ export { } from './tableTarget' export type { TableDevtoolsRegistration, + TableDevtoolsRegistrationManager, TableDevtoolsStore, TableDevtoolsTable, UpsertTableDevtoolsTargetOptions, diff --git a/packages/table-devtools/src/production.ts b/packages/table-devtools/src/production.ts index df414118c9..d39c265b89 100644 --- a/packages/table-devtools/src/production.ts +++ b/packages/table-devtools/src/production.ts @@ -4,6 +4,7 @@ export { TableDevtoolsCore } from './core' export type { TableDevtoolsInit } from './core' export { + createTableDevtoolsRegistrationManager, getTableDevtoolsTargets, removeTableDevtoolsTarget, setTableDevtoolsTarget, @@ -12,6 +13,7 @@ export { } from './tableTarget' export type { TableDevtoolsRegistration, + TableDevtoolsRegistrationManager, TableDevtoolsStore, TableDevtoolsTable, UpsertTableDevtoolsTargetOptions, diff --git a/packages/table-devtools/src/styles/use-styles.ts b/packages/table-devtools/src/styles/use-styles.ts index 2793262938..1eae90b6e2 100644 --- a/packages/table-devtools/src/styles/use-styles.ts +++ b/packages/table-devtools/src/styles/use-styles.ts @@ -1,5 +1,5 @@ import * as goober from 'goober' -import { createEffect, createSignal } from 'solid-js' +import { createMemo } from 'solid-js' import { createTheme } from '@tanstack/devtools-ui' import { tokens } from './tokens' @@ -398,11 +398,21 @@ const stylesFactory = (theme: 'light' | 'dark') => { export function useStyles() { const { theme } = createTheme() - const [styles, setStyles] = createSignal(stylesFactory(theme())) + const styles = createMemo(() => getStyles(theme())) + return styles +} + +export type TableDevtoolsStyles = ReturnType - createEffect(() => { - setStyles(stylesFactory(theme())) - }) +const stylesByTheme = new Map<'light' | 'dark', TableDevtoolsStyles>() + +function getStyles(theme: 'light' | 'dark'): TableDevtoolsStyles { + const cached = stylesByTheme.get(theme) + if (cached) { + return cached + } + const styles = stylesFactory(theme) + stylesByTheme.set(theme, styles) return styles } diff --git a/packages/table-devtools/src/tableTarget.ts b/packages/table-devtools/src/tableTarget.ts index ab6d4c4a90..36f4a41c70 100644 --- a/packages/table-devtools/src/tableTarget.ts +++ b/packages/table-devtools/src/tableTarget.ts @@ -34,6 +34,11 @@ export interface TableDevtoolsRegistration { table: TableDevtoolsTable } +interface TableDevtoolsRegistrationEntry { + registration: TableDevtoolsRegistration + leases: Set +} + export interface UpsertTableDevtoolsTargetOptions< TFeatures extends TableFeatures, TData extends RowData, @@ -41,8 +46,16 @@ export interface UpsertTableDevtoolsTargetOptions< table: Table } +export interface TableDevtoolsRegistrationManager { + update: ( + table: Table | undefined, + enabled?: boolean, + ) => void + dispose: () => void +} + const [registrationsMap, setRegistrationsMap] = createSignal< - Map + Map >(new Map()) function getTableKey(table: TableDevtoolsTable) { @@ -63,35 +76,61 @@ export function upsertTableDevtoolsTarget< } const registrations = untrack(registrationsMap) - const existingRegistration = registrations.get(key) - - if (existingRegistration) { - if (existingRegistration.table === table) { + const existingEntry = registrations.get(key) + const lease = Symbol(key) + + if (existingEntry) { + if ( + existingEntry.registration.table === table || + existingEntry.registration.table.store === table.store + ) { + if (existingEntry.registration.table !== table) { + Object.assign(existingEntry.registration.table, table) + } + existingEntry.leases.add(lease) return () => { - removeTableDevtoolsTarget(key) + releaseTableDevtoolsTarget(key, lease) } } const nextRegistrations = new Map(registrations) nextRegistrations.set(key, { - id: key, - table, + registration: { + id: key, + table, + }, + leases: new Set([lease]), }) setRegistrationsMap(nextRegistrations) } else { const nextRegistrations = new Map(registrations) nextRegistrations.set(key, { - id: key, - table, + registration: { + id: key, + table, + }, + leases: new Set([lease]), }) setRegistrationsMap(nextRegistrations) } return () => { - removeTableDevtoolsTarget(key) + releaseTableDevtoolsTarget(key, lease) } } +function releaseTableDevtoolsTarget(id: string, lease: symbol) { + const registrations = untrack(registrationsMap) + const entry = registrations.get(id) + if (!entry?.leases.delete(lease) || entry.leases.size > 0) { + return + } + + const nextRegistrations = new Map(registrations) + nextRegistrations.delete(id) + setRegistrationsMap(nextRegistrations) +} + export function removeTableDevtoolsTarget(id: string) { const registrations = untrack(registrationsMap) if (!registrations.has(id)) { @@ -105,7 +144,7 @@ export function removeTableDevtoolsTarget(id: string) { } export function getTableDevtoolsTargets(): Array { - return Array.from(registrationsMap().values()) + return Array.from(registrationsMap().values(), (entry) => entry.registration) } export function subscribeTableDevtoolsTargets(listener: Listener) { @@ -129,3 +168,25 @@ export function setTableDevtoolsTarget< upsertTableDevtoolsTarget({ table }) } + +export function createTableDevtoolsRegistrationManager(): TableDevtoolsRegistrationManager { + let cleanup: (() => void) | undefined + + return { + update: (table, enabled = true) => { + if (!enabled || !table) { + cleanup?.() + cleanup = undefined + return + } + + const previousCleanup = cleanup + cleanup = upsertTableDevtoolsTarget({ table }) + previousCleanup?.() + }, + dispose: () => { + cleanup?.() + cleanup = undefined + }, + } +} diff --git a/packages/table-devtools/src/useTableStore.ts b/packages/table-devtools/src/useTableStore.ts index 70fbfa2ec4..f2af769a21 100644 --- a/packages/table-devtools/src/useTableStore.ts +++ b/packages/table-devtools/src/useTableStore.ts @@ -1,31 +1,60 @@ import { createEffect, createSignal, onCleanup } from 'solid-js' +import { scheduleDevtoolsUpdate } from './devtoolsUpdateScheduler' import type { Accessor } from 'solid-js' import type { Readable } from '@tanstack/solid-store' +interface UseTableStoreOptions { + equals?: false | ((previous: U | undefined, next: U | undefined) => boolean) +} + /** * Subscribes to a table store and returns a reactive signal. - * Handles both subscribe APIs: function return (store 0.8.x) and - * { unsubscribe } object return (store 0.9.x). + * Handles both function and `{ unsubscribe }` subscription results. */ export function useTableStore( storeAccessor: Accessor | null | undefined>, selector: (state: T) => U = (s) => s as unknown as U, + options?: UseTableStoreOptions, ): Accessor { - const initialValue = storeAccessor()?.get() - const [signal, setSignal] = createSignal( - initialValue ? selector(initialValue) : undefined, + const initialStore = storeAccessor() + const [signal, setSignal] = createSignal( + initialStore ? selector(initialStore.get()) : undefined, + { equals: options?.equals }, ) createEffect(() => { const store = storeAccessor() - if (!store) return + let cancelPendingUpdate: (() => void) | undefined - const subscription = store.subscribe(() => { - setSignal(() => selector(store.get())) - }) + if (!store) { + setSignal(() => undefined) + return + } + + setSignal(() => selector(store.get())) + + let latestValue: U + const subscription = store.subscribe((snapshot) => { + latestValue = selector(snapshot) + + if (cancelPendingUpdate) { + return + } + + cancelPendingUpdate = scheduleDevtoolsUpdate(() => { + cancelPendingUpdate = undefined + setSignal(() => latestValue) + }) + }) as unknown as { unsubscribe: () => void } | (() => void) onCleanup(() => { - subscription.unsubscribe() + cancelPendingUpdate?.() + + if (typeof subscription === 'function') { + subscription() + } else { + subscription.unsubscribe() + } }) }) diff --git a/packages/table-devtools/tests/panel-lifecycle.test.tsx b/packages/table-devtools/tests/panel-lifecycle.test.tsx new file mode 100644 index 0000000000..7ec8bdb399 --- /dev/null +++ b/packages/table-devtools/tests/panel-lifecycle.test.tsx @@ -0,0 +1,68 @@ +import { createSignal } from 'solid-js' +import { render } from 'solid-js/web' +import { afterEach, describe, expect, it, vi } from 'vitest' +import TableDevtools from '../src/TableDevtools' +import { + getTableDevtoolsTargets, + removeTableDevtoolsTarget, + upsertTableDevtoolsTarget, +} from '../src/tableTarget' +import type { TableDevtoolsTable } from '../src/tableTarget' + +function createDevtoolsTable() { + const unsubscribe = vi.fn() + const subscribe = vi.fn(() => ({ unsubscribe })) + const table = { + _features: {}, + _rowModelFns: {}, + baseAtoms: {}, + initialState: {}, + options: { + features: {}, + key: 'users-table', + }, + reset: vi.fn(), + store: { + get: () => ({}), + state: {}, + subscribe, + }, + } satisfies TableDevtoolsTable + + return { subscribe, table, unsubscribe } +} + +afterEach(() => { + for (const target of getTableDevtoolsTargets()) { + removeTableDevtoolsTarget(target.id) + } +}) + +describe('TableDevtools panel lifecycle', () => { + it('subscribes only while the devtools panel is open', async () => { + const { subscribe, table, unsubscribe } = createDevtoolsTable() + const cleanupTarget = upsertTableDevtoolsTarget({ + table: table as never, + }) + const [open, setOpen] = createSignal(false) + const element = document.createElement('div') + const dispose = render( + () => , + element, + ) + + await Promise.resolve() + expect(subscribe).not.toHaveBeenCalled() + + setOpen(true) + await Promise.resolve() + expect(subscribe).toHaveBeenCalledTimes(1) + + setOpen(false) + await Promise.resolve() + expect(unsubscribe).toHaveBeenCalledTimes(1) + + dispose() + cleanupTarget?.() + }) +}) diff --git a/packages/table-devtools/tests/styles.test.tsx b/packages/table-devtools/tests/styles.test.tsx new file mode 100644 index 0000000000..28dc5444f3 --- /dev/null +++ b/packages/table-devtools/tests/styles.test.tsx @@ -0,0 +1,51 @@ +import { createSignal } from 'solid-js' +import { render } from 'solid-js/web' +import { ThemeContextProvider } from '@tanstack/devtools-ui' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { css } = vi.hoisted(() => ({ + css: vi.fn(() => 'generated-class'), +})) + +vi.mock('goober', () => ({ + css, +})) + +beforeEach(() => { + css.mockClear() +}) + +describe('useStyles', () => { + it('generates each theme once across consumers', async () => { + const { useStyles } = await import('../src/styles/use-styles') + const [theme, setTheme] = createSignal<'light' | 'dark'>('dark') + + function Consumer() { + useStyles() + useStyles() + return null + } + + const element = document.createElement('div') + const dispose = render( + () => ( + + + + ), + element, + ) + + expect(css).toHaveBeenCalledTimes(51) + + setTheme('light') + await Promise.resolve() + expect(css).toHaveBeenCalledTimes(102) + + setTheme('dark') + await Promise.resolve() + expect(css).toHaveBeenCalledTimes(102) + + dispose() + }) +}) diff --git a/packages/table-devtools/tests/tableTarget.test.ts b/packages/table-devtools/tests/tableTarget.test.ts index 0f1d1082ea..4d92f4c6a3 100644 --- a/packages/table-devtools/tests/tableTarget.test.ts +++ b/packages/table-devtools/tests/tableTarget.test.ts @@ -2,8 +2,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { constructTable, coreFeatures } from '@tanstack/table-core' import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings' import { + createTableDevtoolsRegistrationManager, getTableDevtoolsTargets, removeTableDevtoolsTarget, + subscribeTableDevtoolsTargets, upsertTableDevtoolsTarget, } from '../src/tableTarget' @@ -64,6 +66,100 @@ describe('tableTarget', () => { expect(getTableDevtoolsTargets()).toEqual([]) }) + it('does not let stale cleanup remove a replacement table', () => { + const firstTable = createTable('users-table') + const nextTable = createTable('users-table') + + const cleanupFirst = upsertTableDevtoolsTarget({ table: firstTable }) + const cleanupNext = upsertTableDevtoolsTarget({ table: nextTable }) + + cleanupFirst?.() + + expect(getTableDevtoolsTargets()).toEqual([ + { + id: 'users-table', + table: nextTable, + }, + ]) + + cleanupNext?.() + expect(getTableDevtoolsTargets()).toEqual([]) + }) + + it('keeps a shared table registered until its final lease is released', () => { + const table = createTable('users-table') + + const cleanupFirst = upsertTableDevtoolsTarget({ table }) + const cleanupSecond = upsertTableDevtoolsTarget({ table }) + + cleanupFirst?.() + expect(getTableDevtoolsTargets()).toHaveLength(1) + + cleanupSecond?.() + expect(getTableDevtoolsTargets()).toEqual([]) + }) + + it('replaces a managed registration without publishing an empty registry', () => { + const manager = createTableDevtoolsRegistrationManager() + const firstTable = createTable('users-table') + const nextTable = createTable('users-table') + + manager.update(firstTable) + + const targetCounts: Array = [] + const unsubscribe = subscribeTableDevtoolsTargets((targets) => { + targetCounts.push(targets.length) + }) + + manager.update(nextTable) + + expect(targetCounts).toEqual([1, 1]) + expect(getTableDevtoolsTargets()).toEqual([ + { + id: 'users-table', + table: nextTable, + }, + ]) + + manager.dispose() + + expect(targetCounts).toEqual([1, 1, 0]) + unsubscribe() + }) + + it('updates transient wrappers without publishing a new target', () => { + const manager = createTableDevtoolsRegistrationManager() + const table = createTable('users-table') + const firstWrapper = { + ...table, + options: { + ...table.options, + debugAll: false, + }, + } + const nextWrapper = { + ...table, + options: { + ...table.options, + debugAll: true, + }, + } + + manager.update(firstWrapper) + + const listener = vi.fn() + const unsubscribe = subscribeTableDevtoolsTargets(listener) + + manager.update(nextWrapper) + + expect(listener).toHaveBeenCalledTimes(1) + expect(getTableDevtoolsTargets()[0]?.table).toBe(firstWrapper) + expect(getTableDevtoolsTargets()[0]?.table.options.debugAll).toBe(true) + + unsubscribe() + manager.dispose() + }) + it('logs and skips registration when the key is missing', () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) const table = createTable() diff --git a/packages/table-devtools/tests/useTableStore.test.tsx b/packages/table-devtools/tests/useTableStore.test.tsx new file mode 100644 index 0000000000..6c9269d7ae --- /dev/null +++ b/packages/table-devtools/tests/useTableStore.test.tsx @@ -0,0 +1,113 @@ +import { createRoot } from 'solid-js' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useTableStore } from '../src/useTableStore' +import type { Readable } from '@tanstack/solid-store' + +describe('useTableStore scheduling', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.runOnlyPendingTimers() + vi.unstubAllGlobals() + vi.useRealTimers() + }) + + it('coalesces snapshots and applies only the latest value', () => { + let listener: ((snapshot: number) => void) | undefined + const unsubscribe = vi.fn() + const store = { + get: () => 0, + subscribe: (nextListener) => { + listener = + typeof nextListener === 'function' ? nextListener : nextListener.next + return { unsubscribe } + }, + } satisfies Readable + + let dispose = () => {} + const value = createRoot((disposeRoot) => { + dispose = disposeRoot + return useTableStore(() => store) + }) + + expect(value()).toBe(0) + + listener?.(1) + listener?.(2) + listener?.(3) + + expect(value()).toBe(0) + + vi.runAllTimers() + + expect(value()).toBe(3) + + dispose() + expect(unsubscribe).toHaveBeenCalledTimes(1) + }) + + it('cancels a queued update when its owner is disposed', () => { + let listener: ((snapshot: number) => void) | undefined + const store = { + get: () => 0, + subscribe: (nextListener) => { + listener = + typeof nextListener === 'function' ? nextListener : nextListener.next + return { unsubscribe: vi.fn() } + }, + } satisfies Readable + + let dispose = () => {} + const value = createRoot((disposeRoot) => { + dispose = disposeRoot + return useTableStore(() => store) + }) + + listener?.(1) + dispose() + vi.runAllTimers() + + expect(value()).toBe(0) + }) + + it('uses requestIdleCallback when the browser provides it', () => { + const requestIdleCallback = vi + .fn<(callback: IdleRequestCallback) => number>() + .mockImplementation((callback) => { + callback({ + didTimeout: false, + timeRemaining: () => 10, + }) + return 1 + }) + + vi.stubGlobal('requestIdleCallback', requestIdleCallback) + vi.stubGlobal('cancelIdleCallback', vi.fn()) + + let listener: ((snapshot: number) => void) | undefined + const store = { + get: () => 0, + subscribe: (nextListener) => { + listener = + typeof nextListener === 'function' ? nextListener : nextListener.next + return { unsubscribe: vi.fn() } + }, + } satisfies Readable + + let dispose = () => {} + const value = createRoot((disposeRoot) => { + dispose = disposeRoot + return useTableStore(() => store) + }) + + listener?.(1) + vi.advanceTimersByTime(32) + + expect(requestIdleCallback).toHaveBeenCalledTimes(1) + expect(value()).toBe(1) + + dispose() + }) +}) diff --git a/packages/vue-table-devtools/src/VueTableDevtools.tsx b/packages/vue-table-devtools/src/VueTableDevtools.tsx index ec9ba617d7..564229333e 100644 --- a/packages/vue-table-devtools/src/VueTableDevtools.tsx +++ b/packages/vue-table-devtools/src/VueTableDevtools.tsx @@ -36,16 +36,17 @@ function createPanelWrapper( name, props: ['theme', 'devtoolsOpen'], setup(props: TableDevtoolsVueInit) { - const devtoolsProps = { - theme: props.theme ?? 'dark', - devtoolsOpen: props.devtoolsOpen ?? false, - } - - return () => - h(Component, { + return () => { + const devtoolsProps = { + theme: props.theme ?? 'dark', + devtoolsOpen: props.devtoolsOpen ?? false, + } + return h(Component, { + key: `${devtoolsProps.theme}:${devtoolsProps.devtoolsOpen}`, props, devtoolsProps, }) + } }, }) as DefineComponent } diff --git a/packages/vue-table-devtools/src/production.ts b/packages/vue-table-devtools/src/production.ts index 5823644b5a..88724a9295 100644 --- a/packages/vue-table-devtools/src/production.ts +++ b/packages/vue-table-devtools/src/production.ts @@ -1,5 +1,5 @@ -export { TableDevtoolsPanel } from './VueTableDevtools' +export { TableDevtoolsPanel } from './production/VueTableDevtools' export type { TableDevtoolsVueInit } from './VueTableDevtools' -export { tableDevtoolsPlugin } from './plugin' +export { tableDevtoolsPlugin } from './production/plugin' export { useTanStackTableDevtools } from './useTanStackTableDevtools' export type { UseTanStackTableDevtoolsOptions } from './useTanStackTableDevtools' diff --git a/packages/vue-table-devtools/src/production/VueTableDevtools.tsx b/packages/vue-table-devtools/src/production/VueTableDevtools.tsx new file mode 100644 index 0000000000..d9ad422562 --- /dev/null +++ b/packages/vue-table-devtools/src/production/VueTableDevtools.tsx @@ -0,0 +1,46 @@ +import { createVuePanel } from '@tanstack/devtools-utils/vue' +import { TableDevtoolsCore } from '@tanstack/table-devtools/production' +import { defineComponent, h } from 'vue' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/vue' +import type { DefineComponent } from 'vue' +import type { TableDevtoolsVueInit } from '../VueTableDevtools' + +class TableDevtoolsVueCore { + private readonly core = new TableDevtoolsCore() + + constructor(_props: DevtoolsPanelProps) {} + + mount(el: HTMLElement, props?: DevtoolsPanelProps) { + void this.core.mount(el, { + theme: props?.theme ?? 'dark', + devtoolsOpen: props?.devtoolsOpen ?? false, + }) + } + + unmount() { + this.core.unmount() + } +} + +const [TableDevtoolsPanelBase] = createVuePanel< + DevtoolsPanelProps, + TableDevtoolsVueCore +>(TableDevtoolsVueCore) + +export const TableDevtoolsPanel = defineComponent({ + name: 'TableDevtoolsPanel', + props: ['theme', 'devtoolsOpen'], + setup(props: TableDevtoolsVueInit) { + return () => { + const devtoolsProps = { + theme: props.theme ?? 'dark', + devtoolsOpen: props.devtoolsOpen ?? false, + } + return h(TableDevtoolsPanelBase, { + key: `${devtoolsProps.theme}:${devtoolsProps.devtoolsOpen}`, + props, + devtoolsProps, + }) + } + }, +}) as DefineComponent diff --git a/packages/vue-table-devtools/src/production/plugin.ts b/packages/vue-table-devtools/src/production/plugin.ts new file mode 100644 index 0000000000..c20a591a12 --- /dev/null +++ b/packages/vue-table-devtools/src/production/plugin.ts @@ -0,0 +1,9 @@ +import { createVuePlugin } from '@tanstack/devtools-utils/vue' +import { TableDevtoolsPanel } from './VueTableDevtools' + +const [tableDevtoolsPlugin] = createVuePlugin( + 'TanStack Table', + TableDevtoolsPanel, +) + +export { tableDevtoolsPlugin } diff --git a/packages/vue-table-devtools/src/useTanStackTableDevtools.ts b/packages/vue-table-devtools/src/useTanStackTableDevtools.ts index 23da5d8b59..feef50dbb0 100644 --- a/packages/vue-table-devtools/src/useTanStackTableDevtools.ts +++ b/packages/vue-table-devtools/src/useTanStackTableDevtools.ts @@ -1,5 +1,5 @@ -import { unref, watchEffect } from 'vue' -import { upsertTableDevtoolsTarget } from '@tanstack/table-devtools' +import { onScopeDispose, unref, watchEffect } from 'vue' +import { createTableDevtoolsRegistrationManager } from '@tanstack/table-devtools' import type { RowData, Table, TableFeatures } from '@tanstack/table-core' import type { MaybeRef } from 'vue' @@ -14,19 +14,14 @@ export function useTanStackTableDevtools< table: MaybeRef | undefined>, options?: MaybeRef, ): void { - watchEffect((onCleanup) => { + const registration = createTableDevtoolsRegistrationManager() + onScopeDispose(() => registration.dispose()) + + watchEffect(() => { const resolvedOptions = unref(options) const resolvedTable = unref(table) - if (!(resolvedOptions?.enabled ?? true) || !resolvedTable) { - return - } - - const cleanup = upsertTableDevtoolsTarget({ table: resolvedTable }) - - onCleanup(() => { - cleanup?.() - }) + registration.update(resolvedTable, resolvedOptions?.enabled ?? true) }) }