KKiotaPay UI
Browse documentation

Paginated Data Table

A server-paginated table. Page, size and search live in the URL, so a filtered view is shareable and survives a refresh.

Installation

pnpm dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/paginated-data-table.json

Adds @tanstack/react-table, @tanstack/react-query and nuqs, plus lib/kiota-pagination.ts.

Setup

Needs both providers above it:

'use client'

import { QueryClientProvider } from '@tanstack/react-query'
import { NuqsAdapter } from 'nuqs/adapters/next/app'

export function Providers({ client, children }) {
  return (
    <QueryClientProvider client={client}>
      <NuqsAdapter>{children}</NuqsAdapter>
    </QueryClientProvider>
  )
}

Usage

searchMethod receives a zero-based page index and must resolve to a PaginatedApiResponse.

'use client'

import { PaginatedDataTable } from '@/components/custom/paginated-data-table'
import { buildPaginationQuery, type SearchMethodParams } from '@/lib/kiota-pagination'

async function searchEmployees(params: SearchMethodParams) {
  const query = buildPaginationQuery(params)
  return fetch(`/api/employees?${query}`).then((response) => response.json())
}

export function EmployeeTable() {
  return (
    <PaginatedDataTable
      columns={columns}
      searchKeys={['employeeNo', 'fullName']}
      searchMethod={searchEmployees}
      tableKey="employees"
    />
  )
}

Response shape

interface PaginatedApiResponse<T> {
  status: number
  message: string
  data: {
    content: T[]
    page: number
    size: number
    totalElements: number
    totalPages: number
    last: boolean
  } | null
}

Props

PropTypeDefaultDescription
columnsColumnDef<TData, TValue>[]—Required.
searchKeysstring[]—Required. Used only to build the placeholder text.
searchMethod(params: SearchMethodParams) => Promise<unknown>—Required. Fetches one page.
tableKeystring'paginated-datatable'Query-cache key and id prefix. Unique per table.
queryParams{ search?, page?, size? }search / page / sizeRename the URL parameters.
searchPlaceholderstringderivedOverrides the generated placeholder.
sortField / sortDescstring / boolean—Initial client-side sort of the current page.
selectedItems / setSelectedItemsstring[] / setter—Passing these turns on the selection column.
bodyClassstring—Applied to the scroll container; override the 70vh maximum.
loadingState / errorStateReactNode—Replace the default skeleton and error panel.

Behaviour notes

Two tables on one route need two queryParams sets, or they will fight over ?page. tableKey alone only separates the caches.

Page numbers are one-based in the URL and zero-based on the wire. People read ?page=1 as the first page; most back ends read 0. The component converts at the boundary.

The previous page stays on screen while the next loads, via keepPreviousData, so paging does not flash an empty table between requests. The thin bar at the top marks the fetch.

Sorting applies to the current page only. The server decides the order across pages; the header sort reorders the rows already fetched. Sort across the whole set in searchMethod instead.

Selection is keyed on uuid or id and persists across pages and searches — it lives in your state, not the table's, so a bulk action can gather rows from several pages.

A hand-edited ?size=99999 is clamped to 500 and written back to the URL, so what is fetched and what the URL claims cannot disagree.