Shadcn React Table

Search documentation

Search the docs

Production-ready tables

A table over a real API: data fetched with React Query, paging, sorting and search done on the server, the table's state kept in one hook, and edits saved through a mutation.

React QueryServer-side paging, sorting and search with React Query.
GitHub
Loading…
ID
$0
Rows per page
0–0 of 0

1. Keep the server-facing state in one hook

tsx
import type { PaginationState, SortingState } from "@tanstack/react-table"

function usePeopleTableState() {
  const [pagination, setPagination] = React.useState<PaginationState>({
    pageIndex: 0,
    pageSize: 25,
  })
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [globalFilter, setGlobalFilter] = React.useState("")
  return {
    state: { pagination, sorting, globalFilter },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    // What the API needs, derived in one place.
    query: {
      page: pagination.pageIndex + 1,
      size: pagination.pageSize,
      sort: sorting[0],
      q: globalFilter,
    },
  }
}

Swap the useState calls for your router's search params to make the URL the source of truth.

2. Fetch with React Query

tsx
const tableState = usePeopleTableState()
const people = useQuery({
  queryKey: ["people", tableState.query],
  queryFn: () => api.people(tableState.query),
  placeholderData: keepPreviousData, // keep the current page while the next loads
})

3. Hand both to the table

tsx
const table = useDataTable({
  data: people.data?.rows ?? [],
  columns,
  getRowId: (row) => row.id,
  rowCount: people.data?.total ?? 0,
  manualPagination: true,
  manualSorting: true,
  manualFiltering: true,
  state: tableState.state,
  onPaginationChange: tableState.onPaginationChange,
  onSortingChange: tableState.onSortingChange,
  onGlobalFilterChange: tableState.onGlobalFilterChange,
  isLoading: people.isPending, // skeletons on the first load
  showProgressBars: people.isFetching, // a progress bar on later loads
})

With manual filtering, the search box waits for a pause in typing before it sets globalFilter, so each keystroke doesn't send a request.

4. Save edits through a mutation

tsx
const save = useMutation({
  mutationFn: api.updatePerson,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ["people"] }),
})

useDataTable({
  // …
  enableEditing: true,
  editDisplayMode: "modal",
  isSaving: save.isPending,
  onEditingRowSave: async ({ row, values, exit }) => {
    await save.mutateAsync({ id: row.id, ...values })
    exit()
  },
})

isSaving shows the progress bar while the request runs; exit() closes the editor only once the save succeeds.

Checklist

  • getRowId from your API's id.
  • Stable columns (module scope).
  • manual* for everything the server does, plus rowCount.
  • isLoading for the first load, showProgressBars for refetches, isSaving for writes.
  • Show errors (people.isError) with renderEmpty or above the table.