Quick start
A table with search, a filter, sorting and pages, all kept in the URL, in three steps.
This page builds an orders table. It assumes you've installed both blocks. The result looks like the Introduction's preview.
Define columns and fields
The table needs two lists:
- Columns decide what the table shows.
- Fields decide what users can filter by.
They often match, but don't have to: you can filter by something you don't show, and show something you can't filter by.
Columns are TanStack Table column definitions. The helper types the extra options tablecn reads:
"use client"
import { createDataTableColumnHelper } from "@querycn/table-react"
import { createSelectionColumn } from "@/components/data-table/data-table-selection-column"
export interface Order {
id: string
customer: string
status: "paid" | "pending"
amount: number
}
const column = createDataTableColumnHelper<Order>()
export const orderColumns = [
createSelectionColumn<Order>(),
column.accessor("customer", { header: "Customer", size: 200 }),
column.accessor("status", { header: "Status" }),
column.accessor("amount", { header: "Amount", sortDescFirst: true }),
]Fields have a name (matching a key of your rows), a label and a type. Put them in a file without "use client", so server code can import them too:
import type { FieldDefinition } from "@querycn/filter-core"
export const orderFields: FieldDefinition[] = [
{ name: "customer", label: "Customer", type: "text" },
{
name: "status",
label: "Status",
type: "select",
options: [
{ label: "Paid", value: "paid" },
{ label: "Pending", value: "pending" },
],
},
{ name: "amount", label: "Amount", type: "number" },
]Define both outside your components, so they don't change on every render. More in Columns and Fields.
Build the table
useDataTable creates the table. The components render it:
"use client"
import { useDataTable } from "@querycn/table-react"
import { DataTable } from "@/components/data-table/data-table"
import { DataTablePagination } from "@/components/data-table/data-table-pagination"
import { DataTableSearch } from "@/components/data-table/data-table-search"
import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"
import { FilterBuilder } from "@/components/filter/filter-builder"
import { FilterChips } from "@/components/filter/filter-chips"
import { orderColumns, type Order } from "./order-columns"
export function OrdersTable({ orders }: { orders: Order[] }) {
const table = useDataTable({
data: orders,
columns: orderColumns,
getRowId: (order) => order.id,
storageKey: "orders-table", // saves the column layout in the browser
})
return (
<div className="flex flex-col gap-3">
<DataTableToolbar table={table}>
<DataTableSearch table={table} />
<FilterBuilder />
<FilterChips />
</DataTableToolbar>
<DataTable table={table} className="max-h-[600px]" />
<DataTablePagination table={table} />
</div>
)
}Wrap it in the filter provider
The provider holds the filter and connects everything to the URL. The table finds it on its own, so the filter, search, sort and page all share one URL. Pick your router:
Add the Next.js adapter first. The registry doesn't install it, since only Next.js apps need it:
pnpm add @querycn/filter-next"use client"
import { NextFilterProvider } from "@querycn/filter-next"
import { resetPagePatch } from "@querycn/table-react"
import { OrdersTable } from "@/components/orders/orders-table"
import type { Order } from "@/components/orders/order-columns"
import { orderFields } from "@/lib/order-fields"
export function OrdersView({ orders }: { orders: Order[] }) {
return (
<NextFilterProvider fields={orderFields} shallow onApply={() => resetPagePatch()}>
<OrdersTable orders={orders} />
</NextFilterProvider>
)
}shallow updates the URL without asking the server for a new page, which is right when every row is already in the browser. If your page fetches one page at a time on the server, leave it off: see Server data.
Render OrdersView from your page. On a statically prerendered route, wrap it in <Suspense>: the provider reads the URL with useSearchParams, and next build fails without a boundary.
onApply={() => resetPagePatch()} sends users back to page 1 when the filter changes. Page 4 of the old results means nothing for the new ones.
Try it
Search, filter, sort and page, and watch the URL change. The rows are filtered, sorted and paged in the browser. For large data, let your backend do it: see Server data.
Without the filter
The table doesn't need the filter. Skip @tablecn/filter-builder, drop FilterBuilder and FilterChips from the toolbar, and pass an adapter straight to useDataTable:
import { useNextAdapter } from "@querycn/filter-next"
const adapter = useNextAdapter({ shallow: true })
const table = useDataTable({ data, columns, getRowId, adapter })Without an adapter or a provider, the search, sort and page stay in memory and the URL is left alone.