shadcn/ui Data Table’a sıralama, filtreleme, sayfalama, sütun gizleme ve satır seçimi eklemek için her özellikte aynı kalıbı izlersiniz: TanStack Table’dan ilgili satır modelini (getSortedRowModel gibi) içe aktarır, gerekiyorsa bir React state değişkeni açar, useReactTable çağrısına hem “değiştiğinde” işleyicisini hem de state alanını verir, sonra arayüze butonu, girdi kutusunu ya da onay kutusunu koyarsınız. Bu yazı, belgenin bu ikinci yarısını adım adım ve gerekçeleriyle anlatıyor.
Burada, ilk yazıda kurulan columns.tsx, data-table.tsx ve page.tsx yapısının hazır olduğunu varsayıyoruz. O temeli henüz kurmadıysanız önce shadcn Data Table oluşturma yazısını okuyun. Serinin tüm konuları shadcn/ui öğrenme rehberi sayfasında listeleniyor.
Ortak kalıp: satır modeli, durum ve işleyici

TanStack Table’ın her özelliği üç parçayla çalışır. Satır modeli fonksiyonu, satırların o özelliğe göre işlenmiş hâlini üretir. Durum (state) o anki seçimi tutar: hangi sütuna göre sıralandığı, filtre kutusunda ne yazdığı, hangi satırların seçildiği. İşleyici (onSortingChange gibi) tablo durumu değiştirmek istediğinde çağrılır; belge her özellikte bunu doğrudan React’in setState fonksiyonuna bağlar. Böylece durum sizin bileşeninizde yaşar ve tabloya state alanıyla geri verilir.
| Özellik | Satır modeli | State tipi ve başlangıcı | İşleyici |
|---|---|---|---|
| Sayfalama | getPaginationRowModel() | Belgede ayrı state yok | Belgede ayrı işleyici yok |
| Sıralama | getSortedRowModel() | SortingState, [] | onSortingChange |
| Filtreleme | getFilteredRowModel() | ColumnFiltersState, [] | onColumnFiltersChange |
| Sütun görünürlüğü | Ek satır modeli yok | VisibilityState, {} | onColumnVisibilityChange |
| Satır seçimi | Ek satır modeli yok | Tipsiz useState({}) | onRowSelectionChange |
Tablo, belgedeki kodun özetidir. Sayfalamada state açılmaz; sayfa bilgisini tablo kendi içinde tutar ve siz table.getState().pagination ile okursunuz. Görünürlük ve seçim için ayrı bir satır modeli içe aktarılmaz, yalnızca durum ve işleyici eklenir.
Sayfalama
İlk adım satır modelini eklemektir. app/payments/data-table.tsx dosyasında içe aktarmaya getPaginationRowModel eklenir ve useReactTable içine verilir:
import {
ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
// ...
}Belgeye göre bu tek satır, satırları otomatik olarak 10’arlı sayfalara böler. Sayfa boyutunu değiştirmek ya da sunucu taraflı (manuel) sayfalama yapmak için belge TanStack Table’ın sayfalama belgesine yönlendirir. Sayfalama eklendikten sonra table.getRowModel().rows yalnızca geçerli sayfanın satırlarını döndürür; ilk yazıdaki gövde kodu değişmeden çalışmaya devam eder.
Kontroller için iki Button yeterlidir. table.previousPage() ve table.nextPage() sayfayı değiştirir; getCanPreviousPage() ve getCanNextPage() ise ilk ya da son sayfada butonu devre dışı bırakmak için kullanılır:
import { Button } from "@/components/ui/button"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
return (
<div>
<div className="overflow-hidden rounded-md border">
<Table>{/* ilk yazıdaki başlık ve gövde */}</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Önceki
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Sonraki
</Button>
</div>
</div>
)
}Kapsayıcı yapı da değişti: dış katmanda artık çerçeveli tablo ve onun altında kontrol satırı olan sade bir div var. Diğer özellikler de arama kutusu, sütun menüsü gibi öğeleri tablonun üstüne ya da altına bu şekilde ekler. shadcn’in ayrı bir Pagination bileşeni de vardır; ancak data table rehberi sayfalamayı bu butonlarla ve aşağıdaki yeniden kullanılabilir DataTablePagination bileşeniyle kurar.
Sıralama
Sıralama için hem state hem işleyici gerekir. SortingState tipinde, boş diziyle başlayan bir state açılır; onSortingChange bu state’in ayarlayıcısına, getSortedRowModel de satır modeline bağlanır:
"use client"
import * as React from "react"
import {
ColumnDef,
SortingState,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
state: {
sorting,
},
})
// ...
}Tablo artık sıralayabilir ama kullanıcının bunu tetikleyeceği bir yer yok. Belge e-posta sütununun başlığını, tıklanınca sıralamayı değiştiren bir ghost butona çevirir. app/payments/columns.tsx dosyasında (bu dosyada Button içe aktarımının da bulunması gerekir):
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { ArrowUpDown } from "lucide-react"
import { Button } from "@/components/ui/button"
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "email",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
E-posta
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
},
]column.toggleSorting(column.getIsSorted() === "asc") ifadesi kısa ama zekice: toggleSorting fonksiyonuna verilen değer “azalan sırala” anlamındadır. Sütun şu an artan sıradaysa true gider ve azalana geçilir; değilse false gider ve artan sıraya geçilir. Belge bunun sonucunu, başlığa tıklandıkça tablonun artan ve azalan arasında otomatik sıralanması olarak tarif eder. header artık bir fonksiyon olduğu için ilk yazıdaki flexRender çağrısı onu buton olarak çizer.
Filtreleme

