Row Selection — row বাছাই
rowSelection state, checkbox কলাম, indeterminate select-all, নির্বাচিত row পড়া, bulk action, server-side select — বাংলায়।
টেবিলে সবচেয়ে ব্যবহৃত ইন্টারঅ্যাকশন — একাধিক row বেছে নিয়ে একসাথে কিছু করা: মুছে ফেলা, এক্সপোর্ট, বাল্ক স্টক আপডেট।
state — অবজেক্ট, অ্যারে নয়
// rowSelection state এর আকার
type RowSelectionState = Record<string, boolean>;
// {
// "p-01": true,
// "p-04": true,
// "p-09": true,
// }
//
// ⚠️ key হলো row.id — তাই getRowId না দিলে index ("0", "1") হয়ে যায়key হলো row.id — এটাই সবচেয়ে জরুরি কথা। getRowId: (row) => row.id না দিলে id হয় index, আর index filter/sort বদলালে বদলে যায়। তখন selection ভেঙে যায় — ভুল row বেছে নেওয়া হয়।
সেটআপ
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
enableRowSelection: true, // default true, বন্ধ করতে চাইলে false
state: { rowSelection },
onRowSelectionChange: setRowSelection,
getRowId: (row) => row.id, // ← এটা না দিলে selection ভাঙবে
});এখানে চারটি row model দিয়েছি কারণ select-all checkbox বলতে চায় "এই পেজের সব" নাকি "সব filter করা row"। Model না থাকলে পার্শ্বপ্রতিক্রিয়াগুলো ভুল হয়।
Checkbox কলাম
import { Checkbox } from "@/components/ui/checkbox";
import type { ColumnDef } from "@tanstack/react-table";
export const selectColumn: ColumnDef<Product> = {
id: "select",
header: ({ table }) => {
const allSelected = table.getIsAllPageRowsSelected();
const someSelected = table.getIsSomePageRowsSelected();
return (
<Checkbox
aria-label="সব row নির্বাচন"
checked={allSelected ? true : someSelected ? "indeterminate" : false}
onCheckedChange={(checked) =>
table.toggleAllPageRowsSelected(checked === true)
}
/>
);
},
cell: ({ row }) => (
<Checkbox
aria-label={`${row.original.name} নির্বাচন`}
checked={row.getIsSelected()}
onCheckedChange={(checked) => row.toggleSelected(checked === true)}
/>
),
// এই কলামটা সাজানো/লুকানো হবে না
enableSorting: false,
enableHiding: false,
}checked-এ তিনটা অবস্থা দেওয়া যায়: true, false, আর "indeterminate" — অর্থাৎ "কিছু বেছে নেওয়া হয়েছে কিন্তু সব নয়"। shadcn-এর Checkbox এটা সামলায়।
const columns: ColumnDef<Product>[] = [
selectColumn, // ← সবসময় প্রথমে
{ accessorKey: "name", header: "পণ্যের নাম" },
// … বাকি কলাম
];তিন ধরনের select-all
এটা সবচেয়ে বেশি ভুল হওয়া জায়গা। "সব" বলতে কী বোঝায় — পেজের সব, নাকি পুরো filter করা ডেটার সব?
| method | কী করে | কখন |
|---|---|---|
getIsAllPageRowsSelected() | বর্তমান পেজের সব বেছে আছে কিনা | সাধারণ সবচেয়ে বেশি ব্যবহৃত |
getIsSomePageRowsSelected() | পেজের কিছু বেছে আছে | indeterminate দেখাতে |
getIsAllRowsSelected() | সব row বেছে আছে (সব পেজ মিলিয়ে) | "সব ফলাফল বেছে নিন" লিংক দেখাতে |
header: ({ table }) => {
const all = table.getIsAllPageRowsSelected();
const some = table.getIsSomePageRowsSelected();
return (
<div className="flex items-center gap-2">
<Checkbox
checked={all ? true : some ? "indeterminate" : false}
onCheckedChange={(v) =>
table.toggleAllPageRowsSelected(v === true)
}
/>
{/* সব পেজ ধরে বেছে নিতে চাইলে লিংক */}
{some && !all && table.getFilteredRowModel().rows.length > table.getRowModel().rows.length && (
<Button
variant="link"
size="sm"
className="h-auto p-0 text-xs"
onClick={() => table.toggleAllRowsSelected(true)}
>
সব {table.getFilteredRowModel().rows.length}টি বেছে নিন
</Button>
)}
</div>
);
}}Gmail/Mail-এও ঠিক এভাবেই হয়: পেজের সব বেছে নিলে checkbox tick হয়, কিন্তু অন্য পেজে কিছু বাকি থাকলে একটা ছোট লিংক দেখায় — "সব ১২৮৪টি বেছে নিন"। ছোট ডেটায় এটা ব্যবহার না করলেও চলে, কিন্তু user কী বেছে নিচ্ছেন তা পরিষ্কার থাকে।
নির্বাচিত row পড়া
// রেন্ডার করা (যেমন selection-এর উপর ভিত্তি করে style)
row.getIsSelected() // boolean
row.getToggleSelectedHandler()
// পুরো টেবিল থেকে
const selectedRows = table.getSelectedRowModel().rows; // Row<Product>[]
const selectedIds = table.getSelectedRowModel().flatRows.map(
(row) => row.id,
);
// state সরাসরি পড়া
Object.keys(table.getState().rowSelection); // ⚠️ false ও থাকতে পারে
// ✅ নির্ভরযোগ্য উপায়
const ids = table
.getSelectedRowModel()
.rows.map((row) => row.original.id);Object.keys(state.rowSelection) ব্যবহার করবেন না — TanStack খালি করার সময় { "p-01": false } রেখে দিতে পারে, ফলে অচেনা id চলে যায়। getSelectedRowModel() সবসময় পরিষ্কার ফলাফল দেয়।
Row selection-এর উপর effect
useEffect(() => {
const ids = table.getSelectedRowModel().rows.map((r) => r.original.id);
setSelectedCount(ids.length);
}, [table.getState().rowSelection]);effect-এর ভেতরে getSelectedRowModel() না পড়ে state থেকে হিসাব করাই নিরাপদ — প্রতিটি render-এ নতুন array তৈরি হওয়ায় effect বারবার চলতে।
Bulk action বার
const selectedRows = table.getSelectedRowModel().rows;
const count = selectedRows.length;
{count > 0 && (
<div className="flex items-center gap-3 border-b bg-muted/40 px-4 py-2">
<span className="text-sm font-medium">
{count}টি নির্বাচিত
</span>
<div className="ml-auto flex gap-2">
<Button
variant="outline"
size="sm"
onClick={async () => {
const ids = selectedRows.map((r) => r.original.id);
await deleteProducts(ids);
table.resetRowSelection();
}}
>
মুছে ফেলুন
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => table.resetRowSelection()}
>
বাতিল
</Button>
</div>
</div>
)}// নির্বাচিত সব বাদ দিতে চাইলে
row.toggleSelected(false);
// ঐ পেজের সব বাদ
table.toggleAllPageRowsSelected(false);
// পুরো ফলাফল বাদ
table.resetRowSelection();শুধু কিছু কলামে select করা যায়
{
accessorKey: "name",
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onCheckedChange={(v) => row.toggleSelected(v === true)}
/>
),
}
// কোনো শর্তে select বন্ধ
const table = useReactTable({
// …
enableRowSelection: (row) => row.original.isActive, // inactive select করা যাবে না
});Server-side selection
অনেক অ্যাপে একটি পেজের বেছে নেওয়া কাজ নেই — মোট নির্বাচিতের সংখ্যা দেখাতে হয়, আর সব পরিবর্তন API-তে যায়।
// নির্বাচিত id গুলো URL-এ
useEffect(() => {
const ids = table.getSelectedRowModel().rows.map((r) => r.original.id);
const params = new URLSearchParams();
if (ids.length) params.set("ids", ids.join(","));
router.replace(`?${params.toString()}`, { scroll: false });
}, [table.getState().rowSelection]);getIsAllRowsSelected() + toggleAllRowsSelected(true) ব্যবহার করলে selection state-এ প্রতিটি row-এর id চলে আসে — ১০,০০০ row হলে ১০,০০০ key। খুব বড় ডেটায় বদলে একটা পতাকা ("all matching: true) রাখুন, আর mutation-এ সেটা ব্যবহার করে queryKey-এর সাথে মিলিয়ে server-এ বুঝিয়ে দিন।
সাধারণ বাগ
| ঘটনা | কারণ | সমাধান |
|---|---|---|
| সাজালে selection চলে যায় | getRowId নেই | getRowId: (row) => row.id |
| পেজ বদলালে selection থেকে যায় | এটাই আসলে চাওয়া | getRowModel().rows দিয়ে render |
| checkbox-এ indeterminate দেখায় না | checked তিন অবস্থায় দেননি | "indeterminate" pass করুন |
| filter করলে selection চলে যায় | expected behaviour | table.resetRowSelection() |
| মুছে ফেলার পরে বাকি row-ও হারায় | অপ্রয়োজনীয় re-render | getRowId ঠিক আছে কিনা দেখুন |
| selected count ভুল | Object.keys(state) | getSelectedRowModel() ব্যবহার করুন |
| header checkbox কাজ করে না | onCheckedChange এ checked সরাসরি দিয়েছেন | checked === true করে toggle* কল |
সারসংক্ষেপ
rowSelectionহলোRecord<rowId, boolean>— তাইgetRowIdজরুরি- header checkbox-এ তিনটা অবস্থা:
true/"indeterminate"/false - "সব" মানে পেজের সব, নাকি পুরো ফলাফলের সব — স্পষ্ট করে নিন
- নির্বাচিত row পড়তে
getSelectedRowModel(),Object.keysনয়
পরবর্তী
এতক্ষণ কলামগুলো সবসময় দেখা যাচ্ছিল। পরের অধ্যায়ে column management — visibility, order, pinning (sticky কলাম) আর width resize।