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.
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
getRowIdfrom your API's id.- Stable
columns(module scope). manual*for everything the server does, plusrowCount.isLoadingfor the first load,showProgressBarsfor refetches,isSavingfor writes.- Show errors (
people.isError) withrenderEmptyor above the table.
Related
Server-side dataWhen your data lives on a server, hand pagination, sorting, and filtering off to it.State managementThe data table keeps all of its state in TanStack Table's store: the standard table state (sorting, filters, selection…) and its own UI state (density, editing, filter modes…).Best practicesA few habits keep tables fast, typed and easy to change.