Skip to content

Endless Scrolling

Endless scrolling — append mode — accumulates rows as the user reaches the end of the loaded list. New pages are concatenated onto what’s already loaded; there is no total count and no jump-to-row. The fetch function receives { cursor, limit, params, signal } and returns { rows, cursor, hasMore }.

  • mode: 'append' — turns on append-mode bookkeeping
  • defaultAppendViewportHandler — built-in viewport handler that triggers loadMore near the end
  • model.send({ action: 'loadMore' }) — manual load-more trigger
import { useState } from 'react'

import { Button } from '@/components/ui/button'
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { defaultAppendViewportHandler, remoteModel } from '@virtuoso.dev/data-table'

import { fetchProducts } from './api'

import type { Product } from './api'

export default function App() {
  const [model] = useState(() =>
    remoteModel<Product>({
      fetch: fetchProducts,
      initialParams: {},
      mode: 'append',
      onViewportChange: defaultAppendViewportHandler,
      pageSize: 20,
    })
  )

  return (
    <div className="space-y-4">
      <Button onClick={() => model.send({ action: 'loadMore' })} variant="outline">
        Load more
      </Button>

      <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>
      </DataTable>
    </div>
  )
}

Users can’t jump to row 50,000 without loading everything before it. For known total counts and arbitrary positioning, see the Server-side Pages example.