Skip to content

State Management

The table keeps all of its state itself. You only need to take over a slice of it when you want to read it, change it from outside, or persist it.

Inside a template, a $derived or an $effect, read a TanStack slice from table.atoms:

<script lang="ts">
const table = createShadcnSvelteTable({ columns, data });
const sorting = $derived(table.atoms.sorting.get());
const pageIndex = $derived(table.atoms.pagination.get().pageIndex);
</script>
<p>Page {pageIndex + 1}, sorted by {sorting.map((s) => s.id).join(', ') || 'nothing'}</p>
<ShadcnSvelteTable {table} />

State that MRT adds (density, full screen, the row being edited, …) is on table.mrtState. Both are reactive.

To own a slice, pass it in state as a getter and handle its on…Change option. The change handler receives either a value or an updater function:

<script lang="ts">
let sorting = $state<MRT_SortingState>([]);
</script>
<ShadcnSvelteTable
options={{
columns,
data,
onSortingChange: (updater) => {
sorting = typeof updater === 'function' ? updater(sorting) : updater;
},
state: {
get sorting() {
return sorting;
},
},
}}
/>

This works for every state key: columnFilters, globalFilter, pagination, rowSelection, expanded, grouping, columnOrder, columnPinning, columnVisibility, density, isFullScreen, isLoading, and more. The handler is named on<Key>Change, for example onDensityChange.

initialState sets the starting value without taking control:

options={{
columns,
data,
initialState: {
density: 'compact',
showGlobalFilter: true,
sorting: [{ id: 'lastName', desc: false }],
pagination: { pageIndex: 0, pageSize: 25 },
},
}}

isLoading shows skeleton rows and an overlay, showProgressBars shows only the progress bar, showSkeletons only the skeletons, and isSaving disables the save buttons. These are plain state keys, so pass them as getters.

state: {
get isLoading() {
return isLoading;
},
}

Setting one to an explicit false disables that indicator even while isLoading is true.