Server-side Paging
Offset mode requests only the pages near the viewport. Placeholders fill the rest of the scroll height while pages are pending, so total scroll size stays correct from the first render.
The fetch function receives { offset, limit, params, signal } and returns { rows, totalCount }.
APIs used
Section titled “APIs used”remoteModel()defaultOffsetViewportHandler— built-in handler for offset/limit backendsAbortSignal-aware fetch functions
import { useState } from 'react'
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { defaultOffsetViewportHandler, remoteModel } from '@virtuoso.dev/data-table'
import { fetchProducts, placeholder } from './api'
import type { Product, Query } from './api'
export default function App() {
const [category, setCategory] = useState('all')
const [model] = useState(() =>
remoteModel<Product, Query>({
actions: {
filter: {
handler: ({ params, payload }) => ({ ...params, category: payload as string }),
},
},
fetch: fetchProducts,
initialParams: { category: 'all' },
onViewportChange: defaultOffsetViewportHandler,
pageSize: 40,
placeholder,
})
)
return (
<div className="space-y-4">
<select
className="h-9 rounded-md border bg-background px-3 text-sm"
onChange={(event) => {
const nextCategory = event.target.value
setCategory(nextCategory)
model.send({ action: 'filter', payload: nextCategory })
}}
value={category}
>
<option value="all">All categories</option>
<option value="Office">Office</option>
<option value="Peripherals">Peripherals</option>
<option value="Audio">Audio</option>
</select>
<DataTable className="rounded-xl" model={model} style={{ height: 420 }}>
<DataTableColumn field="name">
<DataTableColumnHeader>Product</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ row }) => row.data.name}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="category">
<DataTableColumnHeader>Category</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="price">
<DataTableColumnHeader className="justify-end">Price</DataTableColumnHeader>
<DataTableCell className="text-right tabular-nums">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
</DataTable>
</div>
)
}When this doesn’t fit
Section titled “When this doesn’t fit”Offset mode needs the backend to answer count-aware slice queries efficiently. For cursor APIs, search-after pagination, or feeds that don’t expose a total count, see the Cursor Load More example.