ColumnDef — কলাম সংজ্ঞা
accessorKey বনাম accessorFn বনাম id, header/cell রেন্ডার, display column, enable* অপশন, meta, columnHelper — বাংলায়।
টেবিলের প্রতিটি কলাম একটি ColumnDef অবজেক্ট। এই একটি অবজেক্টই বলে দেয় কোন ডেটা দেখাবে, কীভাবে format করবে, আর কোন কাজগুলো চালু থাকবে।
তিনটি উপায়ে ডেটা ঠিক করা
import type { ColumnDef } from "@tanstack/react-table";
const columns: ColumnDef<Product>[] = [
// ১. accessorKey — ডেটার key-র নামই id
{ accessorKey: "name", header: "নাম" },
// ২. accessorFn — হিসাব করে মান বানানো
{
id: "fullName",
accessorFn: (row) => `${row.name} (${row.stock}টি)`,
header: "বিবরণ",
},
// ৩. display column — ডেটা নেই, শুধু UI
{
id: "actions",
header: "অ্যাকশন",
cell: ({ row }) => <RowActions id={row.original.id} />,
},
];| উপায় | id কোথা থেকে | কখন ব্যবহার |
|---|---|---|
accessorKey | key-র নাম নিজে থেকেই | সরাসরি একটা ফিল্ড দেখালে |
accessorFn | id আলাদা লিখতে হয় | জোড়া লাগা বা গণনা করা ডেটা |
id + cell | id আলগা করে | বাটন, checkbox, badge |
id ছাড়া শুধু accessorFn লিখলে কম্পাইলার এরর দেবে — id বলে দিতে হয় কোন কলামের কথা বলছেন।
কলামের id = sorting/filtering state-এর key, URL-এর query param, আর getRowId-এর মতোই। তাই id বা accessorKey বদলানোর পর লুকানো সাজানো পুরোোনো URL ভেঙে যেতে পারে। স্থায়ী রাখুন।
header — হেডার লেখা ও action
// সহজ: শুধু লেখা
{ accessorKey: "name", header: "পণ্যের নাম" }
// header-এ sorting button — সবচেয়ে common
{
accessorKey: "price",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
মূল্য
{column.getIsSorted() === "asc" && <ArrowUp className="ml-2 size-4" />}
{column.getIsSorted() === "desc" && <ArrowDown className="ml-2 size-4" />}
</Button>
),
}
// align করা
{
accessorKey: "price",
header: "মূল্য",
meta: { align: "right" },
}header-এর function-এ column instance পাওয়া যায়, তাই getIsSorted() দিয়ে তীর দেখাতে পারেন। meta.align নিজের নিয়ম — TanStack এটা চেনে না, রেন্ডারে পড়ে class বানাতে হয় (নিচে দেখানো হলো)।
// meta পড়া — মূল রেন্ডারে
<TableHead className={cn(
"px-4",
header.column.columnDef.meta?.align === "right" && "text-right",
)}>cell — মান render করা
// accessorKey + cell একসাথে
{
accessorKey: "price",
header: "মূল্য",
cell: ({ getValue }) => {
const price = getValue<number>(); // ← টাইপ জানানো
return (
<span className="font-medium tabular-nums">
{new Intl.NumberFormat("bn-BD", {
style: "currency",
currency: "BDT",
maximumFractionDigits: 0,
}).format(price)}
</span>
);
},
}
// অন্য কলামের ডেটা দরকার হলে
{
id: "status",
accessorFn: (row) => (row.stock === 0 ? "out" : "in"),
cell: ({ row, getValue }) => (
<Badge variant={getValue() === "out" ? "destructive" : "default"}>
{getValue() === "out" ? "স্টক শেষ" : row.original.name}
</Badge>
),
}cell function-এর getValue() সবসময় এই কলামের accessor-এর ফলাখানা। যদি accessorKey না থাকে (display column), তাহলে getValue() undefined দেয় — সেখানে row.original থেকে নিন।
নিজস্ব cell component
বড় টেবিলে cell function-এর ভেতরে লজিক রাখলে columns.tsx পড়া কঠিন হয়ে যায়। আলাদা ফাইলে component লিখে রেখে সেটা রেফার করুন।
// components/data-table/cell/price-cell.tsx
export function PriceCell({ value }: { value: number }) {
const format = new Intl.NumberFormat("bn-BD", {
style: "currency",
currency: "BDT",
maximumFractionDigits: 0,
});
return (
<span className="font-medium tabular-nums">{format.format(value)}</span>
);
}
// columns.tsx
{ accessorKey: "price", cell: ({ getValue }) => <PriceCell value={getValue<number>()} /> }enable* — কোন কাজ চালু থাকবে
const columns: ColumnDef<Product>[] = [
{
accessorKey: "name",
enableSorting: false, // এ কলাম সাজানো যাবে না
enableHiding: false, // column visibility মেনুতে আসবে না
enableGlobalFilter: false, // global search-এ অংশ নেবে না
},
{
id: "select",
enableSorting: false,
enableHiding: false,
cell: ({ row }) => <Checkbox />,
},
{
accessorKey: "createdAt",
enableGlobalFilter: false, // ISO date global search-এ noise দেয়
},
];| option | default | false করলে |
|---|---|---|
enableSorting | true | header-এ সাজানোর তীর আসবে না |
enableHiding | true | column visibility তালিকায় থাকবে না |
enableGlobalFilter | true | global search এ কলামটা মিলবে না |
enableGrouping | true | grouping করা যাবে না |
enablePinning | true | pin/unpin করা যাবে না |
columnHelper — boilerplate কমানো
createColumnHelper দিলে accessor-এর নাম থেকেই টাইপ বের হয়ে আসে — আলাদা করে <number> লেখা লাগে না। বড় প্রজেক্টে এটা ব্যবহার করা ভালো।
import { createColumnHelper } from "@tanstack/react-table";
const helper = createColumnHelper<Product>();
export const columns = [
helper.accessor("name", {
header: "পণ্যের নাম",
}),
helper.accessor("price", {
header: "মূল্য",
cell: (info) => <PriceCell value={info.getValue()} />,
// ↑ info.getValue(): number — টাইপ ইনফার
}),
helper.display({
id: "actions",
header: "অ্যাকশন",
cell: ({ row }) => <RowActions id={row.original.id} />,
}),
];সাধারণ ColumnDef | columnHelper |
|---|---|
accessorKey: "price" | helper.accessor("price", …) |
id: "x" + accessorFn | helper.accessor("x", …) |
id: "y" + cell | helper.display({ id: "y", … }) |
getValue<number>() | info.getValue() |
একটা সম্পূর্ণ columns.tsx
import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import type { ColumnDef } from "@tanstack/react-table";
import { PriceCell } from "./cell/price-cell";
import { RowActions } from "./cell/actions-cell";
const formatDate = new Intl.DateTimeFormat("bn-BD", {
dateStyle: "medium",
});
export const columns: ColumnDef<Product>[] = [
{
accessorKey: "name",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
পণ্যের নাম
{column.getIsSorted() === "asc" && <ArrowUp className="ml-2 size-4" />}
{column.getIsSorted() === "desc" && <ArrowDown className="ml-2 size-4" />}
{!column.getIsSorted() && <ChevronsUpDown className="ml-2 size-4 opacity-50" />}
</Button>
),
cell: ({ row }) => (
<div className="flex flex-col">
<span className="font-medium">{row.original.name}</span>
<span className="text-xs text-muted-foreground">
{row.original.id}
</span>
</div>
),
},
{
accessorKey: "price",
header: ({ column }) => (
<Button
variant="ghost"
className="-ml-3"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
মূল্য
{column.getIsSorted() === "asc" && <ArrowUp className="ml-2 size-4" />}
{column.getIsSorted() === "desc" && <ArrowDown className="ml-2 size-4" />}
</Button>
),
cell: ({ getValue }) => <PriceCell value={getValue<number>()} />,
meta: { align: "right" },
},
{
accessorKey: "stock",
header: "স্টক",
cell: ({ getValue }) => {
const stock = getValue<number>();
return (
<Badge variant={stock === 0 ? "destructive" : "secondary"}>
{stock === 0 ? "শেষ" : `${stock}টি`}
</Badge>
);
},
},
{
accessorKey: "createdAt",
header: "তারিখ",
enableGlobalFilter: false,
cell: ({ getValue }) => (
<span className="text-muted-foreground">
{formatDate.format(new Date(getValue<string>()))}
</span>
),
},
{
id: "actions",
header: "অ্যাকশন",
enableSorting: false,
enableHiding: false,
cell: ({ row }) => <RowActions id={row.original.id} />,
meta: { align: "right" },
},
];checkbox আর actions-এর মতো display column-এ enableSorting: false আর enableHiding: false দুটোই দিন। না দিলে header-এ অর্থহীন সাজানোর তীর চড়ে উঠবে, আর column visibility মেনুতে "actions" নামে একটা মূর্খ item থাকবে।
সাধারণ ভুল
| ভুল | ফল | সমাধান |
|---|---|---|
accessorFn ছাড়া শুধু cell | sorting কাজ করে না | accessorKey বা accessorFn দিন |
getValue()-কে + 1 | "1850001" হয় | Number(getValue()) |
header: () => "নাম" | context পেলেও কোচাকাচা | header: "নাম" |
অ্যাকশন কলাম enableHiding: false ছাড়া | মেনুতে ভুল item | দুটোই false |
প্রতিটি রেন্ডারে new Intl.NumberFormat() | বারবার অবজেক্ট তৈরি | ফাইলের উপরে একবার |
পরবর্তী
এখন কলাম ঠিক আছে, কিন্তু ডেটা কীভাবে ভেতরে ঢোকে সেটা বোঝা বাকি। পরের অধ্যায়ে row model-এর pipeline দেখবো — core, filtered, sorted, grouped, expanded, paginated।