Docs
Cloudflare
CodeRabbit
Railway
SerpAPI
Netlify
OpenRouter
WorkOS
Clerk
AG Grid
Unkey
Prisma
Sentry
Electric
Cloudflare
CodeRabbit
Railway
SerpAPI
Netlify
OpenRouter
WorkOS
Clerk
AG Grid
Unkey
Prisma
Sentry
Electric
Table API Reference
Column API Reference
Row API Reference
Cell API Reference
Header API Reference
Features API Reference
Static Functions API Reference

Solid Example: Virtualized Infinite Scrolling

import {
  FlexRender,
  columnSizingFeature,
  createColumnHelper,
  createSortedRowModel,
  createTable,
  rowSortingFeature,
  sortFns,
  tableFeatures,
} from '@tanstack/solid-table'
import { keepPreviousData, useInfiniteQuery } from '@tanstack/solid-query'
import { createAtom, useSelector } from '@tanstack/solid-store'
import { createVirtualizer } from '@tanstack/solid-virtual'
import { For, Show, createMemo, onMount } from 'solid-js'
import { fetchData } from './makeData'
import type { Person, PersonApiResponse } from './makeData'
import type { SortingState } from '@tanstack/solid-table'
import type { Virtualizer } from '@tanstack/solid-virtual'

const fetchSize = 50

const features = tableFeatures({ columnSizingFeature, rowSortingFeature })

const columnHelper = createColumnHelper<typeof features, Person>()

const columns = columnHelper.columns([
  columnHelper.accessor('id', {
    header: 'ID',
    size: 60,
  }),
  columnHelper.accessor('firstName', {
    cell: (info) => info.getValue(),
  }),
  columnHelper.accessor((row) => row.lastName, {
    id: 'lastName',
    cell: (info) => info.getValue(),
    header: () => <span>Last Name</span>,
  }),
  columnHelper.accessor('age', {
    header: () => 'Age',
    size: 50,
  }),
  columnHelper.accessor('visits', {
    header: () => <span>Visits</span>,
    size: 50,
  }),
  columnHelper.accessor('status', {
    header: 'Status',
  }),
  columnHelper.accessor('progress', {
    header: 'Profile Progress',
    size: 80,
  }),
  columnHelper.accessor('createdAt', {
    header: 'Created At',
    cell: (info) => info.getValue<Date>().toLocaleString(),
    size: 200,
  }),
])

