Tech TutorBangla Web Documentation
Portfolio
HomeHTMLCSSTailwind CSSJavaScriptTypeScriptReactNext.jsNode.jsExpressSocket.IOExpoWordPressPostgreSQLPrismaPostgresNestJSMongoDBDockerTanStackTanStack TableRTK QueryGitGitHub
  • TanStack Documentation42টি অধ্যায়
প্রযুক্তি নির্বাচন
HomeHTMLCSSTailwind CSSJavaScriptTypeScriptReactNext.jsNode.jsExpressSocket.IOExpoWordPressPostgreSQLPrismaPostgresNestJSMongoDBDockerTanStackTanStack TableRTK QueryGitGitHub
  • Project Overview
  • Architecture
  • Folder Structure
  • Setup Next.js
  • Axios Setup
  • API Client
  • API Types
  • Query Install
  • Query Provider
  • Query Keys
  • Data Fetching
  • States
  • Devtools
  • Create Mutation
  • Update Mutation
  • Delete Mutation
  • Cache Invalidation
  • Optimistic Update
  • Pagination
  • Search
  • Filtering
  • Sorting
  • TanStack Table
  • Table Explore
  • Table Columns
  • Table Actions
  • React Hook Form
  • Zod Validation
  • Create Form
  • Edit Form
  • Dialog Modal
  • Delete Confirmation
  • Complete CRUD Flow
  • Full Integration
  • Error Handling
  • Loading States
  • Reusable Components
  • Production
  • Complete Project
  • Common Mistakes
  • Best Practices
  • Interview Q&A
  1. Docs
  2. TanStack
  3. TanStack Table — গভীরে (Explore)
TanStackAdvanced

TanStack Table — গভীরে (Explore)

row model, client-side ও server-side mode, controlled state, rowCount, column visibility ও row selection — বাংলায়।

51টি অংশবাংলা ভাষায়

আগের অধ্যায়গুলো table-এর মূল শিখিয়েছিল — data, columns আর getCoreRowModel। এই অধ্যায়ে সেই মূলের উপরে থাকা বাকি সব স্তর: data কীভাবে ভেতরে ভেতরে ঢোকে, sorting/filtering/pagination কখন table-এর নিজের কাজ আর কখন server-এর, এবং selection/column visibility-র মতো feature কীভাবে যোগ হয়।

লাইভ ডেমো: TanStack Table + shadcn + Query পেজে একই ডেটা দুইভাবে দেখানো হয়েছে — একটা পুরোপুরি client-side, আরেকটা TanStack Query-ও route handler-যুক্ত server-side। অধ্যায়ের প্রতিটি কোড block-ই সেই ডেমোর প্রকৃত কোড।

এই অধ্যায়ে কী কী নতুন

আগের অধ্যায়ে ছিলএই অধ্যায়ে যোগ হচ্ছে
getCoreRowModelgetSortedRowModel, getFilteredRowModel, getPaginationRowModel
server-side sort/pagination (শুধু query param)manualSorting / manualPagination / rowCount — দুই পাশের সংযোগ
key={row.id} (index-based id)getRowId — স্থায়ী row id
শুধু data দেখানোrow selection, column visibility, empty state

Row model — data ভেতরে যায় কীভাবে

TanStack Table ডেটাকে একবারে রেন্ডার করে না। এটি একটা pipeline — প্রতিটি row model আগেরটার উপর থেকে কাজ করে, তাই সাজানোর ক্রম গুরুত্বপূর্ণ।

Row model pipelinetext
text
data (raw)
  ↓  getCoreRowModel      → মূল row তৈরি
  ↓  getFilteredRowModel  → search/filter মেরে বাকি
  ↓  getSortedRowModel    → সাজানো
  ↓  getPaginationRowModel → কয়টি row দেখাবে
table.getRowModel().rows

