preact-client-to-server

star 28.1k

Convert a client-side `@tanstack/preact-table` to server-side (a.k.a. manual) modes. Pass server-paginated data, set `manualSorting` / `manualFiltering` / `manualPagination` / `manualGrouping` / `manualExpanding` for whatever the server owns, supply `rowCount`, key external atoms for pagination/sorting/ filters and trigger a refetch when they change. Routing keywords: server-side pagination, manual pagination, manualSorting, manualFiltering, rowCount, remote data preact.

TanStack By TanStack schedule Updated 6/5/2026

name: preact/client-to-server description: > Convert a client-side @tanstack/preact-table to server-side (a.k.a. manual) modes. Pass server-paginated data, set manualSorting / manualFiltering / manualPagination / manualGrouping / manualExpanding for whatever the server owns, supply rowCount, key external atoms for pagination/sorting/ filters and trigger a refetch when they change. Routing keywords: server-side pagination, manual pagination, manualSorting, manualFiltering, rowCount, remote data preact. type: lifecycle library: tanstack-table framework: preact library_version: '9.0.0-alpha.48' requires: - state-management - pagination - filtering - sorting - preact/table-state sources: - TanStack/table:examples/preact/basic-external-atoms/src/main.tsx - TanStack/table:examples/preact/with-tanstack-query/src/main.tsx - TanStack/table:examples/preact/with-tanstack-query/src/fetchData.ts - TanStack/table:docs/framework/preact/guide/table-state.md

Client-side tables run sort/filter/paginate through registered row-model factories. Server-side tables let the server own those stages; the table just renders what the server returned and emits state changes that the app uses to refetch. Same features, same APIs — different ownership.

The Manual Flags

Set the matching flag(s) to true to tell the table that the server (not the registered row-model factory) is doing that stage:

Flag Owned by server
manualPagination page slicing
manualSorting row ordering
manualFiltering column + global filters
manualGrouping group-by rows
manualExpanding row expansion

The matching *Feature should still be in features so its state slice exists and its APIs work — you are only telling the row-model pipeline to skip the transform.

For pagination, supply rowCount so table.getPageCount() is correct. Optional but usually required for a UI.

Source: examples/preact/with-tanstack-query/src/main.tsx.

Standard Pattern

Own the slices that drive the server request with external atoms. Subscribe to them with useSelector so the request key is reactive. Pass them through options.atoms. Trigger the refetch from the same atoms.

import { useMemo } from 'preact/hooks'
import { useCreateAtom, useSelector } from '@tanstack/preact-store'
import {
  rowPaginationFeature,
  tableFeatures,
  useTable,
  type PaginationState,
} from '@tanstack/preact-table'

const features = tableFeatures({ rowPaginationFeature })

function App() {
  const paginationAtom = useCreateAtom<PaginationState>({
    pageIndex: 0,
    pageSize: 10,
  })
  const pagination = useSelector(paginationAtom)

  // Any data fetcher works — fetch / SWR / preact-query / a Suspense source.
  const { data: rowsPayload } = useSomeServerFetcher({
    queryKey: ['rows', pagination],
    queryFn: () => fetchRows(pagination),
  })

  const defaultData = useMemo(() => [], [])

  const table = useTable(
    {
      features, // no paginatedRowModel factory — server owns the slicing
      columns,
      data: rowsPayload?.rows ?? defaultData,
      rowCount: rowsPayload?.rowCount, // makes getPageCount() correct
      atoms: { pagination: paginationAtom },
      manualPagination: true,
    },
    (state) => state,
  )
  // ...
}

Source: examples/preact/with-tanstack-query/src/main.tsx (lines 56–86).

All Three Slices Server-Owned

Same shape, more atoms. Compose pagination + sorting + columnFilters + globalFilter into the request key.

const features = tableFeatures({
  rowPaginationFeature,
  rowSortingFeature,
  columnFilteringFeature,
  globalFilteringFeature,
})

const paginationAtom = useCreateAtom<PaginationState>({
  pageIndex: 0,
  pageSize: 10,
})
const sortingAtom = useCreateAtom<SortingState>([])
const columnFiltersAtom = useCreateAtom<ColumnFiltersState>([])
const globalFilterAtom = useCreateAtom<string>('')

const pagination = useSelector(paginationAtom)
const sorting = useSelector(sortingAtom)
const columnFilters = useSelector(columnFiltersAtom)
const globalFilter = useSelector(globalFilterAtom)