function App() {
  let tableContainerRef: HTMLDivElement | undefined

  const sortingAtom = createAtom<SortingState>([])
  const sorting = useSelector(sortingAtom)

  const query = useInfiniteQuery<PersonApiResponse>(() => ({
    queryKey: ['people', sorting()],
    queryFn: async ({ pageParam = 0 }) => {
      const start = (pageParam as number) * fetchSize
      return fetchData(start, fetchSize, sorting())
    },
    initialPageParam: 0,
    getNextPageParam: (
      _lastGroup: PersonApiResponse,
      groups: Array<PersonApiResponse>,
    ) => groups.length,
    refetchOnWindowFocus: false,
    placeholderData: keepPreviousData,
  }))

  const flatData = createMemo(
    () => query.data?.pages.flatMap((page) => page.data) ?? [],
  )
  const totalDBRowCount = () => query.data?.pages[0]?.meta?.totalRowCount ?? 0
  const totalFetched = () => flatData().length

  const fetchMoreOnBottomReached = (
    containerRefElement?: HTMLDivElement | null,
  ) => {
    if (containerRefElement) {
      const { scrollHeight, scrollTop, clientHeight } = containerRefElement
      if (
        scrollHeight - scrollTop - clientHeight < 500 &&
        !query.isFetching &&
        totalFetched() < totalDBRowCount()
      ) {
        void query.fetchNextPage()
      }
    }
  }

  // Check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data
  onMount(() => {
    fetchMoreOnBottomReached(tableContainerRef)
  })

  const table = createTable({
    features,
    rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
    get data() {
      return flatData()
    },
    columns,
    atoms: {
      sorting: sortingAtom,
    },
    manualSorting: true,
    debugTable: true,
  })

  const rows = () => table.getRowModel().rows

  // Important: The virtualizer and the scroll container ref must be in the same
  // component scope, and NOT inside a <Show> wrapper. <Show> creates a reactive
  // boundary that disrupts the virtualizer's onMount timing.
  const rowVirtualizer = createVirtualizer<HTMLDivElement, HTMLTableRowElement>(
    {
      get count() {
        return rows().length
      },
      estimateSize: () => 33,
      getScrollElement: () => tableContainerRef ?? null,
      measureElement:
        typeof window !== 'undefined' &&
        navigator.userAgent.indexOf('Firefox') === -1
          ? (element) => element.getBoundingClientRect().height
          : undefined,
      overscan: 5,
    },
  )

  return (
    <div class="app">
      <Show when={import.meta.env.DEV}>
        <p>
          <strong>Notice:</strong> You are currently running Solid in
          development mode. Virtualized rendering performance will be slightly
          degraded until this application is built for production.
        </p>
      </Show>
      ({totalFetched().toLocaleString()} of {totalDBRowCount().toLocaleString()}{' '}
      rows fetched)
      <div
        class="container"
        onScroll={(e) => fetchMoreOnBottomReached(e.currentTarget)}
        ref={tableContainerRef}
        style={{
          overflow: 'auto',
          position: 'relative',
          height: '600px',
        }}
      >
        <table style={{ display: 'grid' }}>
          <thead
            style={{
              display: 'grid',
              position: 'sticky',
              top: '0px',
              'z-index': 1,
            }}
          >
            <For each={table.getHeaderGroups()}>
              {(headerGroup) => (
                <tr style={{ display: 'flex', width: '100%' }}>
                  <For each={headerGroup.headers}>
                    {(header) => (
                      <th
                        style={{
                          display: 'flex',
                          width: `${header.getSize()}px`,
                        }}
                      >
                        <div
                          class={
                            header.column.getCanSort() ? 'sortable-header' : ''
                          }
                          onClick={header.column.getToggleSortingHandler()}
                        >
                          <FlexRender header={header} />
                          {(
                            {
                              asc: ' 🔼',
                              desc: ' 🔽',
                            } as Record<string, string>
                          )[header.column.getIsSorted() as string] ?? null}
                        </div>
                      </th>
                    )}
                  </For>
                </tr>
              )}
            </For>
          </thead>
          <tbody
            style={{
              display: 'grid',
              height: `${rowVirtualizer.getTotalSize()}px`,
              position: 'relative',
            }}
          >
            <For each={rowVirtualizer.getVirtualItems()}>
              {(virtualRow) => {
                const row = rows()[virtualRow.index]
                return (
                  <tr
                    data-index={virtualRow.index}
                    ref={(node) => rowVirtualizer.measureElement(node)}
                    style={{
                      display: 'flex',
                      position: 'absolute',
                      transform: `translateY(${virtualRow.start}px)`,
                      width: '100%',
                    }}
                  >
                    <For each={row.getAllCells()}>
                      {(cell) => (
                        <td
                          style={{
                            display: 'flex',
                            width: `${cell.column.getSize()}px`,
                          }}
                        >
                          <FlexRender cell={cell} />
                        </td>
                      )}
                    </For>
                  </tr>
                )
              }}
            </For>
          </tbody>
        </table>
      </div>
      <Show when={query.isFetching}>
        <div>Fetching More...</div>
      </Show>
    </div>
  )
}

export default App
import {
  FlexRender,
  columnSizingFeature,
  createColumnHelper,
  createSortedRowModel,
  createTable,
  rowSortingFeature,
  sortFns,
  tableFeatures,
} from '@tanstack/solid-table'
import { keepPreviousData, useInfiniteQuery } from '@tanstack/solid-query'
import { createAtom, useSelector } from '@tanstack/solid-store'
import { createVirtualizer } from '@tanstack/solid-virtual'
import { For, Show, createMemo, onMount } from 'solid-js'
import { fetchData } from './makeData'
import type { Person, PersonApiResponse } from './makeData'
import type { SortingState } from '@tanstack/solid-table'
import type { Virtualizer } from '@tanstack/solid-virtual'

const fetchSize = 50

const features = tableFeatures({ columnSizingFeature, rowSortingFeature })

const columnHelper = createColumnHelper<typeof features, Person>()

const columns = columnHelper.columns([
  columnHelper.accessor('id', {
    header: 'ID',
    size: 60,
  }),
  columnHelper.accessor('firstName', {
    cell: (info) => info.getValue(),
  }),
  columnHelper.accessor((row) => row.lastName, {
    id: 'lastName',
    cell: (info) => info.getValue(),
    header: () => <span>Last Name</span>,
  }),
  columnHelper.accessor('age', {
    header: () => 'Age',
    size: 50,
  }),
  columnHelper.accessor('visits', {
    header: () => <span>Visits</span>,
    size: 50,
  }),
  columnHelper.accessor('status', {
    header: 'Status',
  }),
  columnHelper.accessor('progress', {
    header: 'Profile Progress',
    size: 80,
  }),
  columnHelper.accessor('createdAt', {
    header: 'Created At',
    cell: (info) => info.getValue<Date>().toLocaleString(),
    size: 200,
  }),
])