শেষেরটা getRowModel() — যা রেন্ডারে ব্যবহার হয়। কিন্তু প্রতিটা ধাপের নিজস্ব accessor আছে: filtered-এর জন্য getFilteredRowModel(), sorted-এর জন্য `getSortedRowModel()।

row modelকী করেstate
getCoreRowModel()মূল row বানায়—
getFilteredRowModel()filter মেরে কমায়globalFilter, columnFilters
getSortedRowModel()সাজায়sorting
getPaginationRowModel()ছেঁকে নেয়pagination

Client-side — পুরো সেটআপ

ছোট ডেটা (কয়েকশো row) হলে সবকিছু browser-এ করানোই সবচেয়ে সহজ। চারটা row model দিলেই table নিজেই filter, sort ও paginate করে — কোনো server, কোনো query লাগে না।

product-table.tsxtsx
tsx
"use client";

import { useState } from "react";
import {
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
  type SortingState,
} from "@tanstack/react-table";

export function ProductTable({ data, columns }: ProductTableProps) {
  // যেসব state আমরা চাই, ওগুলো এখানে ধরে দিই
  const [sorting, setSorting] = useState<SortingState>([
    { id: "price", desc: true },
  ]);
  const [globalFilter, setGlobalFilter] = useState("");

  const table = useReactTable({
    data,
    columns,
    getRowId: (row) => row.id,
    state: { sorting, globalFilter },
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    initialState: { pagination: { pageSize: 10 } },
  });
}

state দিয়ে যে slice-গুলো ধরে দিলাম, onXYChange handler-ই সেগুলোর মালিক — table নিজে বদলায় না। বাকি slice (যেমন rowSelection`) table নিজের মধ্যেই সামলায়। এটাই controlled ও uncontrolled state-এর ভাগাভাগি।

রেন্ডার অংশ — আগেরটির মতোইtsx
tsx
<Table>
  <TableHeader>
    {table.getHeaderGroups().map((headerGroup) => (
      <TableRow key={headerGroup.id}>
        {headerGroup.headers.map((header) => (
          <TableHead key={header.id}>
            {header.isPlaceholder
              ? null
              : flexRender(
                  header.column.columnDef.header,
                  header.getContext()
                )}
          </TableHead>
        ))}
      </TableRow>
    ))}
  </TableHeader>
  <TableBody>
    {table.getRowModel().rows.map((row) => (
      <TableRow key={row.id}>
        {row.getVisibleCells().map((cell) => (
          <TableCell key={cell.id}>
            {flexRender(
              cell.column.columnDef.cell,
              cell.getContext()
            )}
          </TableCell>
        ))}
      </TableRow>
    ))}
  </TableBody>
</Table>

রেন্ডার কোড আগের অধ্যায়ের হুবহু — কারণ headless মানেই এটাই: row model বদলালেও মার্কআপে হাত দিতে হয় না।

Server-side — manual mode

আগের pagination ও sorting অধ্যায়ে sort state Query-এর উপরে ছিল, table-এর বাইরে। ওটাই সঠিক উপায়, কিন্তু ফলে table কিছুই জানে না — যেমন মোট কত পেজ, এখন কোন পেজে আছি। manual mode এই সেতুটাই তৈরি করে।

manual modetsx
tsx
const table = useReactTable({
  data: data?.items ?? [],
  columns: productColumns,
  getRowId: (row) => row.id,

  // state table-এর, কিন্তু খোলা (controlled)
  state: { sorting, pagination },
  onSortingChange: setSorting,
  onPaginationChange: setPagination,

  // "আমি নিজে transform করব না" — শুধু state রাখব
  manualSorting: true,
  manualFiltering: true,
  manualPagination: true,

  // মোট কত row আছে (API থেকে) — page count হিসাবের জন্য
  rowCount: data?.total ?? -1,

  // এখন শুধু core যথেষ্ট — বাকি model দিলে কাজ হতো না
  getCoreRowModel: getCoreRowModel(),
});
কীmanual mode-এ কী হয়
manualSortingheader-এ ক্লিক করলে state বদলায়, কিন্তু row সাজায় না
manualFilteringfilter state বদলায়, কিন্তু filter করে না
manualPaginationpage বদলায়, কিন্তু slice করে না
rowCountgetPageCount() এটা থেকে হিসাব হয়
row model দিয়ে সাজাবেন না

manual mode-এ getSortedRowModel() বা getPaginationRowModel() দিলে দুটো কাজ করেই যাবে না — manual flag-ই তাকে উপেক্ষা করে। সেই অবস্থায় সাজানোর দায়িত্ব queryKey-র উপরে।

State উপরে উঠানো — table থেকে Query

এখন table-এর state (sorting, pagination) থেকে Query-এর params বানাই। State যেই না-ই বদলাক, queryKey বদলাবে, আর নতুন page আসবে — এটাই দুই লাইব্রেরির মাঝের সেতু।