E-posta sütununda arama yapmak için ColumnFiltersState tipinde bir state, onColumnFiltersChange işleyicisi ve getFilteredRowModel satır modeli eklenir. Arayüz tarafında tablonun üstüne bir Input konur:
"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
state: {
sorting,
columnFilters,
},
})
return (
<div>
<div className="flex items-center py-4">
<Input
placeholder="E-posta ara..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
</div>
<div className="overflow-hidden rounded-md border">
<Table>{/* ... */}</Table>
</div>
</div>
)
}table.getColumn("email"), sütunu accessorKey’ten gelen kimliğiyle bulur. Sonundaki ?., sütun bulunamazsa hata vermek yerine undefined döner. Girdi kutusunun değeri getFilterValue() ile filtre durumundan okunur, her tuş vuruşunda da setFilterValue ile geri yazılır. ?? "" kısmı, henüz filtre yokken kutuya boş metin verilmesini sağlar.
Belgenin bir sonraki adımındaki (görünürlük) kodda bu ?? "" eki yazılmamıştır. Filtre adımındaki biçimi korumanız daha tutarlıdır; aksi hâlde girdi kutusuna ilk anda undefined değer gider. Belge diğer sütunlara da filtre eklenebileceğini söyler ve özel filtreler için TanStack Table’ın filtre belgesine yönlendirir. Sıralanmış ve filtrelenmiş satırlar sayfalamadan önce hesaplandığı için, filtre uygulandığında sayfa sayısı da kendiliğinden değişir.
Sütun görünürlüğü
Kullanıcının hangi sütunları göreceğini seçmesi için VisibilityState tipinde, boş nesneyle başlayan bir state ve onColumnVisibilityChange işleyicisi eklenir. Arayüzde, tablonun sağ üstüne bir DropdownMenu konur ve her sütun için bir DropdownMenuCheckboxItem üretilir. Kodun ilgili parçası:
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const table = useReactTable({
// ... önceki ayarlar
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
})
// arama kutusunun yanında:
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Sütunlar
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>Bu kod parçasında VisibilityState, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent ve DropdownMenuTrigger içe aktarımları belgede olduğu gibi dosyanın başına eklenir. getAllColumns() tüm sütunları verir; getCanHide() filtresi gizlenmesine izin verilmeyen sütunları listeden çıkarır. Menüde etiket olarak column.id gösterilir; capitalize sınıfı ilk harfi büyütür. Sütun kimlikleri İngilizce alan adlarıysa menüde de onlar görünür; Türkçe etiket istiyorsanız bunu kendi kodunuzda ayrıca eşlemeniz gerekir.
onCheckedChange içindeki !!value, gelen değeri kesin bir boolean’a çevirir. Aynı kalıp satır seçiminde de karşımıza çıkacak; orada Checkbox bileşeninin checked değeri, aşağıda göreceğiniz gibi "indeterminate" da olabilir.
Satır seçimi
Seçim için önce sütun tanımlarının başına bir select sütunu eklenir. Başlıkta “tümünü seç”, her hücrede “bu satırı seç” onay kutusu bulunur. app/payments/columns.tsx dosyasında:
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
export const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Tümünü seç"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Satırı seç"
/>
),
enableSorting: false,
enableHiding: false,
},
]Başlık kutusunun checked değeri üç durumdan birini alır: sayfadaki tüm satırlar seçiliyse true, bazıları seçiliyse "indeterminate" (kısmen seçili görünüm), hiçbiri seçili değilse false. Fonksiyon adlarındaki “Page” sözcüğü, bu kutunun geçerli sayfadaki satırlar üzerinde çalıştığını gösterir. enableSorting: false ve enableHiding: false seçim sütununun sıralanmasını ve görünürlük menüsünden gizlenmesini engeller. aria-label değerleri, görünür metni olmayan bu kutulara erişilebilir ad verir.
Ardından DataTable bileşenine rowSelection state’i ve onRowSelectionChange işleyicisi eklenir:
const [rowSelection, setRowSelection] = React.useState({})
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
})İlk yazıdaki gövde kodunda bulunan data-state={row.getIsSelected() && "selected"} satırı artık işe yarar: seçilen satır, TableRow stilindeki data-[state=selected]:bg-muted sayesinde vurgulanır. Kaç satırın seçildiğini göstermek için belge getFilteredSelectedRowModel() API’sini kullanır:
<div className="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} /{" "}
{table.getFilteredRowModel().rows.length} satır seçildi.
</div>Burada iki farklı sayı var: seçili satırların sayısı ve filtreden geçen toplam satır sayısı. İkisi de filtrelenmiş satır modeline dayanır, yani arama kutusuna bir şey yazdığınızda payda da küçülür.
Yeniden kullanılabilir parçalar