function App() {
  let tableContainerRef: HTMLDivElement | undefined

  const sortingAtom = createAtom<SortingState>([])
  const sorting = useSelector(sortingAtom)

  const query = useInfiniteQuery<PersonApiResponse>(() => ({
    queryKey: ['people', sorting()],
    queryFn: async ({ pageParam = 0 }) => {
      const start = (pageParam as number) * fetchSize
      return fetchData(start, fetchSize, sorting())
    },
    initialPageParam: 0,
    getNextPageParam: (
      _lastGroup: PersonApiResponse,
      groups: Array<PersonApiResponse>,
    ) => groups.length,
    refetchOnWindowFocus: false,
    placeholderData: keepPreviousData,
  }))

  const flatData = createMemo(
    () => query.data?.pages.flatMap((page) => page.data) ?? [],
  )
  const totalDBRowCount = () => query.data?.pages[0]?.meta?.totalRowCount ?? 0
  const totalFetched = () => flatData().length

  const fetchMoreOnBottomReached = (
    containerRefElement?: HTMLDivElement | null,
  ) => {
    if (containerRefElement) {
      const { scrollHeight, scrollTop, clientHeight } = containerRefElement
      if (
        scrollHeight - scrollTop - clientHeight < 500 &&
        !query.isFetching &&
        totalFetched() < totalDBRowCount()
      ) {
        void query.fetchNextPage()
      }
    }
  }

  // Check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data
  onMount(() => {
    fetchMoreOnBottomReached(tableContainerRef)
  })

  const table = createTable({
    features,
    rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
    get data() {
      return flatData()
    },
    columns,
    atoms: {
      sorting: sortingAtom,
    },
    manualSorting: true,
    debugTable: true,
  })

  const rows = () => table.getRowModel().rows

  // Important: The virtualizer and the scroll container ref must be in the same
  // component scope, and NOT inside a <Show> wrapper. <Show> creates a reactive
  // boundary that disrupts the virtualizer's onMount timing.
  const rowVirtualizer = createVirtualizer<HTMLDivElement, HTMLTableRowElement>(
    {
      get count() {
        return rows().length
      },
      estimateSize: () => 33,
      getScrollElement: () => tableContainerRef ?? null,
      measureElement:
        typeof window !== 'undefined' &&
        navigator.userAgent.indexOf('Firefox') === -1
          ? (element) => element.getBoundingClientRect().height
          : undefined,
      overscan: 5,
    },
  )

  return (
    <div class="app">
      <Show when={import.meta.env.DEV}>
        <p>
          <strong>Notice:</strong> You are currently running Solid in
          development mode. Virtualized rendering performance will be slightly
          degraded until this application is built for production.
        </p>
      </Show>
      ({totalFetched().toLocaleString()} of {totalDBRowCount().toLocaleString()}{' '}
      rows fetched)
      <div
        class="container"
        onScroll={(e) => fetchMoreOnBottomReached(e.currentTarget)}
        ref={tableContainerRef}
        style={{
          overflow: 'auto',
          position: 'relative',
          height: '600px',
        }}
      >
        <table style={{ display: 'grid' }}>
          <thead
            style={{
              display: 'grid',
              position: 'sticky',
              top: '0px',
              'z-index': 1,
            }}
          >
            <For each={table.getHeaderGroups()}>
              {(headerGroup) => (
                <tr style={{ display: 'flex', width: '100%' }}>
                  <For each={headerGroup.headers}>
                    {(header) => (
                      <th
                        style={{
                          display: 'flex',
                          width: `${header.getSize()}px`,
                        }}
                      >
                        <div
                          class={
                            header.column.getCanSort() ? 'sortable-header' : ''
                          }
                          onClick={header.column.getToggleSortingHandler()}
                        >
                          <FlexRender header={header} />
                          {(
                            {
                              asc: ' 🔼',
                              desc: ' 🔽',
                            } as Record<string, string>
                          )[header.column.getIsSorted() as string] ?? null}
                        </div>
                      </th>
                    )}
                  </For>
                </tr>
              )}
            </For>
          </thead>
          <tbody
            style={{
              display: 'grid',
              height: `${rowVirtualizer.getTotalSize()}px`,
              position: 'relative',
            }}
          >
            <For each={rowVirtualizer.getVirtualItems()}>
              {(virtualRow) => {
                const row = rows()[virtualRow.index]
                return (
                  <tr
                    data-index={virtualRow.index}
                    ref={(node) => rowVirtualizer.measureElement(node)}
                    style={{
                      display: 'flex',
                      position: 'absolute',
                      transform: `translateY(${virtualRow.start}px)`,
                      width: '100%',
                    }}
                  >
                    <For each={row.getAllCells()}>
                      {(cell) => (
                        <td
                          style={{
                            display: 'flex',
                            width: `${cell.column.getSize()}px`,
                          }}
                        >
                          <FlexRender cell={cell} />
                        </td>
                      )}
                    </For>
                  </tr>
                )
              }}
            </For>
          </tbody>
        </table>
      </div>
      <Show when={query.isFetching}>
        <div>Fetching More...</div>
      </Show>
    </div>
  )
}

export default App