Moving templates to current.*

Since v6.4, every row, cell and the view model itself expose a current object whose values are plain and reactive. A template that reads them needs no <Subscribe> wrapper, no fromStore, and no $ prefix. This guide converts an existing store-based template to current.*.

Warning:
On v7 this migration is mandatory; the store API no longer exists — see Migrating to v7. The rest of this page describes 6.4+, where both APIs exist side by side, so you can convert templates before upgrading.
Information:
On 6.x nothing here is required. The store API ($tableAttrs, cell.attrs(), cell.props(), <Subscribe>) keeps working unchanged, and both views can be mixed in one template. Convert when it makes a template simpler.

Hand this to your AI assistant

This conversion is mechanical, which makes it a good job for a coding assistant. Every page of these docs is also published as plain Markdown. Copy this prompt into your assistant from your project’s root:

Convert this project's @humanspeak/svelte-headless-table templates from the
store API to current.*, staying on version 6.

1. Read https://table.svelte.page/docs/guides/moving-to-current.md in full
   before changing anything.
2. Keep the package on 6.x: the installed version must satisfy ^6.4.0. Do not
   upgrade to 7 in this change.
3. Apply the guide's "Step by step" section to every template: keep the view
   model in one variable, replace $tableAttrs / $headerRows / $rows /
   $pageRows with vm.current.*, unwrap every <Subscribe> and read
   x.current.attrs and x.current.props, and move actions to
   use:cell.current.props.<plugin>.
4. Change templates only. Leave column definitions, createRender calls,
   plugins and plugin state ($sortKeys, pageIndex.set(...)) exactly as they
   are: on 6.x they are still stores.
5. Work through the guide's "Checklist", then remove the Subscribe import
   where it is no longer used. Run the project's type check and tests and
   report which files you changed.
Convert this project's @humanspeak/svelte-headless-table templates from the
store API to current.*, staying on version 6.

1. Read https://table.svelte.page/docs/guides/moving-to-current.md in full
   before changing anything.
2. Keep the package on 6.x: the installed version must satisfy ^6.4.0. Do not
   upgrade to 7 in this change.
3. Apply the guide's "Step by step" section to every template: keep the view
   model in one variable, replace $tableAttrs / $headerRows / $rows /
   $pageRows with vm.current.*, unwrap every <Subscribe> and read
   x.current.attrs and x.current.props, and move actions to
   use:cell.current.props.<plugin>.
4. Change templates only. Leave column definitions, createRender calls,
   plugins and plugin state ($sortKeys, pageIndex.set(...)) exactly as they
   are: on 6.x they are still stores.
5. Work through the guide's "Checklist", then remove the Subscribe import
   where it is no longer used. Run the project's type check and tests and
   report which files you changed.

If your assistant cannot fetch URLs, paste the contents of moving-to-current.md after the prompt. When this is done and shipped, Migrating to v7 has the prompt for the next step.

What changes and what does not

BeforeAfter
const { headerRows, rows, tableAttrs, … } = table.createViewModel(columns)const vm = table.createViewModel(columns)
{...$tableAttrs} / {...$tableBodyAttrs}{...vm.current.tableAttrs} / {...vm.current.tableBodyAttrs}
{#each $headerRows as headerRow}{#each vm.current.headerRows as headerRow}
{#each $rows as row} / {#each $pageRows as row}{#each vm.current.rows as row} / {#each vm.current.pageRows as row}
<Subscribe attrs={cell.attrs()} let:attrs> … {...attrs}{...cell.current.attrs}
<Subscribe props={cell.props()} let:props> … props.sort.ordercell.current.props.sort.order
<Subscribe rowAttrs={row.attrs()} let:rowAttrs> … {...rowAttrs}{...row.current.attrs}
use:props.resizeuse:cell.current.props.resize
<Render of={cell.render()} />unchanged
pluginStates.sort.sortKeys and every other plugin storeunchanged on 6.x (store reads still work); on v7 read sortKeys.current
Column definitions, createRender, snippets, pluginsunchanged on 6.x; on v7 createRender takes a props getter instead of a store

current.props has exactly the shape props() had: one key per plugin name, with that plugin’s props inside. Only the way you read it changes.

Step by step

1. Keep the view model in one variable

Destructuring the stores is what forced $-prefixed reads at the top level of the script. Keep vm instead and pull out only what you still need as stores (usually plugin state):

<script>
    // before
    const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates } =
        table.createViewModel(columns)
    const { sortKeys } = pluginStates.sort

    // after
    const vm = table.createViewModel(columns)
    const { sortKeys } = vm.pluginStates.sort
</script>
<script>
    // before
    const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates } =
        table.createViewModel(columns)
    const { sortKeys } = pluginStates.sort

    // after
    const vm = table.createViewModel(columns)
    const { sortKeys } = vm.pluginStates.sort
</script>

2. Replace the table-level stores