dashboard-এ state থেকে paramstsx
tsx
const [sorting, setSorting] = useState<SortingState>([
  { id: "createdAt", desc: true },
]);
const [pagination, setPagination] = useState<PaginationState>({
  pageIndex: 0,
  pageSize: 10,
});

// table-এর 0-based pageIndex → API-র 1-based page
const params: ProductQueryParams = {
  page: pagination.pageIndex + 1,
  pageSize: pagination.pageSize,
  search: search || undefined,
  sortBy: sorting[0]?.id as ProductQueryParams["sortBy"],
  sortOrder: sorting[0]?.desc ? "desc" : "asc",
};

const { data, isPending, isFetching } = useProducts(params);

০ vs ১ এই অফসেটটা সবচেয়ে বেশি ভুল হয়: table pageIndex 0 থেকে গোনে, আমাদের API 1 থেকে। তাই + 1 লাগেই।

Filter বদলালে প্রথম পেজে ফেরা

manualPagination: true দিলে table নিজে আর page reset করে না (autoResetPageIndex নিজে থেকেই false হয়ে যায়)। ফলে page ৫-এ থেকে filter বদলালে খালি পেজ পেতেন। তাই reset-টা আমাদের দায়িত্ব।

page resettsx
tsx
useEffect(() => {
  setPagination((old) =>
    old.pageIndex === 0 ? old : { ...old, pageIndex: 0 },
  );
}, [search, category, status, sorting]);

old ফেরত দিলে state বদলায় না, তাই অপ্রয়োজনীয় re-render-ও হয় না।

getRowId — স্থায়ী row id

row.id ডিফল্টভাবে row-এর index। ডেটা সাজালে বা pagination বদলালে index বদলে যায় — ফলে selection ভেঙে যায়, checkbox মিসম্যাচ হয়, animation ঝাঁপায়। getRowId সেটা ঠিক করে।

getRowIdtsx
tsx
// ছাড়া: row.id = "0", "1", "2"  → সাজালেই বদলে যায়
getRowId: (row) => row.id,   // "p-01" — অপরিবর্তনীয়
কখন বসাবেন

row selection, column pinning, expanded state বা server-side data — এগুলোর যেকোনো একটা থাকলেই getRowId বসান। ব্যবহার না হলেও বসিয়ে রাখলে কোনো ক্ষতি নেই, শুধু একটা option।

Column visibility

কোন কলাম দেখাবেন সেটাও একটা state — columnVisibility। এটার জন্য কোনো ফাইলে ছোঁয় না লাগে; শুধু getAllLeafColumns() ঘুরে প্রতিটির checkbox বানিয়ে toggleVisibility কল করলেই হয়।

