State Persistence Example
A remote category filter and the column widths both survive a reload. The model action opts into persistence with persistence: true; the column-width adapter saves resize overrides by field name. Both restore on mount through DataTableStatePersistence.
APIs used
Section titled “APIs used”DataTableStatePersistence— mounts the adapters under one storage keymodelStatePersistenceAdapter()— persists model action statecolumnWidthPersistenceAdapter()— persists column width overridespersistence: true— opt-in flag on a model action
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader, HeaderEdge } from '@/components/ui/data-table'
import { ResizeHandle } from '@/components/ui/data-table/column-resize'
import { columnWidthPersistenceAdapter } from '@virtuoso.dev/data-table/column-resize'
import { defaultOffsetViewportHandler, remoteModel } from '@virtuoso.dev/data-table'
import { DataTableStatePersistence, modelStatePersistenceAdapter } from '@virtuoso.dev/data-table/state-persistence'
import { fetchProducts, placeholder } from './api'
import type { Product, Query } from './api'
const adapters = [modelStatePersistenceAdapter(), columnWidthPersistenceAdapter()]
export default function App() {
const [model] = useState(() =>
remoteModel<Product, Query>({
actions: {
filter: {
persistence: true,
handler: ({ params, payload }) => ({ ...params, category: String(payload) }),
},
},
fetch: fetchProducts,
initialParams: { category: 'all' },
onViewportChange: defaultOffsetViewportHandler,
pageSize: 40,
placeholder,
})
)
function applyCategory(nextCategory: string) {
model.send({ action: 'filter', payload: nextCategory })
}
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{['all', 'Office', 'Peripherals', 'Audio'].map((value) => (
<Button key={value} onClick={() => applyCategory(value)} variant="outline">
{value}
</Button>
))}
</div>
<DataTable className="rounded-xl" model={model} style={{ height: 420 }}>
<DataTableStatePersistence adapters={adapters} storageKey="docs-state-persistence-example" />
<DataTableColumn field="name">
<DataTableColumnHeader>
<HeaderEdge component={ResizeHandle} />
{() => 'Product'}
</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ row }) => row.data.name}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="category">
<DataTableColumnHeader>
<HeaderEdge component={ResizeHandle} />
{() => 'Category'}
</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="price">
<DataTableColumnHeader className="justify-end">
<HeaderEdge component={ResizeHandle} />
{() => 'Price'}
</DataTableColumnHeader>
<DataTableCell className="text-right tabular-nums">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
</DataTable>
</div>
)
}Restore happens after columns and the model register with the engine, so a brief unpersisted render is normal on mount. Keep the adapters array stable (define outside the component or wrap in useMemo), and bump storageKey or resetKey when saved state should no longer apply.