Skip to content

Commit ef60ec5

Browse files
fix(svelte-table): set App* contexts in their own components (#6618)
Co-authored-by: Kevin Van Cott <kevinvandy656@gmail.com>
1 parent 87c6bcc commit ef60ec5

8 files changed

Lines changed: 98 additions & 11 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/svelte-table': patch
3+
---
4+
5+
fix(svelte-table): set the `AppTable`/`AppCell`/`AppHeader`/`AppFooter` contexts in their own components, so cells created later (e.g. when showing a column) no longer throw `set_context_after_init` in async mode

‎packages/svelte-table/src/AppCell.svelte‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts">
2+
import { setContext, untrack } from 'svelte'
3+
import { cellContextKey } from './context-keys'
24
import type { Snippet } from 'svelte'
35
46
interface Props {
@@ -8,6 +10,11 @@
810
}
911
1012
let { children, cell, cellComponents }: Props = $props()
13+
14+
setContext(
15+
cellContextKey,
16+
untrack(() => cell),
17+
)
1118
</script>
1219

1320
{@render children?.(Object.assign(cell, cellComponents))}

‎packages/svelte-table/src/AppHeader.svelte‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts">
2+
import { setContext, untrack } from 'svelte'
3+
import { headerContextKey } from './context-keys'
24
import type { Snippet } from 'svelte'
35
46
interface Props {
@@ -8,6 +10,11 @@
810
}
911
1012
let { children, header, headerComponents }: Props = $props()
13+
14+
setContext(
15+
headerContextKey,
16+
untrack(() => header),
17+
)
1118
</script>
1219

1320
{@render children?.(Object.assign(header, headerComponents))}
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
<script lang="ts">
2+
import { setContext, untrack } from 'svelte'
3+
import { tableContextKey } from './context-keys'
24
import type { Snippet } from 'svelte'
35
46
interface Props {
7+
table: any
58
children: Snippet
69
}
710
8-
let { children }: Props = $props()
11+
let { children, table }: Props = $props()
12+
13+
setContext(
14+
tableContextKey,
15+
untrack(() => table),
16+
)
917
</script>
1018

1119
{@render children?.()}

‎packages/svelte-table/src/createTableHook.svelte.ts‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getContext, setContext } from 'svelte'
1+
import { getContext } from 'svelte'
22
import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core'
33
import { createTable } from './createTable.svelte'
44
import { mergeObjects } from './merge-objects'
@@ -647,17 +647,14 @@ export function createTableHook<
647647
}
648648

649649
// Create wrapper components using the svelte-form (internal, props) => pattern.
650-
// setContext is called in the closure — this runs during component
651-
// initialization, so Svelte's context API works correctly.
652-
// With keyed {#each} blocks, components are recreated on reorder,
653-
// so context is always fresh.
650+
// They only add props: each App*Svelte component sets its own context while
651+
// it initializes. Calling setContext here would run before that component
652+
// exists and write into the context of the component rendering it.
654653
const AppTable = ((internal: any, props: any) => {
655-
setContext(tableContextKey, table)
656-
return AppTableSvelte(internal, { ...props })
654+
return AppTableSvelte(internal, { ...props, table })
657655
}) as Component<{ children: Snippet }>
658656

659657
const AppCell = ((internal: any, { children, cell }: any) => {
660-
setContext(cellContextKey, cell)
661658
return AppCellSvelte(internal, {
662659
cell,
663660
cellComponents: cellComponentsWithFlexRender,
@@ -669,7 +666,6 @@ export function createTableHook<
669666
}>
670667

671668
const AppHeader = ((internal: any, { children, header }: any) => {
672-
setContext(headerContextKey, header)
673669
return AppHeaderSvelte(internal, {
674670
header,
675671
headerComponents: headerComponentsWithFlexRender,
@@ -682,7 +678,6 @@ export function createTableHook<
682678

683679
// AppFooter reuses AppHeaderSvelte (footers use Header type in table-core)
684680
const AppFooter = ((internal: any, { children, header }: any) => {
685-
setContext(headerContextKey, header)
686681
return AppHeaderSvelte(internal, {
687682
header,
688683
headerComponents: headerComponentsWithFlexRender,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
import { getContext } from 'svelte'
3+
import {
4+
cellContextKey,
5+
headerContextKey,
6+
tableContextKey,
7+
} from '../../src/context-keys'
8+
9+
const visible = [tableContextKey, cellContextKey, headerContextKey].filter(
10+
(key) => getContext(key) !== undefined,
11+
)
12+
</script>
13+
14+
<output aria-label="Contexts outside the wrappers">{visible.join(',')}</output>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script lang="ts">
2+
import HookContextProbe from './HookContextProbe.svelte'
3+
import { hook } from './hook-fixture'
4+
import type { HookData } from './hook-fixture'
5+
6+
const columnHelper = hook.createAppColumnHelper<HookData>()
7+
const table = hook.createAppTable({
8+
data: [{ id: '1', title: 'First' }],
9+
columns: columnHelper.columns([
10+
columnHelper.accessor('title', { header: 'Title', footer: 'Title' }),
11+
]),
12+
})
13+
const cell = $derived(table.getRowModel().rows[0]!.getAllCells()[0]!)
14+
const header = $derived(table.getHeaderGroups()[0]!.headers[0]!)
15+
const footer = $derived(table.getFooterGroups()[0]!.headers[0]!)
16+
</script>
17+
18+
<table.AppTable>
19+
<table.TableBadge />
20+
</table.AppTable>
21+
22+
<table.AppCell {cell}>
23+
{#snippet children(value)}
24+
<value.CellBadge />
25+
{/snippet}
26+
</table.AppCell>
27+
28+
<table.AppHeader {header}>
29+
{#snippet children(value)}
30+
<value.HeaderBadge />
31+
{/snippet}
32+
</table.AppHeader>
33+
34+
<table.AppFooter header={footer}>
35+
{#snippet children(value)}
36+
<value.HeaderBadge />
37+
{/snippet}
38+
</table.AppFooter>
39+
40+
<HookContextProbe />

‎packages/svelte-table/tests/rendering.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { stockFeatures } from '@tanstack/table-core'
66
import { renderComponent } from '../src/render-component'
77
import ContextFailure from './fixtures/ContextFailure.svelte'
88
import FlexRenderHarness from './fixtures/FlexRenderHarness.svelte'
9+
import HookContextScope from './fixtures/HookContextScope.svelte'
910
import HookHarness from './fixtures/HookHarness.svelte'
1011
import RenderBadge from './fixtures/RenderBadge.svelte'
1112
import { hook } from './fixtures/hook-fixture'
@@ -209,6 +210,16 @@ describe('createTableHook', () => {
209210
expect(outputText('Hook footer')).toBe('footer:title')
210211
})
211212

213+
// https://github.com/TanStack/table/issues/6614
214+
test('wrapper contexts stay inside the wrappers', () => {
215+
render(HookContextScope)
216+
217+
expect(screen.getByText('table-component:row-1')).toBeTruthy()
218+
expect(screen.getByText('cell-component:First')).toBeTruthy()
219+
expect(screen.getAllByText('header-component:title')).toHaveLength(2)
220+
expect(outputText('Contexts outside the wrappers')).toBe('')
221+
})
222+
212223
test.each([
213224
['useTableContext', () => hook.useTableContext()],
214225
['useCellContext', () => hook.useCellContext()],

0 commit comments

Comments
 (0)