column-visibility.tsxtsx
tsx
export function ColumnVisibility({ table }: { table: Table<unknown> }) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="outline" size="sm" />}>
        কলাম
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        {table
          .getAllLeafColumns()
          .filter((column) => column.getCanHide())
          .map((column) => (
            <DropdownMenuCheckboxItem
              key={column.id}
              checked={column.getIsVisible()}
              onCheckedChange={(checked) =>
                column.toggleVisibility(checked)
              }
            >
              {column.id}
            </DropdownMenuCheckboxItem>
          ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

লুকানো কলাম আর row.getVisibleCells()-এ আসেই না — তাই rendering কোডে কোনো শর্ত লিখতে হয় না। getCanHide() false হলে (যেমন checkbox কলাম) সেটি তালিকায় আসবে না।

Row selection

Selection-এর প্রতিটি ধাপ table-ই সামলায়: row.getIsSelected(), row.toggleSelected(), আর পুরো পেজের জন্য table.getIsAllPageRowsSelected() / getIsSomePageRowsSelected()।

checkbox ColumnDeftsx
tsx
{
  id: "select",
  header: ({ table }) => {
    const all = table.getIsAllPageRowsSelected();
    const some = table.getIsSomePageRowsSelected();
    return (
      <Checkbox
        aria-label="সব row নির্বাচন"
        checked={all}
        indeterminate={!all && some}
        onCheckedChange={(checked) =>
          table.toggleAllPageRowsSelected(checked)
        }
      />
    );
  },
  cell: ({ row }) => (
    <Checkbox
      aria-label={`${row.original.name} নির্বাচন`}
      checked={row.getIsSelected()}
      onCheckedChange={(checked) => row.toggleSelected(checked)}
    />
  ),
  enableSorting: false,
  enableHiding: false,
}

এটি actions কলামের মতোই display column — accessorKey নেই, শুধু id। আর খেয়াল করুন: row.original.name-এর বদলে row.id ব্যবহার করা উচিত ছিল, কারণ id অপরিবর্তনীয় — getRowId না দিলে row.id মানে index।

selection পড়াtsx
tsx
// নির্বাচিত row-এর কাজ (bulk delete ইত্যাদি)
const selectedIds = Object.keys(table.getState().rowSelection);
const selectedRows = table.getSelectedRowModel().rows;

Empty state ও loading

rows.length === 0 হলে একটা পুরো TableRow + colSpan দিয়ে খালি বার্তা দেখান — এটা না করলে table header-সহ ফাঁকা থেকে যায়, যা দেখতে ভাঙা মনে হয়।

empty rowtsx
tsx
{rows.length === 0 ? (
  <TableRow>
    <TableCell
      colSpan={table.getVisibleLeafColumns().length}
      className="h-24 text-center text-muted-foreground"
    >
      কোনো product মেলেনি।
    </TableCell>
  </TableRow>
) : (
  rows.map((row) => <TableRow key={row.id}>…</TableRow>)
)}

Server mode-এ loading-এর জন্য row নয়, cell-এর ভেতরে Skeleton বসান — তাহলে header ও কলামের স্থান নড়ে না। আর isFetching (বারবার refetch) আর isPending (প্রথমবার) আলাদা — দুটোর জায়গা আলাদা।

loading skeletontsx
tsx
{isPending
  ? Array.from({ length: 5 }, (_, i) => (
      <TableRow key={i}>
        {table.getVisibleLeafColumns().map((column) => (
          <TableCell key={column.id}>
            <Skeleton className="h-4 w-full" />
          </TableCell>
        ))}
      </TableRow>
    ))
  : rows.map((row) => …)}

এক নজরে — কোন feature-এ কী লাগে

কাজoptionযা পড়বেন
সাজানোgetSortedRowModelgetSortedRowModel()
খুঁজে বের করাgetFilteredRowModelgetFilteredRowModel()
পেজ ভাগgetPaginationRowModelgetRowModel()
সার্ভারে সাজানোmanualSortingsorting[0]?.id → queryKey
সার্ভারে পেজmanualPagination + rowCountgetPageCount()
স্থায়ী idgetRowIdrow.id
কলাম লুকানোcolumnVisibilitygetVisibleCells()
row বাছাইrowSelectiongetSelectedRowModel()

সাধারণ ভুল

ভুলফলসমাধান
manualPagination + getPaginationRowModel দুটোইকাজ করে নাmanual mode-এ row model দেবেন না
rowCount না দেওয়ামোট ১ পেজ দেখায়API-র total পাঠান
getRowId না দেওয়াসাজালে selection ভাঙেgetRowId: (row) => row.id
filter বদলেও pageIndex ০ না করাখালি পেজuseEffect-এ reset
pageIndex সরাসরি API-তেoff-by-one+ 1 করে পাঠান
colSpan={columns.length}লুকানো কলাম থাকলে ভাঙাtable.getVisibleLeafColumns().length
key={index} বা default row.idre-render বাগgetRowId
headless মানে দায়িত্বও আপনার

TanStack Table state ও logic দেয়, markup-ওর দায়িত্ব আপনার — shadcn-র Table, TableHead ছাড়াও loading, empty state, এবং aria-label সব আপনাকেই লিখতে হবে। উপরের ডেমোতে ঠিক সেটাই করা হয়েছে: গুছানো shadcn markup + headless state।

শেষ প্যারায় নিজে practice করুন — কোড কপি করে নিজের এডিটরে চালিয়ে দেখুন।
আগেরTanStack Table — Headless PowerপরেরTable Columns — Definitions
এই পেজে
Tech Tutor

বাংলায় ওয়েব ডেভেলপমেন্ট ডকুমেন্টেশন শিখুন — ধাপে ধাপে।

Homeসব ডকুমেন্টেশনQuick Startআমার নোটস

© 2026 Tech Tutor. বাংলায় শিখুন, বাংলায় গড়ুন।

mdanamolhasan1998@gmail.com