Belgenin son bölümü, shadcn sitesindeki Tasks örneğinden alınmış üç yardımcı bileşeni gösterir. Bunların kaynak kodu shadcn-ui/ui GitHub deposu içinde Tasks örneğinin components klasöründedir. Hepsi DropdownMenu, Select ve Button gibi tanıdık bileşenlerle yazılmıştır.
| Bileşen | Görevi | Kullanımı |
|---|---|---|
| DataTableColumnHeader | Başlığı sıralanabilir ve gizlenebilir yapar | header içinde column ve title ile |
| DataTablePagination | Sayfa boyutu, sayfa numarası, ilk/son sayfa ve seçim sayısı | table prop ile |
| DataTableViewOptions | Sütun görünürlüğü menüsü | table prop ile |
DataTableColumnHeader
Bu bileşen sütun sıralanamıyorsa (column.getCanSort() yanlışsa) yalnızca başlık metnini gösterir. Sıralanabiliyorsa bir açılır menü üretir: “Asc” öğesi column.toggleSorting(false), “Desc” öğesi column.toggleSorting(true), “Hide” öğesi column.toggleVisibility(false) çağırır. Butondaki ikon da o anki duruma göre değişir: azalan sırada aşağı ok, artan sırada yukarı ok, sırasızken çift yönlü ok. Sütun tanımında kullanımı şöyledir:
export const columns = [
{
accessorKey: "email",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="E-posta" />
),
},
]DataTablePagination
Bu bileşen <DataTablePagination table={table} /> biçiminde kullanılır. Solda seçim sayısını, sağda üç kontrol grubunu gösterir. Sayfa boyutu bir Select ile seçilir; seçenekler 10, 20, 25, 30, 40 ve 50’dir ve seçim table.setPageSize(Number(value)) ile uygulanır. Sayfa göstergesi table.getState().pagination.pageIndex + 1 ve table.getPageCount() değerlerinden oluşur; pageIndex sıfırdan başladığı için bir eklenir. İlk sayfa butonu table.setPageIndex(0), son sayfa butonu table.setPageIndex(table.getPageCount() - 1) çağırır. İlk ve son sayfa butonları hidden lg:flex sınıflarıyla yalnızca geniş ekranda görünür.
DataTableViewOptions
<DataTableViewOptions table={table} /> biçiminde kullanılan bu menü, yukarıdaki görünürlük kodunun toparlanmış hâlidir; tek önemli fark filtredir. Burada yalnızca typeof column.accessorFn !== "undefined" && column.getCanHide() koşulunu sağlayan sütunlar listelenir. Yani veriye bağlı olmayan sütunlar, örneğin accessorKey taşımayan aksiyon sütunu, menüde görünmez. İlk yazıda aksiyon sütununa yalnızca id vermemizin bir faydası da budur.
Uçtan uca küçük bir senaryo
Bütün parçaları bir araya getirince akış şöyle işler. Kullanıcı arama kutusuna “gmail” yazar: filtre durumu güncellenir, filtrelenmiş satır modeli yalnızca eşleşen satırları bırakır. E-posta başlığına tıklar: sıralama durumu değişir ve bu satırlar alfabetik dizilir. Sayfalama bu sonucun üzerinde çalıştığı için ilk 10 satır gösterilir ve sayfa sayısı eşleşen satır sayısına göre yeniden hesaplanır. Ardından iki satırı işaretler: seçim sayacı “2 / N satır seçildi.” biçiminde güncellenir ve seçilen satırlar vurgulanır. Son olarak sütun menüsünden “amount” işaretini kaldırır: tutar sütunu gizlenir, çünkü gövde kodu row.getVisibleCells() ile yalnızca görünür hücreleri çizer.
Bu senaryodaki hiçbir adım için ilk yazıdaki gövde kodunu değiştirmediğimize dikkat edin. Bütün davranış useReactTable ayarlarından ve sütun tanımlarından gelir; görünüm katmanı aynı kalır. Belgenin hazır bileşen yerine kılavuz sunmasının nedeni de bu esnekliği korumaktır.
Sık yapılan hatalar
- State’i oluşturup
statealanına vermemek. ÖrneğinonSortingChange: setSortingyazıpstate: { sorting }eklemezseniz durum bileşeninizde güncellenir ama tabloya geri dönmez. Belgedeki her adım ikisini birlikte ekler. - Satır modelini içe aktarmayı unutmak.
getSortedRowModelya dagetFilteredRowModelhem içe aktarılmalı hem de çağrılarak (getSortedRowModel()) verilmelidir. - Butonu içe aktarmadan başlığa koymak. Sıralama adımında başlık
Buttondöndürür;columns.tsxdosyasına@/components/ui/buttoniçe aktarımını eklemeyi unutmayın. - Filtre sütun adını yanlış yazmak.
getColumn("eposta")gibi sütunda olmayan bir kimlik verirseniz,?.sayesinde hata almazsınız ama filtre de çalışmaz. - Seçim sütununu sıralanabilir bırakmak. Belgede seçim sütununa
enableSorting: falseveenableHiding: falseverilir; bunlar olmazsa sütun görünürlük menüsünde de listelenir. - “Tümünü seç”in bütün veriyi seçtiğini varsaymak. Kullanılan fonksiyonlar (
toggleAllPageRowsSelected,getIsAllPageRowsSelected) sayfa düzeyindedir.
Sık Sorulan Sorular
Sayfa başına 10 satır yerine başka bir sayı nasıl gösterilir?
Belgeye göre getPaginationRowModel() varsayılan olarak 10’arlı sayfa üretir. Sayfa boyutunu kullanıcıya seçtirmek için DataTablePagination bileşenindeki gibi table.setPageSize() çağırabilirsiniz; ayrıntılı ayarlar için belge TanStack Table sayfalama belgesine yönlendirir.
Sunucu taraflı sayfalama ya da filtreleme yapılabilir mi?
Belge bu konuyu shadcn tarafında anlatmaz; manuel sayfalama ve özel filtreler için TanStack Table belgelerine bağlantı verir. shadcn bileşenleri yalnızca görünüm katmanıdır, mantık TanStack’tedir.
Birden fazla sütunda arama yapabilir miyim?
Belge yalnızca e-posta sütunu için örnek verir ve diğer sütunlara da filtre eklenebileceğini söyler. Her sütun için table.getColumn("kimlik")?.setFilterValue(...) çağıran ayrı bir girdi ya da seçim kutusu ekleyebilirsiniz.
Seçili satırların verisine nasıl ulaşırım?
Belge sayaç için table.getFilteredSelectedRowModel().rows kullanır. Bu dizideki her satırın original özelliği, ilk yazıdaki aksiyon sütununda olduğu gibi ham veriyi verir; toplu silme gibi işlemleri buradan kurabilirsiniz.
Hazır parçaları projeme nasıl eklerim?
DataTableColumnHeader, DataTablePagination ve DataTableViewOptions CLI ile eklenen registry bileşenleri olarak belgelenmemiştir; belge kaynak kodlarını Tasks örneğinden gösterir. Kodları kopyalayıp içe aktarma yollarını kendi projenize uyarlarsınız.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: shadcn/ui öğrenme rehberi · Data Table oluşturma · Checkbox ve seçim bileşenleri · Select · Input · Dropdown Menu