const { data } = useSomeServerFetcher({
  queryKey: ['rows', pagination, sorting, columnFilters, globalFilter],
  queryFn: () =>
    fetchRows({ pagination, sorting, columnFilters, globalFilter }),
})

const table = useTable({
  features, // no row-model factories — server owns every stage
  columns,
  data: data?.rows ?? EMPTY,
  rowCount: data?.rowCount,
  atoms: {
    pagination: paginationAtom,
    sorting: sortingAtom,
    columnFilters: columnFiltersAtom,
    globalFilter: globalFilterAtom,
  },
  manualPagination: true,
  manualSorting: true,
  manualFiltering: true,
})

When manualFiltering: true, both columnFilters and the global filter are treated as server-owned.

Common Mistakes

CRITICAL Setting manualPagination without rowCount

Wrong:

useTable({
  features,
  columns,
  data: response?.rows ?? [],
  atoms: { pagination: paginationAtom },
  manualPagination: true,
  // no rowCount
})
table.getPageCount() // Infinity / wrong

Correct:

useTable({
  features,
  columns,
  data: response?.rows ?? [],
  rowCount: response?.rowCount,
  atoms: { pagination: paginationAtom },
  manualPagination: true,
})

Without rowCount the table cannot know how many pages exist. Source: examples/preact/with-tanstack-query/src/main.tsx.

HIGH Keeping a client-side row-model factory when the server owns that stage

Wrong:

const features = tableFeatures({
  rowPaginationFeature,
  paginatedRowModel: createPaginatedRowModel(), // still runs; wasted work
})
useTable({ features, data: server.rows, manualPagination: true })

Correct:

const features = tableFeatures({ rowPaginationFeature }) // no paginatedRowModel
useTable({
  features,
  data: server.rows,
  rowCount: server.rowCount,
  manualPagination: true,
})

With manualPagination, the paginated row model has nothing useful to do; omit it from tableFeatures. Same for sortedRowModel under manualSorting, filteredRowModel under manualFiltering. Source: examples/preact/with-tanstack-query/src/main.tsx.

HIGH Forgetting to key the request on the slices the server owns

Wrong:

const { data } = useQuery({
  queryKey: ['rows'], // never changes
  queryFn: () => fetchRows(pagination),
})

Correct:

const { data } = useQuery({
  queryKey: ['rows', pagination, sorting, columnFilters, globalFilter],
  queryFn: () =>
    fetchRows({ pagination, sorting, columnFilters, globalFilter }),
})

The request must vary by the slice values; otherwise the fetcher cache returns stale data when the user sorts or pages. Source: examples/preact/with-tanstack-query/src/main.tsx.

HIGH Page flashes empty between fetches

Wrong: the request resolves to undefined while loading, so the table shows zero rows between pages.

Correct: pass a stable defaultData and (with @tanstack/preact-query) placeholderData: keepPreviousData. The table re-uses the last page's rows during the fetch.

import { keepPreviousData } from '@tanstack/preact-query'

const defaultData = useMemo(() => [], [])

const { data } = useQuery({
  queryKey: ['rows', pagination],
  queryFn: () => fetchRows(pagination),
  placeholderData: keepPreviousData,
})

const table = useTable({
  features,
  columns,
  data: data?.rows ?? defaultData,
  rowCount: data?.rowCount,
  atoms: { pagination: paginationAtom },
  manualPagination: true,
})

Source: examples/preact/with-tanstack-query/src/main.tsx.

MEDIUM Removing the matching feature from features

Wrong:

const features = tableFeatures({}) // dropped rowPaginationFeature
useTable({
  features,
  data: server.rows,
  rowCount: server.rowCount,
  manualPagination: true,
})
table.setPageIndex(0) // type error / no-op

Correct: keep the feature registered. manualPagination: true only tells the row-model pipeline to skip slicing — you still want the pagination state slice and setPageIndex / nextPage APIs.

const features = tableFeatures({ rowPaginationFeature })

Source: docs/guide/features.md.

See Also

  • tanstack-table/preact/compose-with-tanstack-query — full @tanstack/preact-query recipe with keepPreviousData and refetch ergonomics.
  • tanstack-table/preact/table-state — atoms vs state, table.Subscribe.
  • tanstack-table/pagination, tanstack-table/filtering, tanstack-table/sorting — feature-level state shapes.
Install via CLI
npx skills add https://github.com/TanStack/table --skill preact-client-to-server
Repository Details
star Stars 28,079
call_split Forks 3,522
navigation Branch main
article Path SKILL.md
More from Creator