<!-- before -->
<table {...$tableAttrs}>
    <tbody {...$tableBodyAttrs}>
        {#each $pageRows as row (row.id)}

<!-- after -->
<table {...vm.current.tableAttrs}>
    <tbody {...vm.current.tableBodyAttrs}>
        {#each vm.current.pageRows as row (row.id)}
<!-- before -->
<table {...$tableAttrs}>
    <tbody {...$tableBodyAttrs}>
        {#each $pageRows as row (row.id)}

<!-- after -->
<table {...vm.current.tableAttrs}>
    <tbody {...vm.current.tableBodyAttrs}>
        {#each vm.current.pageRows as row (row.id)}

Use vm.current.rows where you used $rows and vm.current.pageRows where you used $pageRows; the pagination and virtual-scroll plugins act on pageRows exactly as before.

3. Unwrap <Subscribe> and read current directly

<!-- before -->
<Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs>
    <tr {...rowAttrs}>
        {#each headerRow.cells as cell (cell.id)}
            <Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
                <th {...attrs} onclick={props.sort.toggle} class:sorted={props.sort.order}>
                    <Render of={cell.render()} />
                </th>
            </Subscribe>
        {/each}
    </tr>
</Subscribe>

<!-- after -->
<tr {...headerRow.current.attrs}>
    {#each headerRow.cells as cell (cell.id)}
        <th
            {...cell.current.attrs}
            onclick={cell.current.props.sort.toggle}
            class:sorted={cell.current.props.sort.order}
        >
            <Render of={cell.render()} />
        </th>
    {/each}
</tr>
<!-- before -->
<Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs>
    <tr {...rowAttrs}>
        {#each headerRow.cells as cell (cell.id)}
            <Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
                <th {...attrs} onclick={props.sort.toggle} class:sorted={props.sort.order}>
                    <Render of={cell.render()} />
                </th>
            </Subscribe>
        {/each}
    </tr>
</Subscribe>

<!-- after -->
<tr {...headerRow.current.attrs}>
    {#each headerRow.cells as cell (cell.id)}
        <th
            {...cell.current.attrs}
            onclick={cell.current.props.sort.toggle}
            class:sorted={cell.current.props.sort.order}
        >
            <Render of={cell.render()} />
        </th>
    {/each}
</tr>

Every let: name maps to the matching current property on the same object: attrs → x.current.attrs, props → x.current.props. Body rows are identical with row in place of headerRow and <td> in place of <th>.

4. Actions, class directives and conditionals

They all read through current.props the same way:

<th {...cell.current.attrs} use:cell.current.props.resize>
    {#if cell.current.props.filter?.render}
        <Render of={cell.current.props.filter.render} />
    {/if}
    {#if !cell.current.props.resize.disabled}
        <div class="resizer" use:cell.current.props.resize.drag></div>
    {/if}
</th>

<tr {...row.current.attrs} class:selected={row.current.props.select.selected}>
<th {...cell.current.attrs} use:cell.current.props.resize>
    {#if cell.current.props.filter?.render}
        <Render of={cell.current.props.filter.render} />
    {/if}
    {#if !cell.current.props.resize.disabled}
        <div class="resizer" use:cell.current.props.resize.drag></div>
    {/if}
</th>

<tr {...row.current.attrs} class:selected={row.current.props.select.selected}>

5. Delete the import

Remove Subscribe from the import list once nothing in the file uses it. Render stays.

Reading current outside the template

current values are also correct outside a template or effect; they simply do not track. That makes them safe in event handlers and plain functions:

const selectedNames = () =>
    vm.current.rows.filter((row) => row.current.props.select.selected).map((row) => row.original.name)
const selectedNames = () =>
    vm.current.rows.filter((row) => row.current.props.select.selected).map((row) => row.original.name)

Inside a $derived or $effect, the same reads track and re-run as state changes.

Things to know

  • Plugin state is still stores on 6.x. pluginStates.<plugin> (sortKeys, pageIndex, selectedDataIds, filterValue, …) is unchanged, so $-prefixed reads and pageIndex.set(…) keep working. Only the view-model and component read side gained current. On v7 plugin state moves to current too (pageIndex.current); see Migrating to v7.
  • Server rendering produces the same markup through current as through the stores. No change is needed for SvelteKit SSR.
  • Mixing is fine. A template can read vm.current.pageRows and still wrap one cell in <Subscribe>, or read $pageRows and use row.current.attrs. The two views are backed by the same derivation chain.
  • fromStore is no longer needed for table stores. If you adopted {@const attrs = fromStore(cell.attrs())} in the meantime, replace it with cell.current.attrs.
  • Custom cell components created with createRender(Component, props) receive whatever you pass; if you passed a store, keep passing it. current does not change how cell content is rendered.

Checklist

  1. const vm = table.createViewModel(columns); pull plugin stores from vm.pluginStates.
  2. $tableAttrs → vm.current.tableAttrs; same for tableHeadAttrs and tableBodyAttrs.
  3. $headerRows / $rows / $pageRows → vm.current.headerRows / rows / pageRows.
  4. Remove each <Subscribe … let:…>; read x.current.attrs and x.current.props.
  5. Actions: use:cell.current.props.<plugin>….
  6. Drop the Subscribe import. Leave Render, plugin state and column definitions alone.

See TableViewModel, BodyCell and the quick start for the resulting template in full.