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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/framework/alpine/guide/custom-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ The `getInitialState` method in a table feature is responsible for setting the d

#### initTableInstanceData and resetTableInstanceData

Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the conflicting lifecycle guarantee across all framework guides.

The new text says initialization and construction are interleaved per feature, but the following constructTableAPIs description still says all feature-owned data has already been initialized. That can mislead feature authors into depending on later registrations.

  • docs/framework/alpine/guide/custom-features.md#L143-L143: Clarify the following paragraph to guarantee only the current and earlier features’ data.
  • docs/framework/angular/guide/custom-features.md#L149-L149: Replace the global initialization implication with the current-feature guarantee.
  • docs/framework/ember/guide/custom-features.md#L149-L149: State that later feature initialization has not occurred yet.
  • docs/framework/lit/guide/custom-features.md#L141-L141: Align the construct-hook paragraph with single-pass execution.
  • docs/framework/preact/guide/custom-features.md#L149-L149: Remove the obsolete all-features guarantee.
  • docs/framework/react/guide/custom-features.md#L149-L149: Document that only the current feature’s initialization is complete.
  • docs/framework/solid/guide/custom-features.md#L143-L143: Preserve the dependency boundary for later features.
  • docs/framework/svelte/guide/custom-features.md#L143-L143: Update the construct-hook wording to match interleaving.
  • docs/framework/vue/guide/custom-features.md#L143-L143: Replace “all feature-owned” with the precise current-feature guarantee.
📍 Affects 9 files
  • docs/framework/alpine/guide/custom-features.md#L143-L143 (this comment)
  • docs/framework/angular/guide/custom-features.md#L149-L149
  • docs/framework/ember/guide/custom-features.md#L149-L149
  • docs/framework/lit/guide/custom-features.md#L141-L141
  • docs/framework/preact/guide/custom-features.md#L149-L149
  • docs/framework/react/guide/custom-features.md#L149-L149
  • docs/framework/solid/guide/custom-features.md#L143-L143
  • docs/framework/svelte/guide/custom-features.md#L143-L143
  • docs/framework/vue/guide/custom-features.md#L143-L143
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/framework/alpine/guide/custom-features.md` at line 143, Correct the
constructTableAPIs lifecycle wording so it guarantees access only to data
initialized by the current and previously registered features, not all features.
Apply this clarification in
docs/framework/alpine/guide/custom-features.md:143-143,
docs/framework/angular/guide/custom-features.md:149-149,
docs/framework/ember/guide/custom-features.md:149-149,
docs/framework/lit/guide/custom-features.md:141-141,
docs/framework/preact/guide/custom-features.md:149-149,
docs/framework/react/guide/custom-features.md:149-149,
docs/framework/solid/guide/custom-features.md:143-143,
docs/framework/svelte/guide/custom-features.md:143-143, and
docs/framework/vue/guide/custom-features.md:143-143; explicitly state that later
feature initialization has not occurred and preserve the single-pass
registration-order execution model.


Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/angular/guide/custom-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ The `getInitialState` method in a table feature is responsible for setting the d

##### initTableInstanceData and resetTableInstanceData

Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.

Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/ember/guide/custom-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ The `getInitialState` method in a table feature is responsible for setting the d

##### initTableInstanceData and resetTableInstanceData

Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.

Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/lit/guide/custom-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ The `getInitialState` method in a table feature is responsible for setting the d

#### initTableInstanceData and resetTableInstanceData

Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.

Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/preact/guide/custom-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ The `getInitialState` method in a table feature is responsible for setting the d

##### initTableInstanceData and resetTableInstanceData

Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.

Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/react/guide/custom-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ The `getInitialState` method in a table feature is responsible for setting the d

##### initTableInstanceData and resetTableInstanceData

Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.

Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/solid/guide/custom-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ The `getInitialState` method in a table feature is responsible for setting the d

#### initTableInstanceData and resetTableInstanceData

Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.

Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/svelte/guide/custom-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ The `getInitialState` method in a table feature is responsible for setting the d

#### initTableInstanceData and resetTableInstanceData

Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.

Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/vue/guide/custom-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ The `getInitialState` method in a table feature is responsible for setting the d

#### initTableInstanceData and resetTableInstanceData

Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Every feature's initialization hook runs before any feature's `constructTableAPIs` hook.
Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier.

Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`.

