Pagination — পেজ ভাগ
pagination state, getPaginationRowModel, pageIndex 0-based, rowCount, getCanNextPage, page size বদলানো, filter বদলালে page reset — বাংলায়।
পেজিশন খুব সহজ মনে হয়, কিন্তু এখানে যে দুটো ভুল সবচেয়ে বেশি হয় সেগুলো একবার শুরুতেই পরিষ্কার করে নেওয়া দরকার — কারণ পরে চেষ্টা করেঠিক করা যায় না।
state
type PaginationState = {
pageIndex: number; // 0 থেকে শুরু
pageSize: number;
};
// ৫ পেজ, ৩ নম্বর পেজে আছি (মানে তৃতীয় পেজ)
{ pageIndex: 2, pageSize: 20 }শূন্য থেকে শুরু — এটাই TanStack-এর নিয়ম, এবং এখানেই বেশিরভাগ বাগ। মানুষের স্বাভাবিক গণনা ১ থেকে শুরু, কিন্তু pageIndex: 0 মানে প্রথম পেজ।
সেটআপ
"use client";
import { useState } from "react";
import {
getCoreRowModel,
getPaginationRowModel,
useReactTable,
type PaginationState,
} from "@tanstack/react-table";
export function DataTable({ data, columns }: DataTableProps) {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
state: { pagination },
onPaginationChange: setPagination,
});
}| উপায় | কখন |
|---|---|
state + onPaginationChange (controlled) | server-side, URL-এ রাখতে চাইলে |
initialState (uncontrolled) | শুধু client-side, state বাইরে দরকার নেই |
পেজ কন্ট্রোল UI
export function TablePagination({
table,
}: {
table: Table<Product>;
}) {
const { pageIndex, pageSize } = table.getState().pagination;
const pageCount = table.getPageCount();
const visible = table.getRowModel().rows.length;
const from = visible === 0 ? 0 : pageIndex * pageSize + 1;
const to = pageIndex * pageSize + visible;
const total = table.getFilteredRowModel().rows.length;
return (
<div className="flex items-center justify-between border-t p-3">
<p className="text-sm text-muted-foreground">
{from}–{to} / {total}টি
</p>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon-sm"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
aria-label="প্রথম পেজ"
>
<ChevronsLeft className="size-4" />
</Button>
<Button
variant="outline"
size="icon-sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
aria-label="আগের পেজ"
>
<ChevronLeft className="size-4" />
</Button>
<span className="px-2 text-sm">
পেজ {pageIndex + 1} / {Math.max(pageCount, 1)}
</span>
<Button
variant="outline"
size="icon-sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
aria-label="পরের পেজ"
>
<ChevronRight className="size-4" />
</Button>
<Button
variant="outline"
size="icon-sm"
onClick={() => table.setPageIndex(pageCount - 1)}
disabled={!table.getCanNextPage()}
aria-label="শেষ পেজ"
>
<ChevronsRight className="size-4" />
</Button>
</div>
<Select
value={String(pageSize)}
onValueChange={(v) => table.setPageSize(Number(v))}
>
<SelectTrigger className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[10, 20, 30, 50].map((size) => (
<SelectItem key={size} value={String(size)}>
{size}টি
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
);
}getCanNextPage — button কেন disabled
getPaginationRowModel() page count হিসাব করে current page-এর দিকে তাকিয়ে। তাই getCanNextPage()-এর উপর নির্ভর করে button disable করলে মিথ্যা চাপলেও কিছু হবে না।
table.getCanPreviousPage() // pageIndex > 0
table.getCanNextPage() // pageIndex < pageCount - 1
table.getPageCount() // মোট কয়টি পেজ
table.getState().pagination // { pageIndex, pageSize }
// সরাসরি নেভিগেশন
table.setPageIndex(3);
table.nextPage();
table.previousPage();
table.firstPage();
table.lastPage();
// page size
table.setPageSize(50);
table.resetPageIndex(); // মোট অবস্থা রিসেটমাঝে মাঝে কোনো ফিল্টার/সার্চে পেজ ৫-এ থেকে lastPage-এ যেতে চাইলে table.setPageIndex(Number.MAX_SAFE_INTEGER) দিলে table নিজে শেষ পেজে নিয়ে যাবে। শর্ত: manual mode নয় এবং rowCount ঠিক থাকতে হবে।
off-by-one — সবচেয়ে বড় ভুল
// UI তে দেখাতে হলে
const label = pageIndex + 1; // 0 → "1"
// API-তে পাঠাতে হলে
const apiPage = pageIndex + 1; // 0 → 1 (১-ভিত্তিক API)
// ভুল
const apiPage = pageIndex; // 0 → 0 ← প্রথম request-এ 0 পাঠাবে!| জায়গা | মান রাখার নিয়ম | উদাহরণ |
|---|---|---|
state.pagination.pageIndex | 0-ভিত্তিক | প্রথম পেজ = 0 |
| UI লেখা | + 1 | "পেজ 1" |
?page= query param | + 1 | ?page=1 |
| row গণনা শুরু | pageIndex * pageSize | 0, 20, 40… |
| API response-এ | সাধারণত 1-ভিত্তিক | { page: 1, total: 1284 } |
// ৩য় পেজে কোন row দেখাবে
const pageIndex = 2;
const pageSize = 20;
const firstRowOnPage = pageIndex * pageSize; // 40 (0-ভিত্তিক index)
const displayNumber = firstRowOnPage + 1; // 41 (মানুষ যা দেখে)
const lastRowOnPage = (pageIndex + 1) * pageSize; // 60Page size বদলালে pageIndex কী হয়
// pageSize 10 → 50 করলে
// pageIndex: 4 (5ম পেজ)
// → নতুন 50টির পেজে 4 আর নেই
// TanStack অটো-শুধু করে না যদি manual না হয়, কিন্তু যাচাই করা ভালো
const handlePageSizeChange = (size: number) => {
setPagination((old) => {
const maxIndex = Math.max(0, Math.ceil(total / size) - 1);
return {
pageSize: size,
pageIndex: Math.min(old.pageIndex, maxIndex),
};
});
}Filter বদলালে প্রথম পেজে ফেরা
Client mode-এ TanStack নিজেই autoResetPageIndex চালু রাখে — filter/sort বদলালে pageIndex 0 হয়ে যায়। কিন্তু manualPagination: true দিলে এটা নিজে থেকেই বন্ধ হয়ে যায়, কারণ server-এর পেজ গণনা table-এর জানা নেই। তখন এটা আপনার দায়িত্ব।
useEffect(() => {
setPagination((old) =>
old.pageIndex === 0 ? old : { ...old, pageIndex: 0 },
);
}, [search, category, sortKey, sortOrder]);old ফেরত দিলে state বদলায় না, তাই অপ্রয়োজনীয় re-render ও হয় না। এটা একটা খুব গুরুত্বপূর্ণ কৌশল — শর্তে old ফেরত দিলে React state unchanged ধরে নেয়।
rowCount — server mode-এর জন্য
useReactTable({
data: data?.items ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
rowCount: data?.total ?? -1, // ← মোট কত row আছে
});rowCount ছাড়া table ধরে নেয় মাত্র data-র দৈর্ঘ্য — অর্থাৎ বর্তমান পেজের ২০টি। ফলে getPageCount() = ১, আর getCanNextPage() সবসময় false।
| rowCount | মানে |
|---|---|
1234 | মোট ১২৩৪টি row জানা আছে |
-1 (default) | হিসাব করা হবে না, exact মান নেই |
undefined | data.length ধরে নেওয়া হবে |
যেসব API পরের পেজ আছে কিনা বলতে পারে (hasMore) সেখানে rowCount: -1 সঠিক — তখন শেষ পেজে getCanNextPage() দিয়ে নিয়ন্ত্রণ করুন।
API-তে পাঠানো
export function ProductsTable() {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [sorting, setSorting] = useState<SortingState>([]);
const params = {
page: pagination.pageIndex + 1, // ← 0 → 1
pageSize: pagination.pageSize,
sortBy: sorting[0]?.id ?? "createdAt",
sortOrder: sorting[0]?.desc ? "desc" : "asc",
};
const { data, isPending, isFetching } = useProducts(params);
const table = useReactTable({
data: data?.items ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
state: { pagination, sorting },
onPaginationChange: setPagination,
onSortingChange: setSorting,
manualSorting: true,
manualPagination: true,
rowCount: data?.total ?? -1,
});
}এই সেতু — table-এর state থেকে query param, আর server-এর response থেকে row — এটাই server-side table-এর মূল। বিস্তারিত অধ্যায় ১১-এ।
সাধারণ বাগ
| ঘটনা | কারণ | সমাধান |
|---|---|---|
| ০ পেজ দেখায় | pageIndex সরাসরি API-তে | + 1 করুন |
| ১ পেজের ডেটা দুবার | UI-তে + 1 ভুলে দেন | UI-তে pageIndex + 1 |
| শেষ পেজে "Next" সক্রিয় | rowCount দেওয়া হয়নি | API-র total পাঠান |
| filter করলে খালি পেজ | manual mode-এ auto reset বন্ধ | useEffect-এ reset |
| pageSize বদলালে last page | অপ্রয়োজনীয় index | Math.min দিয়ে clamp |
getPaginationRowModel দেওয়া হয়নি | সব row একসাথে | যোগ করুন |
| পুরো row দেখাচ্ছে | manual mode-এ model দেওয়া আছে | model বাদ দিন |
manualPagination: true দেওয়া আছে আর getPaginationRowModel()-ও আছে — এই দুটো একসাথে বিপরীত কাজ করে। getRowModel() ফিরিয়ে দেয় data-কেই (slice করা), আর button-গুলো data.length ধরে হিসাব করে, ফলে UI অসামঞ্জস্য হয়। manual মোডে শুধু getCoreRowModel() রাখুন।
সারসংক্ষেপ
pageIndex০-ভিত্তিক,pageSizeসাধারণ ১০/২০/৫০- UI ও API-তে সবসময়
+ 1 rowCountছাড়া page count হিসাব হয় না- manual mode-এ page reset নিজে থেকে হয় না — করে দিতে হয়
- page size বদলালে
pageIndexclamp করুন
পরবর্তী
এখন user একাধিক row বেছে নিতে পারবে। পরের অধ্যায়ে row selection — indeterminate checkbox, bulk action নির্বাচন, আর select-all-এর সূক্ষ্ম বিষয়।