diff --git a/examples/angular/row-selection/src/app/app.html b/examples/angular/row-selection/src/app/app.html
index a9b5f0461e..35f6a1104b 100644
--- a/examples/angular/row-selection/src/app/app.html
+++ b/examples/angular/row-selection/src/app/app.html
@@ -1,4 +1,8 @@
+
+ {{ enableRowSelection() ? 'Disable' : 'Enable' }} Row selection
+
+
diff --git a/examples/angular/row-selection/src/app/app.ts b/examples/angular/row-selection/src/app/app.ts
index 756ab56f20..727c8081ee 100644
--- a/examples/angular/row-selection/src/app/app.ts
+++ b/examples/angular/row-selection/src/app/app.ts
@@ -32,6 +32,7 @@ export class App {
private readonly rowSelection = signal({})
readonly globalFilter = signal('')
readonly data = signal(makeData(10_000))
+ readonly enableRowSelection = signal(true)
readonly columns = columnHelper.columns([
columnHelper.display({
@@ -93,7 +94,8 @@ export class App {
state: {
rowSelection: this.rowSelection(),
},
- enableRowSelection: true, // enable row selection for all rows
+
+ enableRowSelection: this.enableRowSelection(), // enable row selection for all rows
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
onRowSelectionChange: (updaterOrValue) => {
this.rowSelection.set(
@@ -136,4 +138,8 @@ export class App {
refreshData(): void {
this.data.set(makeData(10_000))
}
+
+ toggleEnableRowSelection() {
+ this.enableRowSelection.update((value) => !value)
+ }
}
diff --git a/examples/angular/row-selection/src/app/selection-column/selection-column.ts b/examples/angular/row-selection/src/app/selection-column/selection-column.ts
index 5be2238e13..a063cbef41 100644
--- a/examples/angular/row-selection/src/app/selection-column/selection-column.ts
+++ b/examples/angular/row-selection/src/app/selection-column/selection-column.ts
@@ -29,6 +29,7 @@ export class TableHeaderSelection {
`,
diff --git a/examples/solid/row-selection/package.json b/examples/solid/row-selection/package.json
index 82cbf8ce2b..8ad3256001 100644
--- a/examples/solid/row-selection/package.json
+++ b/examples/solid/row-selection/package.json
@@ -17,7 +17,9 @@
"vite-plugin-solid": "^2.11.10"
},
"dependencies": {
+ "@tanstack/solid-devtools": "^0.7.26",
"@tanstack/solid-table": "^9.0.0-alpha.10",
+ "@tanstack/solid-table-devtools": "workspace:*",
"solid-js": "^1.9.11"
}
}
diff --git a/examples/solid/row-selection/src/App.tsx b/examples/solid/row-selection/src/App.tsx
index 57991b86f0..93efd2841a 100644
--- a/examples/solid/row-selection/src/App.tsx
+++ b/examples/solid/row-selection/src/App.tsx
@@ -1,3 +1,9 @@
+import type {
+ Column,
+ ColumnDef,
+ SolidTable,
+ Table,
+} from '@tanstack/solid-table'
import {
columnFilteringFeature,
createFilteredRowModel,
@@ -11,15 +17,11 @@ import {
tableFeatures,
} from '@tanstack/solid-table'
import { For, Show, createEffect, createSignal } from 'solid-js'
-import { makeData } from './makeData'
-import type {
- Column,
- ColumnDef,
- SolidTable,
- Table,
-} from '@tanstack/solid-table'
import type { Person } from './makeData'
+import { makeData } from './makeData'
import './index.css'
+import { TanStackDevtools } from '@tanstack/solid-devtools'
+import { tableDevtoolsPlugin } from '@tanstack/solid-table-devtools'
export const _features = tableFeatures({
rowPaginationFeature,
@@ -31,6 +33,7 @@ export const _features = tableFeatures({
function App() {
const [data, setData] = createSignal(makeData(1_000))
const refreshData = () => setData(makeData(100_000)) // stress test
+ const [enableRowSelection, setEnableRowSelection] = createSignal(true)
// Create table first with a placeholder for columns
let table: SolidTable
@@ -51,16 +54,18 @@ function App() {
)
},
- cell: ({ row }) => (
-
-
-
- ),
+ cell: ({ row }) => {
+ return (
+
+
+
+ )
+ },
},
{
header: 'Name',
@@ -124,8 +129,9 @@ function App() {
},
columns,
getRowId: (row) => row.id,
- enableRowSelection: true, // enable row selection for all rows
- // enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
+ get enableRowSelection() {
+ return enableRowSelection()
+ },
debugTable: true,
})
@@ -140,6 +146,8 @@ function App() {
// >
// {(state) => (
+
+
({ globalFilter: state.globalFilter })}
@@ -325,6 +333,12 @@ function App() {
>
Log table.getSelectedRowModel().flatRows
+ setEnableRowSelection((prev) => !prev)}
+ >
+ {enableRowSelection() ? 'Disable' : 'Enable'} Row Selection
+
Row Selection State:
@@ -333,8 +347,6 @@ function App() {
- // )}
- //
)
}
diff --git a/examples/vue/row-selection/src/App.vue b/examples/vue/row-selection/src/App.vue
index 58501f58c8..791e73ec67 100644
--- a/examples/vue/row-selection/src/App.vue
+++ b/examples/vue/row-selection/src/App.vue
@@ -88,18 +88,26 @@ const columns = columnHelper.columns([
])
const data = ref(makeData(10))
+const enableRowSelection = ref(true)
const rerender = () => {
data.value = makeData(10)
}
+const toggleRowSelection = () => {
+ enableRowSelection.value = !enableRowSelection.value
+}
+
const table = useTable(
{
_features,
_rowModels: {},
data,
columns,
- enableRowSelection: true, //enable row selection for all rows
+ // enable row selection for all rows
+ get enableRowSelection() {
+ return enableRowSelection.value
+ },
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
},
(state) => ({ rowSelection: state.rowSelection }),
@@ -152,6 +160,9 @@ const table = useTable(
Rerender
+
+ {{ enableRowSelection ? 'Enable' : 'Disable' }} Row Selection
+
diff --git a/knip.json b/knip.json
index d3668b30bf..4c38f1ee81 100644
--- a/knip.json
+++ b/knip.json
@@ -1,9 +1,12 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"ignoreDependencies": ["@faker-js/faker"],
- "ignoreWorkspaces": ["examples/**", "packages/table-core/tests/**"],
+ "ignoreWorkspaces": ["examples/**"],
"ignore": ["**/*benchmark*", "**/benchmarks/**"],
"workspaces": {
+ "packages/table-core": {
+ "ignore": ["**/tests/**"]
+ },
"packages/match-sorter-utils": {
"ignoreDependencies": ["remove-accents"]
},
diff --git a/packages/angular-table/src/angularReactivityFeature.ts b/packages/angular-table/src/angularReactivityFeature.ts
deleted file mode 100644
index 76b3433310..0000000000
--- a/packages/angular-table/src/angularReactivityFeature.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-import { computed, signal } from '@angular/core'
-import { setReactivePropertiesOnObject } from './reactivityUtils'
-import type { Signal } from '@angular/core'
-import type {
- RowData,
- Table,
- TableFeature,
- TableFeatures,
-} from '@tanstack/table-core'
-
-declare module '@tanstack/table-core' {
- interface TableOptions_Plugins<
- TFeatures extends TableFeatures,
- TData extends RowData,
- > extends TableOptions_AngularReactivity {}
-
- interface Table_Plugins<
- TFeatures extends TableFeatures,
- TData extends RowData,
- > extends Table_AngularReactivity {}
-}
-
-/**
- * Predicate used to skip/ignore a property name when applying Angular reactivity.
- *
- * Returning `true` means the property should NOT be wrapped/made reactive.
- */
-type SkipPropertyFn = (property: string) => boolean
-
-/**
- * Fine-grained configuration for Angular reactivity.
- *
- * Each key controls whether prototype methods/getters on the corresponding TanStack Table
- * objects are wrapped with signal-aware access.
- *
- * - `true` enables wrapping using the default skip rules.
- * - `false` disables wrapping entirely for that object type.
- * - a function allows customizing the skip rules (see {@link SkipPropertyFn}).
- *
- * @example
- * ```ts
- * const table = injectTable(() => {
- * // ...table options,
- * reactivity: {
- * // fine-grained control over which table objects have reactive properties,
- * // and which properties are wrapped
- * header: true,
- * column: true,
- * row: true,
- * cell: true,
- * }
- * })
- * ```
- */
-export interface AngularReactivityFlags {
- /** Controls reactive wrapping for `Header` instances. */
- header: boolean | SkipPropertyFn
- /** Controls reactive wrapping for `Column` instances. */
- column: boolean | SkipPropertyFn
- /** Controls reactive wrapping for `Row` instances. */
- row: boolean | SkipPropertyFn
- /** Controls reactive wrapping for `Cell` instances. */
- cell: boolean | SkipPropertyFn
-}
-
-/**
- * Table option extension for Angular reactivity.
- *
- * Available on `createTable` options via module augmentation in this file.
- */
-interface TableOptions_AngularReactivity {
- /**
- * Enables/disables and configures Angular reactivity on table-related prototypes.
- *
- * If omitted, defaults are provided by the feature.
- */
- reactivity?: Partial
-}
-
-/**
- * Table API extension for Angular reactivity.
- *
- * Added to the table instance via module augmentation.
- */
-interface Table_AngularReactivity<
- TFeatures extends TableFeatures,
- TData extends RowData,
-> {
- /**
- * Returns a table signal that updates whenever the table state or options changes.
- */
- get: Signal>
- /**
- * Sets the reactive notifier that powers {@link get}.
- *
- * @internal Used by the Angular table adapter to connect its notifier to the core table.
- */
- setTableNotifier: (signal: Signal>) => void
-}
-
-/**
- * Type map describing what this feature adds to TanStack Table constructors.
- */
-interface AngularReactivityFeatureConstructors<
- TFeatures extends TableFeatures,
- TData extends RowData,
-> {
- TableOptions: TableOptions_AngularReactivity
- Table: Table_AngularReactivity
-}
-
-/**
- * Resolves the user-provided `reactivity.*` config to a skip predicate.
- *
- * - `false` is handled by callers (feature method returns early)
- * - `true` selects the default predicate
- * - a function overrides the default predicate
- */
-const getUserSkipPropertyFn = (
- value: undefined | null | boolean | SkipPropertyFn,
- defaultPropertyFn: SkipPropertyFn,
-) => {
- if (typeof value === 'boolean') {
- return defaultPropertyFn
- }
-
- return value ?? defaultPropertyFn
-}
-
-function constructAngularReactivityFeature<
- TFeatures extends TableFeatures,
- TData extends RowData,
->(): TableFeature> {
- return {
- getDefaultTableOptions(table) {
- return {
- reactivity: {
- header: true,
- column: true,
- row: true,
- cell: true,
- },
- }
- },
- constructTableAPIs: (table) => {
- const rootNotifier = signal | null>(null)
- table.setTableNotifier = (notifier) => rootNotifier.set(notifier)
- table.get = computed(() => rootNotifier()!(), { equal: () => false })
- setReactivePropertiesOnObject(table.get, table, {
- overridePrototype: false,
- skipProperty: skipBaseProperties,
- })
- },
-
- assignCellPrototype: (prototype, table) => {
- if (table.options.reactivity?.cell === false) {
- return
- }
- setReactivePropertiesOnObject(table.get, prototype, {
- skipProperty: getUserSkipPropertyFn(
- table.options.reactivity?.cell,
- skipBaseProperties,
- ),
- overridePrototype: true,
- })
- },
-
- assignColumnPrototype: (prototype, table) => {
- if (table.options.reactivity?.column === false) {
- return
- }
- setReactivePropertiesOnObject(table.get, prototype, {
- skipProperty: getUserSkipPropertyFn(
- table.options.reactivity?.cell,
- skipBaseProperties,
- ),
- overridePrototype: true,
- })
- },
-
- assignHeaderPrototype: (prototype, table) => {
- if (table.options.reactivity?.header === false) {
- return
- }
- setReactivePropertiesOnObject(table.get, prototype, {
- skipProperty: getUserSkipPropertyFn(
- table.options.reactivity?.cell,
- skipBaseProperties,
- ),
- overridePrototype: true,
- })
- },
-
- assignRowPrototype: (prototype, table) => {
- if (table.options.reactivity?.row === false) {
- return
- }
- setReactivePropertiesOnObject(table.get, prototype, {
- skipProperty: getUserSkipPropertyFn(
- table.options.reactivity?.cell,
- skipBaseProperties,
- ),
- overridePrototype: true,
- })
- },
- }
-}
-
-/**
- * Angular reactivity feature that add reactive signal supports in table core instance.
- * This is used internally by the Angular table adapter `injectTable`.
- *
- * @private
- */
-export const angularReactivityFeature = constructAngularReactivityFeature()
-
-/**
- * Default predicate used to skip base/non-reactive properties.
- */
-function skipBaseProperties(property: string): boolean {
- return (
- // equals `getContext`
- property === 'getContext' ||
- // start with `_`
- property[0] === '_' ||
- // doesn't start with `get`, but faster
- !(property[0] === 'g' && property[1] === 'e' && property[2] === 't') ||
- // ends with `Handler`
- property.endsWith('Handler')
- )
-}
diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts
index e51f5d5043..6147ee1b65 100644
--- a/packages/angular-table/src/index.ts
+++ b/packages/angular-table/src/index.ts
@@ -3,7 +3,6 @@ import { FlexRenderDirective } from './flexRender'
export * from '@tanstack/table-core'
-export * from './angularReactivityFeature'
export * from './flexRender'
export * from './injectTable'
export * from './flex-render/flexRenderComponent'
diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts
index c1cbc5d8fd..0f9b53a4c1 100644
--- a/packages/angular-table/src/injectTable.ts
+++ b/packages/angular-table/src/injectTable.ts
@@ -2,13 +2,17 @@ import {
Injector,
assertInInjectionContext,
computed,
+ effect,
inject,
+ signal,
untracked,
} from '@angular/core'
-import { constructTable } from '@tanstack/table-core'
+import {
+ constructReactivityFeature,
+ constructTable,
+} from '@tanstack/table-core'
import { injectStore } from '@tanstack/angular-store'
import { lazyInit } from './lazySignalInitializer'
-import { angularReactivityFeature } from './angularReactivityFeature'
import type {
RowData,
Table,
@@ -27,6 +31,10 @@ export type AngularTable<
* The selected state from the table store, based on the selector provided.
*/
readonly state: Signal>
+ /**
+ * A signal that returns the entire table instance. Will update on table/options change.
+ */
+ readonly value: Signal>
/**
* Subscribe to changes in the table store with a custom selector.
*/
@@ -103,8 +111,18 @@ export function injectTable<
): AngularTable {
assertInInjectionContext(injectTable)
const injector = inject(Injector)
+ const count = 0
return lazyInit(() => {
+ const stateNotifier = signal(0)
+
+ const angularReactivityFeature = constructReactivityFeature({
+ // optionsNotifier: () => stateNotifier(),
+ stateNotifier: () => {
+ return stateNotifier()
+ },
+ })
+
const resolvedOptions: TableOptions = {
...options(),
_features: {
@@ -113,14 +131,17 @@ export function injectTable<
},
} as TableOptions
- const table: AngularTable = constructTable(
- resolvedOptions,
- ) as AngularTable
+ const table = constructTable(resolvedOptions) as AngularTable<
+ TFeatures,
+ TData,
+ TSelected
+ >
const updatedOptions = computed>(() => {
const tableOptionsValue = options()
+ const currentOptions = table.latestOptions
const result: TableOptions = {
- ...table.options,
+ ...currentOptions,
...tableOptionsValue,
_features: {
...tableOptionsValue._features,
@@ -133,24 +154,34 @@ export function injectTable<
return result
})
- const tableState = injectStore(
- table.store,
- (state: TableState) => state,
+ effect(
+ () => {
+ const newOptions = updatedOptions()
+ untracked(() => table.setOptions(newOptions))
+ },
{ injector },
)
- const tableSignalNotifier = computed(
+ const tableState = injectStore(table.store, (state) => state, { injector })
+ const tableOptions = injectStore(table.optionsStore, (state) => state, {
+ injector,
+ })
+
+ let firstRun = true
+ effect(
() => {
+ tableOptions()
tableState()
- table.setOptions(updatedOptions())
- untracked(() => table.baseStore.setState((prev) => ({ ...prev })))
- return table
+ if (!firstRun) {
+ untracked(() => {
+ stateNotifier.update((n) => n + 1)
+ })
+ }
+ firstRun = false
},
- { equal: () => false },
+ { injector },
)
- table.setTableNotifier(tableSignalNotifier)
-
table.Subscribe = function Subscribe(props: {
selector: (state: TableState) => TSubSelected
equal?: ValueEqualityFn
@@ -160,11 +191,17 @@ export function injectTable<
equal: props.equal,
})
}
-
Object.defineProperty(table, 'state', {
value: injectStore(table.store, selector, { injector }),
})
+ Object.defineProperty(table, 'value', {
+ value: computed(() => {
+ stateNotifier()
+ return table
+ }),
+ })
+
return table
})
}
diff --git a/packages/angular-table/src/reactivityUtils.ts b/packages/angular-table/src/reactivityUtils.ts
deleted file mode 100644
index 1f64f7355c..0000000000
--- a/packages/angular-table/src/reactivityUtils.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-import { computed, isSignal } from '@angular/core'
-import { $internalMemoFnMeta, getMemoFnMeta } from '@tanstack/table-core'
-import type { MemoFnMeta } from '@tanstack/table-core'
-import type { Signal } from '@angular/core'
-
-const $TABLE_REACTIVE = Symbol('reactive')
-
-function markReactive(obj: T): void {
- Object.defineProperty(obj, $TABLE_REACTIVE, { value: true })
-}
-
-function isReactive(obj: T): boolean {
- return Reflect.get(obj as {}, $TABLE_REACTIVE) === true
-}
-
-/**
- * Defines a lazy computed property on an object. The property is initialized
- * with a getter that computes its value only when accessed for the first time.
- * After the first access, the computed value is cached, and the getter is
- * replaced with a direct property assignment for efficiency.
- *
- * @internal should be used only internally
- */
-export function defineLazyComputedProperty(
- notifier: Signal,
- setObjectOptions: {
- originalObject: T
- property: keyof T & string
- valueFn: (...args: any) => any
- overridePrototype?: boolean
- },
-) {
- const { originalObject, property, overridePrototype, valueFn } =
- setObjectOptions
-
- if (overridePrototype) {
- assignReactivePrototypeAPI(notifier, originalObject, property)
- } else {
- Object.defineProperty(originalObject, property, {
- enumerable: true,
- configurable: true,
- get() {
- const computedValue = toComputed(notifier, valueFn, property)
- markReactive(computedValue)
- // Once the property is set the first time, we don't need a getter anymore
- // since we have a computed / cached fn value
- Object.defineProperty(originalObject, property, {
- value: computedValue,
- configurable: true,
- enumerable: true,
- })
- return computedValue
- },
- })
- }
-}
-
-/**
- * @internal should be used only internally
- */
-type ComputedFunction = T extends () => infer TReturn
- ? Signal
- : // 1+ args
- T extends (arg0?: any, ...args: Array) => any
- ? T
- : never
-
-/**
- * @description Transform a function into a computed that react to given notifier re-computations
- *
- * Here we'll handle all type of accessors:
- * - 0 argument -> e.g. table.getCanNextPage())
- * - 0~1 arguments -> e.g. table.getIsSomeRowsPinned(position?)
- * - 1 required argument -> e.g. table.getColumn(columnId)
- * - 1+ argument -> e.g. table.getRow(id, searchAll?)
- *
- * Since we are not able to detect automatically the accessors parameters,
- * we'll wrap all accessors into a cached function wrapping a computed
- * that return it's value based on the given parameters
- *
- * @internal should be used only internally
- */
-export function toComputed<
- T,
- TReturn,
- TFunction extends (...args: any) => TReturn,
->(
- notifier: Signal,
- fn: TFunction,
- debugName: string,
-): ComputedFunction {
- const hasArgs = getFnArgsLength(fn) > 0
- if (!hasArgs) {
- const computedFn = computed(
- () => {
- void notifier()
- return fn()
- },
- { debugName },
- )
- Object.defineProperty(computedFn, 'name', { value: debugName })
- markReactive(computedFn)
- return computedFn as ComputedFunction
- }
-
- const computedFn: ((this: unknown, ...argsArray: Array) => unknown) & {
- _reactiveCache?: Record>
- } = function (this: unknown, ...argsArray: Array) {
- const cacheable =
- argsArray.length === 0 ||
- argsArray.every((arg) => {
- return (
- arg === null ||
- arg === undefined ||
- typeof arg === 'string' ||
- typeof arg === 'number' ||
- typeof arg === 'boolean' ||
- typeof arg === 'symbol'
- )
- })
- if (!cacheable) {
- return fn.apply(this, argsArray)
- }
- const serializedArgs = serializeArgs(...argsArray)
- if ((computedFn._reactiveCache ??= {})[serializedArgs]) {
- return computedFn._reactiveCache[serializedArgs]()
- }
- const computedSignal = computed(
- () => {
- void notifier()
- return fn.apply(this, argsArray)
- },
- { debugName },
- )
-
- computedFn._reactiveCache[serializedArgs] = computedSignal
-
- return computedSignal()
- }
-
- Object.defineProperty(computedFn, 'name', { value: debugName })
- markReactive(computedFn)
-
- return computedFn as ComputedFunction
-}
-
-function serializeArgs(...args: Array) {
- return JSON.stringify(args)
-}
-
-function getFnArgsLength(
- fn: ((...args: any) => any) & { originalArgsLength?: number },
-): number {
- return Math.max(0, getMemoFnMeta(fn)?.originalArgsLength ?? fn.length)
-}
-
-function assignReactivePrototypeAPI(
- notifier: Signal,
- prototype: Record,
- fnName: string,
-) {
- if (isReactive(prototype[fnName])) return
-
- const fn = prototype[fnName]
- const originalArgsLength = getFnArgsLength(fn)
-
- if (originalArgsLength <= 1) {
- Object.defineProperty(prototype, fnName, {
- enumerable: true,
- configurable: true,
- get(this) {
- const self = this
- // Create a cache in the current prototype to allow the signals
- // to be garbage collected. Shorthand for a WeakMap implementation
- self._reactiveCache ??= {}
- const cached = (self._reactiveCache[`${self.id}${fnName}`] ??= computed(
- () => {
- notifier()
- return fn.apply(self)
- },
- {},
- ))
- markReactive(cached)
- cached[$internalMemoFnMeta] = {
- originalArgsLength,
- } satisfies MemoFnMeta
- return cached
- },
- })
- } else {
- prototype[fnName] = function (this: unknown, ...args: Array) {
- notifier()
- return fn.apply(this, args)
- }
- markReactive(prototype[fnName])
- prototype[fnName][$internalMemoFnMeta] = {
- originalArgsLength,
- } satisfies MemoFnMeta
- }
-}
-
-export function setReactivePropertiesOnObject(
- notifier: Signal,
- obj: { [key: string]: any },
- options: {
- overridePrototype?: boolean
- skipProperty: (property: string) => boolean
- },
-) {
- const { skipProperty } = options
- if (isReactive(obj)) {
- return
- }
- markReactive(obj)
-
- for (const property in obj) {
- const value = obj[property]
- if (
- isSignal(value) ||
- typeof value !== 'function' ||
- skipProperty(property)
- ) {
- continue
- }
- defineLazyComputedProperty(notifier, {
- valueFn: value,
- property,
- originalObject: obj,
- overridePrototype: options.overridePrototype,
- })
- }
-}
diff --git a/packages/angular-table/tests/angularReactivityFeature.test.ts b/packages/angular-table/tests/angularReactivityFeature.test.ts
index f2de587be1..d6626b7915 100644
--- a/packages/angular-table/tests/angularReactivityFeature.test.ts
+++ b/packages/angular-table/tests/angularReactivityFeature.test.ts
@@ -44,7 +44,7 @@ describe('angularReactivityFeature', () => {
const table = createTestTable()
const tablePropertyKeys = Object.keys(table)
- describe('Table property reactivity', () => {
+ describe.skip('Table property reactivity', () => {
test.each(
tablePropertyKeys.map((property) => [
property,
@@ -74,7 +74,7 @@ describe('angularReactivityFeature', () => {
})
})
- describe('Header property reactivity', () => {
+ describe.skip('Header property reactivity', () => {
const headers = table.getHeaderGroups()
headers.forEach((headerGroup, index) => {
const headerPropertyKeys = Object.keys(headerGroup)
@@ -112,7 +112,7 @@ describe('angularReactivityFeature', () => {
})
})
- describe('Column property reactivity', () => {
+ describe.skip('Column property reactivity', () => {
const columns = table.getAllColumns()
columns.forEach((column, index) => {
const columnPropertyKeys = Object.keys(column).concat(
@@ -133,7 +133,7 @@ describe('angularReactivityFeature', () => {
})
})
- describe('Row and cells property reactivity', () => {
+ describe.skip('Row and cells property reactivity', () => {
const flatRows = table.getRowModel().flatRows
flatRows.forEach((row, index) => {
const rowsPropertyKeys = Object.keys(row).concat(
@@ -174,11 +174,12 @@ describe('angularReactivityFeature', () => {
})
describe('Integration', () => {
- test('methods works will be reactive effects', () => {
+ test('methods within effect will be re-trigger when options/state changes', () => {
const data = signal>([{ id: '1', title: 'Title' }])
const table = createTestTable(data)
const isSelectedRow1Captor = vi.fn<(val: boolean) => void>()
const cellGetValueCaptor = vi.fn<(val: unknown) => void>()
+ const cellGetValueMemoizedCaptor = vi.fn<(val: unknown) => void>()
const columnIsVisibleCaptor = vi.fn<(val: boolean) => void>()
// This will test a case where you put in the effect a single cell property method
@@ -188,6 +189,8 @@ describe('angularReactivityFeature', () => {
() => table.getRowModel().rows[0]!.getAllCells()[0]!,
)
+ const cellGetValue = computed(() => cell().getValue())
+
TestBed.runInInjectionContext(() => {
effect(() => {
isSelectedRow1Captor(cell().row.getIsSelected())
@@ -195,6 +198,9 @@ describe('angularReactivityFeature', () => {
effect(() => {
cellGetValueCaptor(cell().getValue())
})
+ effect(() => {
+ cellGetValueMemoizedCaptor(cellGetValue())
+ })
effect(() => {
columnIsVisibleCaptor(cell().column.getIsVisible())
})
@@ -202,6 +208,7 @@ describe('angularReactivityFeature', () => {
TestBed.tick()
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(1)
+ expect(cellGetValueMemoizedCaptor).toHaveBeenCalledTimes(1)
expect(cellGetValueCaptor).toHaveBeenCalledTimes(1)
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(1)
@@ -209,7 +216,7 @@ describe('angularReactivityFeature', () => {
TestBed.tick()
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(2)
expect(cellGetValueCaptor).toHaveBeenCalledTimes(1)
- expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(1)
+ expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(2)
data.set([{ id: '1', title: 'Title 3' }])
TestBed.tick()
@@ -217,17 +224,23 @@ describe('angularReactivityFeature', () => {
// the cell instance will be recreated
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3)
expect(cellGetValueCaptor).toHaveBeenCalledTimes(2)
- expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(2)
+ expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(3)
cell().column.toggleVisibility(false)
TestBed.tick()
- expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3)
+ expect(isSelectedRow1Captor).toHaveBeenCalledTimes(4)
expect(cellGetValueCaptor).toHaveBeenCalledTimes(2)
- expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(3)
+ expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(4)
- expect(isSelectedRow1Captor.mock.calls).toEqual([[false], [true], [true]])
+ expect(isSelectedRow1Captor.mock.calls).toEqual([
+ [false],
+ [true],
+ [true],
+ [true],
+ ])
expect(cellGetValueCaptor.mock.calls).toEqual([['1'], ['1']])
expect(columnIsVisibleCaptor.mock.calls).toEqual([
+ [true],
[true],
[true],
[false],
diff --git a/packages/angular-table/tests/flex-render/flex-render-table.test.ts b/packages/angular-table/tests/flex-render/flex-render-table.test.ts
index 4781734d6c..3312226c78 100644
--- a/packages/angular-table/tests/flex-render/flex-render-table.test.ts
+++ b/packages/angular-table/tests/flex-render/flex-render-table.test.ts
@@ -319,7 +319,7 @@ describe('FlexRenderDirective', () => {
callExpandRender()
return flexRenderComponent(ExpandCell, {
bindings: [
- inputBinding('expanded', row.getIsExpanded),
+ inputBinding('expanded', () => row.getIsExpanded()),
outputBinding('toggleExpand', () => row.toggleExpanded()),
],
})
diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts
index 1f74844c66..bd6e4d17e0 100644
--- a/packages/angular-table/tests/injectTable.test.ts
+++ b/packages/angular-table/tests/injectTable.test.ts
@@ -75,7 +75,7 @@ describe('injectTable', () => {
})
test('supports "Object.keys"', () => {
- const keys = Object.keys(table.get()).concat('state')
+ const keys = Object.keys(table.value()).concat('state', 'value')
expect(Object.keys(table)).toEqual(keys)
})
diff --git a/packages/angular-table/tests/reactivityUtils.test.ts b/packages/angular-table/tests/reactivityUtils.test.ts
deleted file mode 100644
index 5aff11bf23..0000000000
--- a/packages/angular-table/tests/reactivityUtils.test.ts
+++ /dev/null
@@ -1,210 +0,0 @@
-import { describe, expect, test, vi } from 'vitest'
-import { effect, isSignal, signal } from '@angular/core'
-import { TestBed } from '@angular/core/testing'
-import { defineLazyComputedProperty, toComputed } from '../src/reactivityUtils'
-
-describe('toComputed', () => {
- describe('args = 0', () => {
- test('creates a computed', () => {
- const notifier = signal(1)
-
- const result = toComputed(
- notifier,
- () => {
- return notifier() * 2
- },
- 'double',
- )
-
- expect(result.name).toEqual('double')
- expect(isSignal(result)).toEqual(true)
-
- TestBed.runInInjectionContext(() => {
- const mockFn = vi.fn()
-
- effect(() => {
- mockFn(result())
- })
-
- TestBed.tick()
- expect(mockFn).toHaveBeenLastCalledWith(2)
-
- notifier.set(3)
- TestBed.tick()
- expect(mockFn).toHaveBeenLastCalledWith(6)
-
- notifier.set(2)
- TestBed.tick()
- expect(mockFn).toHaveBeenLastCalledWith(4)
-
- expect(mockFn.mock.calls.length).toEqual(3)
- })
- })
- })
-
- describe('args >= 1', () => {
- test('creates a fn an explicit first argument and allows other args', () => {
- const notifier = signal(1)
-
- const fn1 = toComputed(
- notifier,
- (arg0: number, arg1: string, arg3?: number) => {
- return { arg0, arg1, arg3 }
- },
- '3args',
- )
- expect(fn1.length).toEqual(0)
-
- // currently full rest parameters is not supported
- const fn2 = toComputed(
- notifier,
- function myFn(...args: Array) {
- return args
- },
- '3args',
- )
- expect(fn2.length).toEqual(0)
- })
-
- test('reuse created computed when args are the same', () => {
- const notifier = signal(1)
-
- const invokeMock = vi.fn()
-
- const sum = toComputed(
- notifier,
- (arg0: number, arg1?: string) => {
- invokeMock(arg0)
- return notifier() + arg0
- },
- 'sum',
- )
-
- sum(1)
- sum(3)
- sum(2)
- sum(1)
- sum(1)
- sum(2)
- sum(3)
-
- expect(invokeMock).toHaveBeenCalledTimes(3)
- expect(invokeMock).toHaveBeenNthCalledWith(1, 1)
- expect(invokeMock).toHaveBeenNthCalledWith(2, 3)
- expect(invokeMock).toHaveBeenNthCalledWith(3, 2)
- })
-
- test('cached computed are reactive', () => {
- const invokeMock = vi.fn()
- const notifier = signal(1)
-
- const sum = toComputed(
- notifier,
- (arg0: number) => {
- invokeMock(arg0)
- return notifier() + arg0
- },
- 'sum',
- )
-
- TestBed.runInInjectionContext(() => {
- const mockSumBy3Fn = vi.fn()
- const mockSumBy2Fn = vi.fn()
-
- effect(() => {
- mockSumBy3Fn(sum(3))
- })
- effect(() => {
- mockSumBy2Fn(sum(2))
- })
-
- TestBed.flushEffects()
- expect(mockSumBy3Fn).toHaveBeenLastCalledWith(4)
- expect(mockSumBy2Fn).toHaveBeenLastCalledWith(3)
-
- notifier.set(2)
- TestBed.flushEffects()
- expect(mockSumBy3Fn).toHaveBeenLastCalledWith(5)
- expect(mockSumBy2Fn).toHaveBeenLastCalledWith(4)
-
- expect(mockSumBy3Fn.mock.calls.length).toEqual(2)
- expect(mockSumBy2Fn.mock.calls.length).toEqual(2)
- })
-
- for (let i = 0; i < 4; i++) {
- sum(3)
- sum(2)
- }
- // invoked every time notifier change
- expect(invokeMock).toHaveBeenCalledTimes(4)
- })
- })
-
- describe('args 0~1', () => {
- test('creates a fn an explicit first argument and allows other args', () => {
- const notifier = signal(1)
- const captor = vi.fn<(arg0?: number) => void>()
- const captor2 = vi.fn<(arg0?: number) => void>()
-
- const fn1 = toComputed(
- notifier,
- (arg0: number | undefined) => {
- if (arg0 === undefined) {
- return 5 * notifier()
- }
- return arg0 * notifier()
- },
- 'optionalArgs',
- )
-
- expect(isSignal(fn1)).toEqual(false)
-
- TestBed.runInInjectionContext(() => {
- effect(() => {
- captor(fn1(0))
- })
- effect(() => {
- captor2(fn1(1))
- })
- })
-
- TestBed.tick()
- notifier.set(2)
- TestBed.tick()
- notifier.set(3)
- expect(captor.mock.calls).toHaveLength(1)
- expect(captor2.mock.calls).toHaveLength(2)
- expect(captor2).toHaveBeenNthCalledWith(1, 1)
- expect(captor2).toHaveBeenNthCalledWith(2, 2)
- })
- })
-})
-
-describe('defineLazyComputedProperty', () => {
- test('define a computed property and cache the result after first access', () => {
- const notifier = signal(1)
- const originalObject = {} as any
- const mockValueFn = vi.fn(() => 2)
-
- defineLazyComputedProperty(notifier, {
- originalObject,
- property: 'computedProp',
- valueFn: mockValueFn,
- })
-
- let propDescriptor = Object.getOwnPropertyDescriptor(
- originalObject,
- 'computedProp',
- )
- expect(propDescriptor && !!propDescriptor.get).toEqual(true)
-
- originalObject.computedProp
-
- propDescriptor = Object.getOwnPropertyDescriptor(
- originalObject,
- 'computedProp',
- )
- expect(propDescriptor!.get).not.toBeDefined()
- expect(isSignal(propDescriptor!.value))
- })
-})
diff --git a/packages/react-table/src/useTable.ts b/packages/react-table/src/useTable.ts
index 60d0685b0b..4f1deb83a0 100644
--- a/packages/react-table/src/useTable.ts
+++ b/packages/react-table/src/useTable.ts
@@ -2,7 +2,7 @@
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { constructTable } from '@tanstack/table-core'
-import { useStore } from '@tanstack/react-store'
+import { shallow, useStore } from '@tanstack/react-store'
import { FlexRender } from './FlexRender'
import { Subscribe } from './Subscribe'
import type {
@@ -103,26 +103,15 @@ export function useTable<
return tableInstance
})
- // sync table options on every render
- table.setOptions((prev) => ({
- ...prev,
- ...tableOptions,
- }))
+ useEffect(() => {
+ // sync table options on every render
+ table.setOptions((prev) => ({
+ ...prev,
+ ...tableOptions,
+ }))
+ }, [table, tableOptions])
- useIsomorphicLayoutEffect(() => {
- // prevent race condition between table.setOptions and table.baseStore.setState
- queueMicrotask(() => {
- table.baseStore.setState((prev) => ({
- ...prev,
- }))
- })
- }, [
- table.options.columns, // re-render when columns change
- table.options.data, // re-render when data changes
- table.options.state, // sync react state to the table store
- ])
-
- const state = useStore(table.store, selector)
+ const state = useStore(table.store, selector, shallow)
return useMemo(
() => ({
diff --git a/packages/solid-table-devtools/eslint.config.js b/packages/solid-table-devtools/eslint.config.js
new file mode 100644
index 0000000000..892f5314df
--- /dev/null
+++ b/packages/solid-table-devtools/eslint.config.js
@@ -0,0 +1,8 @@
+// @ts-check
+
+import rootConfig from '../../eslint.config.js'
+
+/** @type {any} */
+const config = [...rootConfig]
+
+export default config
diff --git a/packages/solid-table-devtools/package.json b/packages/solid-table-devtools/package.json
new file mode 100644
index 0000000000..b8bfaf0765
--- /dev/null
+++ b/packages/solid-table-devtools/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "@tanstack/solid-table-devtools",
+ "version": "9.0.0-alpha.11",
+ "description": "Solid devtools for TanStack Table.",
+ "author": "Tanner Linsley",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/TanStack/table.git",
+ "directory": "packages/react-table-devtools"
+ },
+ "homepage": "https://tanstack.com/table",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "keywords": [
+ "solid",
+ "tanstack",
+ "table",
+ "devtools"
+ ],
+ "scripts": {
+ "clean": "rimraf ./build && rimraf ./dist",
+ "test:eslint": "eslint ./src",
+ "test:lib": "vitest --passWithNoTests",
+ "test:lib:dev": "pnpm test:lib --watch",
+ "test:types": "tsc",
+ "test:build": "publint --strict",
+ "build": "vite build"
+ },
+ "type": "module",
+ "types": "dist/esm/index.d.ts",
+ "module": "dist/esm/index.js",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ }
+ },
+ "./production": {
+ "import": {
+ "types": "./dist/esm/production.d.ts",
+ "default": "./dist/esm/production.js"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "engines": {
+ "node": ">=16"
+ },
+ "files": [
+ "dist",
+ "src"
+ ],
+ "dependencies": {
+ "@tanstack/devtools-utils": "^0.3.0",
+ "@tanstack/table-core": "workspace:*",
+ "@tanstack/table-devtools": "workspace:*"
+ },
+ "peerDependencies": {
+ "solid-js": "^1.9.11"
+ },
+ "devDependencies": {
+ "vite-plugin-solid": "^2.11.10"
+ }
+}
diff --git a/packages/solid-table-devtools/src/TableDevtools.tsx b/packages/solid-table-devtools/src/TableDevtools.tsx
new file mode 100644
index 0000000000..29c1a28712
--- /dev/null
+++ b/packages/solid-table-devtools/src/TableDevtools.tsx
@@ -0,0 +1,25 @@
+import { createSolidPanel } from '@tanstack/devtools-utils/solid'
+import {
+ TableDevtoolsCore,
+ setTableDevtoolsTarget,
+} from '@tanstack/table-devtools'
+import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid'
+
+import type { RowData, Table, TableFeatures } from '@tanstack/table-core'
+
+export interface TableDevtoolsSolidInit<
+ TFeatures extends TableFeatures = TableFeatures,
+ TData extends RowData = RowData,
+> extends DevtoolsPanelProps {
+ table?: Table
+}
+
+const [TableDevtoolsPanelBase, TableDevtoolsPanelNoOp] =
+ createSolidPanel(TableDevtoolsCore)
+
+function TableDevtoolsPanel(props: TableDevtoolsSolidInit) {
+ setTableDevtoolsTarget(props.table)
+ return
+}
+
+export { TableDevtoolsPanel, TableDevtoolsPanelNoOp }
diff --git a/packages/solid-table-devtools/src/index.ts b/packages/solid-table-devtools/src/index.ts
new file mode 100644
index 0000000000..70cdd3bcf1
--- /dev/null
+++ b/packages/solid-table-devtools/src/index.ts
@@ -0,0 +1,14 @@
+import { isDev } from 'solid-js/web'
+import * as plugin from './plugin'
+import * as Devtools from './TableDevtools'
+
+export const TableDevtoolsPanel = !isDev
+ ? Devtools.TableDevtoolsPanelNoOp
+ : Devtools.TableDevtoolsPanel
+
+export const tableDevtoolsPlugin = !isDev
+ ? plugin.tableDevtoolsNoOpPlugin
+ : plugin.tableDevtoolsPlugin
+
+export type { TableDevtoolsSolidInit } from './TableDevtools'
+export type { TableDevtoolsPluginOptions } from './plugin'
diff --git a/packages/solid-table-devtools/src/plugin.tsx b/packages/solid-table-devtools/src/plugin.tsx
new file mode 100644
index 0000000000..07561b9b35
--- /dev/null
+++ b/packages/solid-table-devtools/src/plugin.tsx
@@ -0,0 +1,34 @@
+import { createSolidPlugin } from '@tanstack/devtools-utils/solid'
+import { TableDevtoolsPanel } from './TableDevtools'
+import type { RowData, Table, TableFeatures } from '@tanstack/table-core'
+
+export interface TableDevtoolsPluginOptions<
+ TFeatures extends TableFeatures = TableFeatures,
+ TData extends RowData = RowData,
+> {
+ table: Table
+}
+
+function tableDevtoolsPlugin<
+ TFeatures extends TableFeatures = TableFeatures,
+ TData extends RowData = RowData,
+>(options: TableDevtoolsPluginOptions) {
+ return {
+ name: 'TanStack Table',
+ render: (_el: HTMLElement, theme: 'light' | 'dark') => (
+
+ ),
+ }
+}
+
+function tableDevtoolsNoOpPlugin<
+ TFeatures extends TableFeatures = TableFeatures,
+ TData extends RowData = RowData,
+>(_options: TableDevtoolsPluginOptions) {
+ return {
+ name: 'TanStack Table',
+ render: (_el: HTMLElement, _theme: 'light' | 'dark') => <>>,
+ }
+}
+
+export { tableDevtoolsPlugin, tableDevtoolsNoOpPlugin }
diff --git a/packages/solid-table-devtools/src/production.ts b/packages/solid-table-devtools/src/production.ts
new file mode 100644
index 0000000000..f683bce6b7
--- /dev/null
+++ b/packages/solid-table-devtools/src/production.ts
@@ -0,0 +1,5 @@
+export { TableDevtoolsPanel } from './TableDevtools'
+
+export type { TableDevtoolsSolidInit } from './production/TableDevtools'
+
+export { tableDevtoolsPlugin } from './production/plugin'
diff --git a/packages/solid-table-devtools/src/production/TableDevtools.tsx b/packages/solid-table-devtools/src/production/TableDevtools.tsx
new file mode 100644
index 0000000000..f0a5ef6f4c
--- /dev/null
+++ b/packages/solid-table-devtools/src/production/TableDevtools.tsx
@@ -0,0 +1,10 @@
+import { createSolidPanel } from '@tanstack/devtools-utils/solid'
+import { TableDevtoolsCore } from '@tanstack/table-devtools/production'
+
+import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid'
+
+const [TableDevtoolsPanel] = createSolidPanel(TableDevtoolsCore)
+
+export interface TableDevtoolsSolidInit extends DevtoolsPanelProps {}
+
+export { TableDevtoolsPanel }
diff --git a/packages/solid-table-devtools/src/production/plugin.tsx b/packages/solid-table-devtools/src/production/plugin.tsx
new file mode 100644
index 0000000000..403eedabc3
--- /dev/null
+++ b/packages/solid-table-devtools/src/production/plugin.tsx
@@ -0,0 +1,9 @@
+import { createSolidPlugin } from '@tanstack/devtools-utils/solid'
+import { TableDevtoolsPanel } from './TableDevtools'
+
+const [tableDevtoolsPlugin] = createSolidPlugin({
+ name: 'TanStack Table',
+ Component: TableDevtoolsPanel,
+})
+
+export { tableDevtoolsPlugin }
diff --git a/packages/solid-table-devtools/tsconfig.build.json b/packages/solid-table-devtools/tsconfig.build.json
new file mode 100644
index 0000000000..93755fa253
--- /dev/null
+++ b/packages/solid-table-devtools/tsconfig.build.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "rootDir": "src",
+ "outDir": "dist",
+ "noEmit": false,
+ "declaration": true,
+ "sourceMap": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/solid-table-devtools/tsconfig.json b/packages/solid-table-devtools/tsconfig.json
new file mode 100644
index 0000000000..966bd728f5
--- /dev/null
+++ b/packages/solid-table-devtools/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js"
+ },
+ "include": ["src", "eslint.config.js", "vite.config.ts", "tests"]
+}
diff --git a/packages/solid-table-devtools/vite.config.ts b/packages/solid-table-devtools/vite.config.ts
new file mode 100644
index 0000000000..896c24e885
--- /dev/null
+++ b/packages/solid-table-devtools/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { tanstackViteConfig } from '@tanstack/vite-config'
+import solid from 'vite-plugin-solid'
+import packageJson from './package.json'
+
+const config = defineConfig({
+ plugins: [solid()],
+ test: {
+ name: packageJson.name,
+ dir: './',
+ watch: false,
+ environment: 'jsdom',
+ globals: true,
+ },
+})
+
+export default mergeConfig(
+ config,
+ tanstackViteConfig({
+ entry: ['./src/index.ts', './src/production.ts'],
+ srcDir: './src',
+ cjs: false,
+ }),
+)
diff --git a/packages/solid-table/package.json b/packages/solid-table/package.json
index 9638c02201..8162065bfa 100644
--- a/packages/solid-table/package.json
+++ b/packages/solid-table/package.json
@@ -52,7 +52,7 @@
"build": "tsc -p tsconfig.build.json"
},
"dependencies": {
- "@tanstack/solid-store": "^0.8.0",
+ "@tanstack/solid-store": "^0.9.1",
"@tanstack/table-core": "workspace:*"
},
"devDependencies": {
diff --git a/packages/solid-table/src/createTable.ts b/packages/solid-table/src/createTable.ts
index 0e91d32dab..b4a9658357 100644
--- a/packages/solid-table/src/createTable.ts
+++ b/packages/solid-table/src/createTable.ts
@@ -1,6 +1,10 @@
-import { constructTable } from '@tanstack/table-core'
+import {
+ constructReactivityFeature,
+ constructTable,
+} from '@tanstack/table-core'
import { useStore } from '@tanstack/solid-store'
-import { createComputed, createSignal } from 'solid-js'
+import { createComputed, createSignal, mergeProps } from 'solid-js'
+import type { Accessor, JSX } from 'solid-js'
import type {
NoInfer,
RowData,
@@ -9,7 +13,6 @@ import type {
TableOptions,
TableState,
} from '@tanstack/table-core'
-import type { Accessor, JSX } from 'solid-js'
export type SolidTable<
TFeatures extends TableFeatures,
@@ -54,39 +57,53 @@ export function createTable<
selector: (state: TableState) => TSelected = () =>
({}) as TSelected,
): SolidTable {
- const table = constructTable(tableOptions) as SolidTable<
+ const [notifier, setNotifier] = createSignal(void 0, { equals: false })
+ const [optionsNotifier, setOptionsNotifier] = createSignal(void 0, {
+ equals: false,
+ })
+
+ const solidReactivityFeature = constructReactivityFeature({
+ stateNotifier: () => notifier(),
+ optionsNotifier: () => optionsNotifier(),
+ })
+
+ const mergedOptions = mergeProps(tableOptions, {
+ _features: mergeProps(tableOptions._features, {
+ solidReactivityFeature,
+ }),
+ }) as any
+
+ const resolvedOptions = mergeProps(
+ {
+ mergeOptions: (
+ defaultOptions: TableOptions,
+ options: TableOptions,
+ ) => {
+ return mergeProps(defaultOptions, options)
+ },
+ },
+ mergedOptions,
+ ) as TableOptions
+
+ const table = constructTable(resolvedOptions) as SolidTable<
TFeatures,
TData,
TSelected
>
- /**
- * Temp force reactivity to all state changes on every table.get* method
- */
const allState = useStore(table.store, (state) => state)
- const [renderVersion, setRenderVersion] = createSignal(0)
+ const allOptions = useStore(table.optionsStore, (options) => options)
createComputed(() => {
- // Access storeState to create reactive dependency
- allState()
- // Increment version to invalidate cached get* methods
- setRenderVersion((v) => v + 1)
- // Update options when store changes
- // table.setOptions((prev) => {
- // return mergeProps(prev, tableOptions) as TableOptions
- // })
+ table.setOptions((prev) => {
+ return mergeProps(prev, mergedOptions) as TableOptions
+ })
})
- // Wrap all "get*" methods to make them reactive
- Object.keys(table).forEach((key) => {
- const value = (table as any)[key]
- if (typeof value === 'function' && key.startsWith('get')) {
- const originalMethod = value.bind(table)
- ;(table as any)[key] = (...args: Array) => {
- renderVersion()
- return originalMethod(...args)
- }
- }
+ createComputed(() => {
+ allState()
+ allOptions()
+ setNotifier(void 0)
})
table.Subscribe = function Subscribe(props: {
diff --git a/packages/solid-table/src/createTableHelper.ts b/packages/solid-table/src/createTableHelper.ts
index dfbe95ea1d..768899cf12 100644
--- a/packages/solid-table/src/createTableHelper.ts
+++ b/packages/solid-table/src/createTableHelper.ts
@@ -13,7 +13,7 @@ import type {
export type TableHelper<
TFeatures extends TableFeatures,
TData extends RowData = any,
-> = Omit, 'tableCreator'> & {
+> = Omit, 'tableCreator'> & {
createTable: (
tableOptions: Omit<
TableOptions,
@@ -27,9 +27,12 @@ export function createTableHelper<
TFeatures extends TableFeatures,
TData extends RowData = any,
>(
- tableHelperOptions: TableHelperOptions,
+ tableHelperOptions: TableHelperOptions,
): TableHelper {
- const tableHelper = constructTableHelper(createTable, tableHelperOptions)
+ const tableHelper = constructTableHelper(
+ createTable as any,
+ tableHelperOptions,
+ )
return {
...tableHelper,
createTable: (
diff --git a/packages/table-core/src/core/table/constructTable.ts b/packages/table-core/src/core/table/constructTable.ts
index 927593da06..35f441c451 100644
--- a/packages/table-core/src/core/table/constructTable.ts
+++ b/packages/table-core/src/core/table/constructTable.ts
@@ -42,10 +42,22 @@ export function constructTable<
return Object.assign(obj, feature.getDefaultTableOptions?.(table))
}, {}) as TableOptions
- table.options = {
+ table.latestOptions = {
...defaultOptions,
...tableOptions,
}
+ table.optionsStore = createStore(table.latestOptions)
+ Object.defineProperty(table, 'options', {
+ enumerable: true,
+ configurable: true,
+ get() {
+ return table.optionsStore.state
+ },
+ set(value) {
+ table.latestOptions = value
+ table.optionsStore.setState(() => value)
+ },
+ })
table.initialState = getInitialTableState(
table._features,
@@ -58,6 +70,8 @@ export function constructTable<
const state = table.baseStore.state
return {
...state,
+ // Merge state with non-reactive options.
+ // NOTE: Adapters should call `table.baseStore.setState` to trigger state updates
...(table.options.state ?? {}),
}
})
diff --git a/packages/table-core/src/core/table/coreTablesFeature.types.ts b/packages/table-core/src/core/table/coreTablesFeature.types.ts
index 0124eac018..5b692c740f 100644
--- a/packages/table-core/src/core/table/coreTablesFeature.types.ts
+++ b/packages/table-core/src/core/table/coreTablesFeature.types.ts
@@ -94,14 +94,22 @@ export interface Table_CoreProperties<
* The base store for the table. This can be used to write to the table state.
*/
baseStore: Store>
+ /**
+ * The base store for the table options.
+ */
+ optionsStore: Store>
/**
* This is the resolved initial state of the table.
*/
initialState: TableState
+ /**
+ * The latest table options value (non reactive).
+ */
+ latestOptions: TableOptions
/**
* A read-only reference to the table's current options.
*/
- options: TableOptions
+ readonly options: TableOptions
/**
* Where the table state is stored.
*/
diff --git a/packages/table-core/src/core/table/coreTablesFeature.utils.ts b/packages/table-core/src/core/table/coreTablesFeature.utils.ts
index 85c2cd7869..d897137513 100644
--- a/packages/table-core/src/core/table/coreTablesFeature.utils.ts
+++ b/packages/table-core/src/core/table/coreTablesFeature.utils.ts
@@ -18,12 +18,12 @@ export function table_mergeOptions<
table: Table_Internal,
newOptions: TableOptions,
) {
- if (table.options.mergeOptions) {
- return table.options.mergeOptions(table.options, newOptions)
+ if (table.latestOptions.mergeOptions) {
+ return table.latestOptions.mergeOptions(table.latestOptions, newOptions)
}
return {
- ...table.options,
+ ...table.latestOptions,
...newOptions,
}
}
@@ -35,6 +35,8 @@ export function table_setOptions<
table: Table_Internal,
updater: Updater>,
): void {
- const newOptions = functionalUpdate(updater, table.options)
- table.options = table_mergeOptions(table, newOptions)
+ const newOptions = functionalUpdate(updater, table.latestOptions)
+ const mergedOptions = table_mergeOptions(table, newOptions)
+ table.latestOptions = mergedOptions
+ table.optionsStore.setState(() => mergedOptions)
}
diff --git a/packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts b/packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts
new file mode 100644
index 0000000000..fc63235138
--- /dev/null
+++ b/packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts
@@ -0,0 +1,47 @@
+import type { TableFeature, TableFeatures } from '../../types/TableFeatures'
+import type { RowData } from '../../types/type-utils'
+import type { ReadonlyStore, Store } from '@tanstack/store'
+
+interface TableReactivityFeatureConstructors<
+ TFeatures extends TableFeatures,
+ TData extends RowData,
+> {}
+
+export function constructReactivityFeature<
+ TFeatures extends TableFeatures,
+ TData extends RowData,
+>(bindings: {
+ stateNotifier?: () => unknown
+ optionsNotifier?: () => unknown
+}): TableFeature> {
+ return {
+ constructTableAPIs: (table) => {
+ table.store = bindStore(table.store, bindings.stateNotifier)
+ table.optionsStore = bindStore(
+ table.optionsStore,
+ bindings.optionsNotifier,
+ )
+ },
+ }
+}
+
+const bindStore = | ReadonlyStore>(
+ store: T,
+ notifier?: () => unknown,
+): T => {
+ const stateDescriptor = Object.getOwnPropertyDescriptor(
+ Object.getPrototypeOf(store),
+ 'state',
+ )!
+
+ Object.defineProperty(store, 'state', {
+ configurable: true,
+ enumerable: true,
+ get() {
+ notifier?.()
+ return stateDescriptor.get!.call(store)
+ },
+ })
+
+ return store
+}
diff --git a/packages/table-core/src/index.ts b/packages/table-core/src/index.ts
index ffab1bfbf2..656947d3c4 100755
--- a/packages/table-core/src/index.ts
+++ b/packages/table-core/src/index.ts
@@ -78,6 +78,9 @@ export * from './fns/sortFns'
export * from './features/stockFeatures'
+// tableReactivityFeature
+export * from './features/table-reactivity/tableReactivityFeature'
+
// columnFacetingFeature
export * from './features/column-faceting/columnFacetingFeature'
export * from './features/column-faceting/columnFacetingFeature.types'
diff --git a/packages/table-core/src/utils.ts b/packages/table-core/src/utils.ts
index d8325b6f72..18877a09f3 100755
--- a/packages/table-core/src/utils.ts
+++ b/packages/table-core/src/utils.ts
@@ -16,7 +16,7 @@ export function makeStateUpdater<
K extends (string & {}) | keyof TableState_All | keyof TableState,
>(key: K, instance: Table) {
return (updater: Updater[K & keyof TableState]>) => {
- ;(instance as Table_Internal).baseStore.setState(
+ instance.baseStore.setState(
(old: TTableState) => {
return {
...old,
diff --git a/packages/table-devtools/package.json b/packages/table-devtools/package.json
index 61034c8897..083b7ead37 100644
--- a/packages/table-devtools/package.json
+++ b/packages/table-devtools/package.json
@@ -58,6 +58,7 @@
"dependencies": {
"@tanstack/devtools-ui": "^0.4.4",
"@tanstack/devtools-utils": "^0.3.0",
+ "@tanstack/solid-store": "^0.9.1",
"goober": "^2.1.18",
"solid-js": "^1.9.11"
},
diff --git a/packages/table-devtools/src/TableContextProvider.tsx b/packages/table-devtools/src/TableContextProvider.tsx
index 540c5e3484..8d043e2b9f 100644
--- a/packages/table-devtools/src/TableContextProvider.tsx
+++ b/packages/table-devtools/src/TableContextProvider.tsx
@@ -9,12 +9,11 @@ import {
getTableDevtoolsTarget,
subscribeTableDevtoolsTarget,
} from './tableTarget'
-
import type { Accessor, ParentComponent, Setter } from 'solid-js'
-import type { RowData, Table, TableFeatures } from '@tanstack/table-core'
+import type { RowData, Table, stockFeatures } from '@tanstack/table-core'
-type TableDevtoolsTabId = 'features' | 'state' | 'rows' | 'columns'
-type AnyTable = Table
+type TableDevtoolsTabId = 'features' | 'state' | 'options' | 'rows' | 'columns'
+type AnyTable = Table
interface TableDevtoolsContextValue {
table: Accessor
diff --git a/packages/table-devtools/src/components/FeaturesPanel.tsx b/packages/table-devtools/src/components/FeaturesPanel.tsx
index 06a5fb4f30..42954aa92a 100644
--- a/packages/table-devtools/src/components/FeaturesPanel.tsx
+++ b/packages/table-devtools/src/components/FeaturesPanel.tsx
@@ -188,15 +188,9 @@ export function FeaturesPanel() {
{(rowModelName) => {
const fns = getRowModelFunctions(rowModelName)
- const count = getRowCountForModel(
- tableInstance as { [key: string]: unknown },
- rowModelName,
- )
return (
-
- {rowModelName} ({count.toLocaleString()} rows)
-
+
{rowModelName}
{(fnName) => (
{fnName}
diff --git a/packages/table-devtools/src/components/OptionsPanel.tsx b/packages/table-devtools/src/components/OptionsPanel.tsx
new file mode 100644
index 0000000000..8bd669db21
--- /dev/null
+++ b/packages/table-devtools/src/components/OptionsPanel.tsx
@@ -0,0 +1,40 @@
+import { JsonTree } from '@tanstack/devtools-ui'
+import { useStore } from '@tanstack/solid-store'
+import { useTableDevtoolsContext } from '../TableContextProvider'
+import { useStyles } from '../styles/use-styles'
+import { ResizableSplit } from './ResizableSplit'
+
+export function OptionsPanel() {
+ const styles = useStyles()
+ const { table } = useTableDevtoolsContext()
+
+ const tableInstance = table()
+ const tableState = tableInstance
+ ? useStore(
+ tableInstance.optionsStore,
+ ({ state, data, _features, _rowModels, ...options }) => options,
+ )
+ : undefined
+
+ const getState = (): unknown => {
+ tableState?.()
+ if (!tableInstance) {
+ return undefined
+ }
+ return tableState?.()
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/table-devtools/src/components/Shell.tsx b/packages/table-devtools/src/components/Shell.tsx
index cdbb53bdfd..aab783da08 100644
--- a/packages/table-devtools/src/components/Shell.tsx
+++ b/packages/table-devtools/src/components/Shell.tsx
@@ -6,10 +6,12 @@ import { ColumnsPanel } from './ColumnsPanel'
import { FeaturesPanel } from './FeaturesPanel'
import { RowsPanel } from './RowsPanel'
import { StatePanel } from './StatePanel'
+import { OptionsPanel } from './OptionsPanel'
const tabs = [
{ id: 'features', label: 'Features' },
{ id: 'state', label: 'State' },
+ { id: 'options', label: 'Options' },
{ id: 'rows', label: 'Rows' },
{ id: 'columns', label: 'Columns' },
] as const
@@ -54,6 +56,9 @@ export function Shell() {
+
+
+
diff --git a/packages/vue-table/src/createTableHelper.ts b/packages/vue-table/src/createTableHelper.ts
index fad1c0bd77..57582886ef 100644
--- a/packages/vue-table/src/createTableHelper.ts
+++ b/packages/vue-table/src/createTableHelper.ts
@@ -12,7 +12,7 @@ import type {
export type TableHelper<
TFeatures extends TableFeatures,
TData extends RowData = any,
-> = Omit, 'tableCreator'> & {
+> = Omit, 'tableCreator'> & {
useTable: (
tableOptions: Omit<
TableOptionsWithReactiveData,
@@ -26,9 +26,9 @@ export function createTableHelper<
TFeatures extends TableFeatures,
TData extends RowData,
>(
- tableHelperOptions: TableHelperOptions,
+ tableHelperOptions: TableHelperOptions,
): TableHelper {
- const tableHelper = constructTableHelper(useTable, tableHelperOptions)
+ const tableHelper = constructTableHelper(useTable as any, tableHelperOptions)
return {
...tableHelper,
useTable: (
diff --git a/packages/vue-table/src/useTable.ts b/packages/vue-table/src/useTable.ts
index 27d6dc7fc2..8d6ef2bfdf 100644
--- a/packages/vue-table/src/useTable.ts
+++ b/packages/vue-table/src/useTable.ts
@@ -1,5 +1,8 @@
-import { isRef, unref, watch } from 'vue'
-import { constructTable } from '@tanstack/table-core'
+import { isRef, ref, unref, watch, watchEffect } from 'vue'
+import {
+ constructReactivityFeature,
+ constructTable,
+} from '@tanstack/table-core'
import { useStore } from '@tanstack/vue-store'
import { mergeProxy } from './merge-proxy'
import type {
@@ -74,76 +77,76 @@ export function useTable<
selector: (state: TableState) => TSelected = () =>
({}) as TSelected,
): VueTable {
+ const notifier = ref(0)
+
+ const vueReactivityFeature = constructReactivityFeature({
+ stateNotifier: () => notifier.value,
+ optionsNotifier: () => notifier.value,
+ })
+
const IS_REACTIVE = isRef(tableOptions.data)
- const statefulOptions = mergeProxy(
- IS_REACTIVE ? getOptionsWithReactiveData(tableOptions) : tableOptions,
+ const mergedOptions = {
+ ...tableOptions,
+ _features: {
+ ...tableOptions._features,
+ vueReactivityFeature,
+ },
+ }
+
+ const resolvedOptions = mergeProxy(
+ IS_REACTIVE
+ ? getOptionsWithReactiveData(
+ mergedOptions as TableOptions,
+ )
+ : mergedOptions,
{
// Remove state and onStateChange - store handles it internally
mergeOptions: (
defaultOptions: TableOptions,
newOptions: Partial>,
) => {
- return IS_REACTIVE
- ? {
- ...defaultOptions,
- ...newOptions,
- }
- : mergeProxy(defaultOptions, newOptions)
+ return mergeProxy(defaultOptions, newOptions)
},
},
) as TableOptions
- const table = constructTable(statefulOptions) as VueTable<
+ const table = constructTable(resolvedOptions) as VueTable<
TFeatures,
TData,
TSelected
>
- function updateOptions() {
- table.setOptions((prev) => {
- return mergeProxy(
- prev,
- IS_REACTIVE ? getOptionsWithReactiveData(tableOptions) : tableOptions,
- ) as TableOptions
- })
- }
-
- updateOptions()
-
- // Add reactivity support for reactive data
- if (IS_REACTIVE) {
- watch(
- () => tableOptions.data,
- () => {
- table.baseStore.setState((prev: TableState) => ({
- ...prev,
- data: unref(tableOptions.data),
- }))
- },
- { immediate: true },
- )
- }
-
- /**
- * Temp force reactivity to all state changes on every table.get* method
- */
const allState = useStore(table.store, (state) => state)
+ const allOptions = useStore(table.optionsStore, (state) => state)
- // Wrap all "get*" methods to make them reactive
- // Access allState.value directly to create reactive dependency
- Object.keys(table).forEach((key) => {
- const value = (table as any)[key]
- if (typeof value === 'function' && key.startsWith('get')) {
- const originalMethod = value.bind(table)
- ;(table as any)[key] = (...args: Array) => {
- // Access state to create reactive dependency
- allState.value
- return originalMethod(...args)
- }
- }
+ watchEffect(() => {
+ allState.value
+ allOptions.value
+ notifier.value++
})
+ watch(
+ () =>
+ [
+ IS_REACTIVE ? unref(tableOptions.data) : tableOptions.data,
+ tableOptions,
+ ] as const,
+ () => {
+ table.setOptions((prev) => {
+ return mergeProxy(
+ prev,
+ IS_REACTIVE
+ ? getOptionsWithReactiveData(
+ tableOptions as TableOptions,
+ )
+ : tableOptions,
+ ) as TableOptions
+ })
+ },
+ { immediate: true },
+ )
+
table.Subscribe = function Subscribe(props: {
selector: (state: TableState) => TSelected
children:
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5d8980058c..2dd23b92e5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2778,9 +2778,15 @@ importers:
examples/solid/row-selection:
dependencies:
+ '@tanstack/solid-devtools':
+ specifier: ^0.7.26
+ version: 0.7.26(csstype@3.2.3)(solid-js@1.9.11)
'@tanstack/solid-table':
specifier: ^9.0.0-alpha.10
version: link:../../../packages/solid-table
+ '@tanstack/solid-table-devtools':
+ specifier: workspace:*
+ version: link:../../../packages/solid-table-devtools
solid-js:
specifier: ^1.9.11
version: 1.9.11
@@ -3591,8 +3597,8 @@ importers:
packages/solid-table:
dependencies:
'@tanstack/solid-store':
- specifier: ^0.8.0
- version: 0.8.1(solid-js@1.9.11)
+ specifier: ^0.9.1
+ version: 0.9.1(solid-js@1.9.11)
'@tanstack/table-core':
specifier: workspace:*
version: link:../table-core
@@ -3604,6 +3610,25 @@ importers:
specifier: ^2.11.10
version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2))
+ packages/solid-table-devtools:
+ dependencies:
+ '@tanstack/devtools-utils':
+ specifier: ^0.3.0
+ version: 0.3.0(@types/react@19.2.10)(csstype@3.2.3)(preact@10.28.2)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.27(typescript@5.9.3))
+ '@tanstack/table-core':
+ specifier: workspace:*
+ version: link:../table-core
+ '@tanstack/table-devtools':
+ specifier: workspace:*
+ version: link:../table-devtools
+ solid-js:
+ specifier: ^1.9.11
+ version: 1.9.11
+ devDependencies:
+ vite-plugin-solid:
+ specifier: ^2.11.10
+ version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2))
+
packages/svelte-table:
dependencies:
'@tanstack/svelte-store':
@@ -3643,6 +3668,9 @@ importers:
'@tanstack/devtools-utils':
specifier: ^0.3.0
version: 0.3.0(@types/react@19.2.10)(csstype@3.2.3)(preact@10.28.2)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.27(typescript@5.9.3))
+ '@tanstack/solid-store':
+ specifier: ^0.9.1
+ version: 0.9.1(solid-js@1.9.11)
'@tanstack/table-core':
specifier: workspace:*
version: link:../table-core
@@ -7216,8 +7244,14 @@ packages:
resolution: {integrity: sha512-Lvq5VnH9Rtqci0urHENMMlyswN5fySvIANclS7cUq2xOr5Cc9QfPpwctZ3Bi1X+MibKIcXplTAixIYDFyYquQg==}
engines: {node: '>=12'}
- '@tanstack/solid-store@0.8.1':
- resolution: {integrity: sha512-1p4TTJGIZJ2J7130aTo7oWfHVRSCd9DxLP3HzcDMnzn56pz8krlyBEzsE+z/sHGXP0EC/JjT02fgj2L9+fmf8Q==}
+ '@tanstack/solid-devtools@0.7.26':
+ resolution: {integrity: sha512-HagP7imKP6I+GVaHZp3GvwKUZ4qTHkMmZVOK6+uFrlDDJNtKBEblMW1jyGsh0NKrbajTW2KNT0AlylRu5vsR/A==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ solid-js: '>=1.9.7'
+
+ '@tanstack/solid-store@0.9.1':
+ resolution: {integrity: sha512-gx7ToM+Yrkui36NIj0HjAufzv1Dg8usjtVFy5H3Ll52Xjuz+eliIJL+ihAr4LRuWh3nDPBR+nCLW0ShFrbE5yw==}
peerDependencies:
solid-js: ^1.6.0
@@ -15720,9 +15754,18 @@ snapshots:
- vite-plugin-solid
- webpack
- '@tanstack/solid-store@0.8.1(solid-js@1.9.11)':
+ '@tanstack/solid-devtools@0.7.26(csstype@3.2.3)(solid-js@1.9.11)':
dependencies:
- '@tanstack/store': 0.8.1
+ '@tanstack/devtools': 0.10.7(csstype@3.2.3)(solid-js@1.9.11)
+ solid-js: 1.9.11
+ transitivePeerDependencies:
+ - bufferutil
+ - csstype
+ - utf-8-validate
+
+ '@tanstack/solid-store@0.9.1(solid-js@1.9.11)':
+ dependencies:
+ '@tanstack/store': 0.9.1
solid-js: 1.9.11
'@tanstack/store@0.7.7': {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 5add109e67..773f661ccf 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,7 +1,8 @@
+packages:
+ - examples/**/*
+ - packages/*
+
cleanupUnusedCatalogs: true
+
linkWorkspacePackages: true
preferWorkspacePackages: true
-
-packages:
- - 'examples/**/*'
- - 'packages/*'