Expand Down
23 changes: 13 additions & 10 deletions examples/lit/basic-subscribe/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,25 @@ class LitTableExample extends LitElement {

private tableController = new TableController<typeof features, Person>(this)

private table = this.tableController.table(
{
// Options are re-synced on every render pass (like the other examples), so
// reactive inputs such as `this._data` flow into the table in the same
// update that renders them.
private tableOptions() {
return {
features,
columns,
data: this._data,
getRowId: (row) => row.id,
getRowId: (row: Person) => row.id,
enableRowSelection: true,
atoms: {
rowSelection: rowSelectionAtom,
},
debugTable: true,
},
}
}

private table = this.tableController.table(
this.tableOptions(),
() => null, // subscribe to no table state by default; use table.subscribe below
)

Expand All @@ -141,12 +148,6 @@ class LitTableExample extends LitElement {
pagination: state.pagination,
})

protected updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('_data')) {
this.table.setOptions((prev) => ({ ...prev, data: this._data }))
}
}

private renderColumnFilter(
context: HeaderContext<typeof features, Person, unknown>,
) {
Expand Down Expand Up @@ -211,6 +212,8 @@ class LitTableExample extends LitElement {
}

protected render() {
this.table = this.tableController.table(this.tableOptions(), () => null)

return html`
<div class="demo-root">
<div>
Expand Down
34 changes: 34 additions & 0 deletions examples/preact/basic-external-state/tests/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,37 @@ test('renders the table without crashing', async ({ page }) => {
await server.close()
}
})

test('updates controlled pagination without console errors', async ({
page,
}) => {
const { errors, server } = await openExample(page)

try {
const table = getTable(page)
const nextPageButton = page.getByRole('button', {
name: '>',
exact: true,
})
const pageStatus = page.getByText(/^Page$/).locator('..')

await expect(getBodyRows(table).first()).toBeVisible()
await expect(pageStatus).toContainText('1 of 100')
const firstPageRow = await getFirstBodyRowData(table)

await nextPageButton.click()

await expect(pageStatus).toContainText('2 of 100')
await expect.poll(() => getFirstBodyRowData(table)).not.toBe(firstPageRow)

const secondPageRow = await getFirstBodyRowData(table)

await nextPageButton.click()

await expect(pageStatus).toContainText('3 of 100')
await expect.poll(() => getFirstBodyRowData(table)).not.toBe(secondPageRow)
expect(errors).toEqual([])
} finally {
await server.close()
}
})
4 changes: 3 additions & 1 deletion examples/react/aggregation/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,9 @@ function App() {
Showing {table.getRowModel().rows.length.toLocaleString()} of{' '}
{table.getRowCount().toLocaleString()} Rows
</div>
<pre>{JSON.stringify(table.state, null, 2)}</pre>
<pre data-testid="table-state">
{JSON.stringify(table.state, null, 2)}
</pre>
</div>
)
}
Expand Down
26 changes: 11 additions & 15 deletions examples/react/basic-external-atoms/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { TanStackDevtools } from '@tanstack/react-devtools'
import ReactDOM from 'react-dom/client'
import './index.css'
import { useCreateAtom, useSelector } from '@tanstack/react-store'
import { useCreateAtom } from '@tanstack/react-store'
import {
createColumnHelper,
createPaginatedRowModel,
Expand Down Expand Up @@ -79,14 +79,8 @@ function App() {
pageSize: 10,
})

// Subscribe to each atom independently — fine-grained reactivity.
const sorting = useSelector(sortingAtom)
const pagination = useSelector(paginationAtom)

console.log('sorting', sorting)
console.log('pagination', pagination)

// Create the table and pass your per-slice external atoms.
// Pass the per-slice external atoms directly. The React adapter subscribes
// through table.store, so no separate useSelector call is needed here.
const table = useTable(
{
key: 'basic-external-atoms', // needed for devtools
Expand Down Expand Up @@ -156,7 +150,7 @@ function App() {
<div className="controls">
<button
className="demo-button demo-button-sm"
onClick={() => table.setPageIndex(0)}
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
{'<<'}
Expand All @@ -177,15 +171,15 @@ function App() {
</button>
<button
className="demo-button demo-button-sm"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
{'>>'}
</button>
<span className="inline-controls">
<div>Page</div>
<strong>
{(table.atoms.pagination.get().pageIndex + 1).toLocaleString()} of{' '}
{(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '}
{table.getPageCount().toLocaleString()}
</strong>
</span>
Expand All @@ -195,7 +189,7 @@ function App() {
type="number"
min="1"
max={table.getPageCount()}
defaultValue={table.atoms.pagination.get().pageIndex + 1}
defaultValue={table.state.pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
table.setPageIndex(page)
Expand All @@ -204,7 +198,7 @@ function App() {
/>
</span>
<select
value={table.atoms.pagination.get().pageSize}
value={table.state.pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value))
}}
Expand All @@ -217,7 +211,9 @@ function App() {
</select>
</div>
<div className="spacer-md" />
<pre>{JSON.stringify(table.state, null, 2)}</pre>
<pre data-testid="table-state">
{JSON.stringify(table.state, null, 2)}
</pre>
</div>
)
}
Expand Down
27 changes: 27 additions & 0 deletions examples/react/basic-external-atoms/tests/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,30 @@ test('regenerates table data', async ({ page }) => {
await server.close()
}
})

test('updates from external atoms without an outer React subscription', async ({
page,
}) => {
const { errors, server } = await openExample(page)

try {
const table = page.locator('table').first()
const nextPageButton = page.getByRole('button', {
name: '>',
exact: true,
})
const pageStatus = page.getByText(/^Page$/).locator('..')

await expect(table.locator('tbody tr').first()).toBeVisible()
await expect(pageStatus).toContainText('1 of 100')
const firstPageRow = await getFirstBodyRowText(table)

await nextPageButton.click()

await expect(pageStatus).toContainText('2 of 100')
await expect.poll(() => getFirstBodyRowText(table)).not.toBe(firstPageRow)
expect(errors).toEqual([])
} finally {
await server.close()
}
})
8 changes: 5 additions & 3 deletions examples/react/basic-external-state/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ function App() {
<div className="controls">
<button
className="demo-button demo-button-sm"
onClick={() => table.setPageIndex(0)}
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
{'<<'}
Expand All @@ -175,7 +175,7 @@ function App() {
</button>
<button
className="demo-button demo-button-sm"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
{'>>'}
Expand Down Expand Up @@ -215,7 +215,9 @@ function App() {
</select>
</div>
<div className="spacer-md" />
<pre>{JSON.stringify(table.state, null, 2)}</pre>
<pre data-testid="table-state">
{JSON.stringify(table.state, null, 2)}
</pre>
</div>
)
}
Expand Down
37 changes: 37 additions & 0 deletions examples/react/basic-external-state/tests/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,40 @@ test('regenerates table data', async ({ page }) => {
await server.close()
}
})

test('updates controlled pagination without render-phase errors', async ({
page,
}) => {
const { errors, server } = await openExample(page)

try {
const table = page.locator('table').first()
const nextPageButton = page.getByRole('button', {
name: '>',
exact: true,
})
const pageStatus = page.getByText(/^Page$/).locator('..')

await expect(table.locator('tbody tr').first()).toBeVisible()
await expect(nextPageButton).toBeEnabled()
await expect(pageStatus).toContainText('1 of 100')

const firstPageRow = await getFirstBodyRowText(table)

await nextPageButton.click()

await expect(pageStatus).toContainText('2 of 100')
await expect.poll(() => getFirstBodyRowText(table)).not.toBe(firstPageRow)
expect(errors.join('\n')).not.toContain('Cannot update a component')

const secondPageRow = await getFirstBodyRowText(table)

await nextPageButton.click()

await expect(pageStatus).toContainText('3 of 100')
await expect.poll(() => getFirstBodyRowText(table)).not.toBe(secondPageRow)
expect(errors.join('\n')).not.toContain('Cannot update a component')
} finally {
await server.close()
}
})
Loading
Loading