Tech TutorBangla Web Documentation
Portfolio
HomeHTMLCSSTailwind CSSJavaScriptTypeScriptReactNext.jsNode.jsExpressSocket.IOExpoWordPressPostgreSQLPrismaPostgresNestJSMongoDBDockerTanStackTanStack TableRTK QueryGitGitHub
  • TanStack Table Documentation12টি অধ্যায়
প্রযুক্তি নির্বাচন
HomeHTMLCSSTailwind CSSJavaScriptTypeScriptReactNext.jsNode.jsExpressSocket.IOExpoWordPressPostgreSQLPrismaPostgresNestJSMongoDBDockerTanStackTanStack TableRTK QueryGitGitHub
  • Introduction
  • Installation & Setup
  • Core Concepts
  • Column Definitions
  • Row Models
  • Sorting
  • Filtering
  • Pagination
  • Row Selection
  • Column Management
  • Server-side Mode
  • Advanced Features
  1. Docs
  2. TanStack Table
  3. Advanced Features — grouping, expanding, virtualizing
TanStack TableAdvanced

Advanced Features — grouping, expanding, virtualizing

row grouping, row expanding, faceted filter, virtualized rows, sticky header, sorting নিয়ে advanced কৌশল, accessibility ও চূড়ান্ত চেকলিস্ট — বাংলায়।

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

প্রতিদিনের কাজ শেষ। এই শেষ অধ্যায়ে সেই সব ফিচার যেগুলো মাঝে মাঝে লাগে, কিন্তু সবসময় লাগে না। না পড়লেও চলবে, পড়লে অনেক সমস্যা নিজে থেকেই সমাধান হবে।

Row grouping

একই category-র পণ্যগুলো একত্রে দেখানো। grouping state সাধারণত server-এর মতোই এখানে client-side হয়।

const [grouping, setGrouping] = useState<GroupingState>([]);

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getSortedRowModel: getSortedRowModel(),
  getGroupedRowModel: getGroupedRowModel(),   // ← এটা ছাড়া grouping হবে না
  getExpandedRowModel: getExpandedRowModel(),
  state: { grouping, expanded },
  onGroupingChange: setGrouping,
  onExpandedChange: setExpanded,
});

// group করা শুরু
table.setGrouping([{ id: "category" }]);

// aggregate
column.getAggregationFn();
row.getValue(columnId);   // group row-এ এটাই গণনীত মান

Grouping-এর জন্য কলাম প্রস্তুত

{
  accessorKey: "price",
  aggregationFn: "sum",        // built-in: sum, min, max, mean, count…
  header: "মোট মূল্য",
  cell: ({ getValue, row }) => {
    // group header row-এ aggregate দেখায়
    if (row.getIsGrouped()) {
      return (
        <span className="font-semibold">
          {formatPrice(getValue<number>())}
        </span>
      );
    }
    return formatPrice(row.original.price);
  },
}

Group header-এর মান bold দেখাতে row.getIsGrouped()-এ শর্ত দিয়ে className বদলান — ঠিক উপরের মতো। TanStack group row চেনায়, কিন্তু চেনাতেই row.getIsGrouped() আর row.getIsGroupedColumn() ব্যবহার করুন।

// group header row দেখানো
table.getRowModel().rows.map((row) => {
  if (row.getIsGrouped()) {
    return (
      <TableRow className="bg-muted/50 hover:bg-muted/50">
        {row.getVisibleCells().map((cell) => (
          <TableCell key={cell.id}>
            {flexRender(cell.column.columnDef.cell, cell.getContext())}
          </TableCell>
        ))}
      </TableRow>
    );
  }

  return (
    <TableRow className={row.getIsSubRow() ? "pl-6" : undefined}>
      {/* … */}
    </TableRow>
  );
})}
grouping-এ sorting-এর আচরণ বদলায়

grouping চালু থাকলে sorting আর আগের ক্রমে কাজ করে না — এখন সেটা group row-গুলোর ক্রম ঠিক করে। মানে sorting: [{ id: "category", desc: true }] মানে "ক্যাটাগরি অনুসারে group সাজাও", পণ্য সাজাও না।

Row expanding — sub-row

ক্যাটাগরির নিচে সেই ক্যাটাগরির পণ্য — এটা grouping; আর একটি order-এর নিচে তার line items — এটা expanding। দুটো আলাদা feature, মাঝে মাঝে গুলিয়ে যায়।

// ① sub-row ডেটা দরকার
type Order = {
  id: string;
  total: number;
  items: { id: string; name: string; qty: number }[];
};

// ② expanded state
const [expanded, setExpanded] = useState<ExpandedState>({});
// { "o-01": true, "o-05": true }

const table = useReactTable({
  data: orders,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getExpandedRowModel: getExpandedRowModel(),
  state: { expanded },
  onExpandedChange: setExpanded,

  // তিনটি জিনিস লাগে
  getSubRows: (row) => row.items,   // sub-row কোথা
  expandRows: true,                 // rendering-এ খোলা সারি
});
// একটা row খুলে বন্ধ করা
row.toggleExpanded();
row.getIsExpanded();
row.getCanExpand();
row.getLeafRows();        // সব স্তরের শেষ পর্যন্ত শুধু leaf
row.getParentRow();

// পুরোটা খুলে/বন্ধ
table.getIsAllRowsExpanded();
table.toggleAllRowsExpanded();
table.getCanExpandRows();   // একটা sub-row আছে কিনা
// expand করার বোতাম
cell: ({ row }) =>
  row.getCanExpand() ? (
    <Button
      variant="ghost"
      size="icon-sm"
      onClick={row.getToggleExpandedHandler()}
      aria-label={row.getIsExpanded() ? "ভাঙুন" : "খুলুন"}
    >
      {row.getIsExpanded()
        ? <ChevronDown className="size-4" />
        : <ChevronRight className="size-4" />}
    </Button>
  ) : (
    <span className="w-8" />   // ← সারিগুলো align না হলে ঝাঁপায়
  )

Sub-row শুধু এক স্তরের হলে সহজ। বহু স্তরের হলে indentation বাড়াতে row.depth ব্যবহার করুন।

<TableCell style={{ paddingLeft: `${row.depth * 24 + 16}px` }}>
  …
</TableCell>
getSubRows ছাড়া sub-row হয় না

getExpandedRowModel() আর expandRows: true দুটোই দিতে হবে, আর getSubRows()-এ সঠিক function-এ পাঠাতে হবে। ভুল function দিলে getCanExpand() সবসময় false দেবে — চুপচাপ, কোনো error নেই।

Faceted filter

একই সাথে দুটো filter UI দেখান — category বাছুন, তারপর price range-এর অপশন শুধু ওই category-র।

const table = useReactTable({
  // …
  getFilteredRowModel: getFilteredRowModel(),
  getFacetedRowModel: getFacetedRowModel(),   // ← এটা ছাড়া কাজ হবে না
  getFacetedUniqueValues: getFacetedUniqueValues(),
  getFacetedMinMaxValues: getFacetedMinMaxValues(),

  manualFiltering: true,   // অন্য filter manual হলে এগুলো নিজে থেকেই হবে
  state: { columnFilters },
  onColumnFiltersChange: setColumnFilters,
});
// কোন কলামে কোন অপশন বাকি আছে
table.getColumn("category")?.getFacetedUniqueValues();
// Map { electronics: 4, clothing: 3, books: 5 }

// অন্য filter বাদ দিয়ে সংখ্যা
table.getColumn("price")?.getFacetedMinMaxValues();
// [0, 200000]

Faceted model ডিফল্ট হিসেবে অন্য সব filter উপেক্ষা করে — নিজের filter বাদ দেয়, বাকিগুলোর উপর হিসাব করে। এটাই চাওয়া আচরণ।

Virtualized rows

৫০০০ row একসাথে DOM-এ বসালে browser ধাঁধায় — স্ক্রল লাগে না, মেমরি ফুরিয়ে যায়। @tanstack/react-virtual ব্যবহার করলে শুধু দৃশ্যমান অংশই DOM-এ থাকে।

pnpm add @tanstack/react-virtual
"use client";

import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";

export function VirtualizedTable({ table }: { table: Table<Product> }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const rows = table.getRowModel().rows;

  const rowVirtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 44,        // এক row-এর আনুমানিক উচ্চতা (px)
    overscan: 10,                  // উপরে-নিচে ১০টা extra row রেন্ডার
  });

  return (
    <div
      ref={parentRef}
      className="h-[600px] overflow-auto"   // ← উচ্চতা দরকার
    >
      <table className="w-full" style={{
        // ⚠️ <table> ব্যবহার করলে এটা সবসময় লাগবে
        display: "grid",
      }}>
        <tbody style={{
          height: `${rowVirtualizer.getTotalSize()}px`,
          position: "relative",
        }}>
          {rowVirtualizer.getVirtualItems().map((virtualRow) => {
            const row = rows[virtualRow.index];

            return (
              <tr
                key={row.id}
                style={{
                  display: "flex",
                  position: "absolute",
                  transform: `translateY(${virtualRow.start}px)`,
                  width: "100%",
                }}
              >
                {row.getVisibleCells().map((cell) => (
                  <td key={cell.id} className="flex-1 px-4 py-2">
                    {flexRender(
                      cell.column.columnDef.cell,
                      cell.getContext(),
                    )}
                  </td>
                ))}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}
virtualization ও shadcn Table দ্বন্দ্ব

shadcn-এর Table <table>-এর সাধারণ display: table CSS দেয়, যা virtual row-এর absolute positioning-এর সাথে কাজ করে না। তাই virtualization দিলে display: grid/flex ব্যবহার করতে হয়, অথবা অন্য virtualized table library (TanStack Virtual + custom markup) বেছে নিতে হয়।

শুধু ৫০০-১০০০ row পর্যন্ত virtualization ছাড়াই ঠিকঠাক চলে। এর বেশি হলে দরকার — এবং server-side pagination-এ সাধারণত দরকারই হয় না।

Sticky header

<TableHeader className="sticky top-0 z-20 bg-background">
  {table.getHeaderGroups().map((group) => (
    <TableRow key={group.id}>
      {group.headers.map((header) => (
        <TableHead key={header.id}>
          …
        </TableHead>
      ))}
    </TableRow>
  ))}
</TableHeader>

sticky কাজ করার জন্য কোনো ancestor-এ overflow: hidden বা overflow: auto থাকলে সমস্যা হয় — sticky তখন সেই container-এর মধ্যেই আটকায়। <Table>-এর wrapper-এ overflow না থাকা দরকার।

Accessibility

Headless মানে accessibility-ও আপনার দায়িত্ব। Table-এর জন্য মূল কয়েকটি:

<table>
  {/* টেবিলের একটি বর্ণনা */}
  <caption className="sr-only">
    পণ্যের তালিকা, মোট {total}টি
  </caption>

  {/* sortable header-এ state ঘোষণা */}
  <th
    aria-sort={
      column.getIsSorted() === "asc"
        ? "ascending"
        : column.getIsSorted() === "desc"
          ? "descending"
          : "none"
    }
  >
    …
  </th>

  {/* পেজ কন্ট্রোলে বর্তমান অবস্থা */}
  <span aria-live="polite">
    পেজ {pageIndex + 1}, মোট {pageCount}টির মধ্যে
  </span>
</table>
// row selection বোঝানো
<TableRow aria-selected={row.getIsSelected() || undefined}>
  …
</TableRow>

// filter-এর ফলাফল live ঘোষণা
<div role="status" aria-live="polite">
  {filteredCount}টির মধ্যে {filteredCount}টি পাওয়া গেছে
</div>
কীকেন
aria-sortscreen reader সাজানোর অবস্থা বলে
aria-selectedবেছে নেওয়া row বোঝায়
aria-liveপেজ/ফলাফল বদলালে ঘোষণা হয়
caption (sr-only)টেবিলটা কী, তা বোঝায়
aria-label checkbox-একোন row বলে বোঝায়
scope="col"header কোন কলামের

Sorting-এর advanced কৌশল

// ১. default sort রাখতে
initialState: {
  sorting: [{ id: "createdAt", desc: true }],
}

// ২. sort-এর অবস্থা URL-এ
useEffect(() => {
  const first = sorting[0];
  const value = first ? `${first.id}:${first.desc ? "desc" : "asc"}` : null;
  setSort(value, { scroll: false });
}, [sorting]);

// ৩. মাত্র ১০টা কলামের বেশি sort বন্ধ
maxMultiSortColCount: 3,

// ৪. যাচাই না করে দেখানো
enableSortingRemoval: true,   // ৩য় ক্লিকে sort সরে যাবে

Mutation-এর পর selection ও পেজ ঠিক করা

// একটি মুছে ফেললে last page-এ একটাই row বাকি থাকতে পারে
const mutation = useMutation({
  mutationFn: (id: string) => api.delete(`/products/${id}`),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: productKeys.all });

    // শেষ পেজে শূন্য হয়ে গেলে এক পেজ পিছনে
    const remaining = table.getRowModel().rows.length - 1;
    if (remaining === 0 && pagination.pageIndex > 0) {
      setPagination((old) => ({ ...old, pageIndex: old.pageIndex - 1 }));
    }
  },
});
এই কোডটা বাস্তবে লাগে

শেষ পেজে ১টি row মুছে দিলে খালি পেজ দেখাবে — অনেক অ্যাপে এটা ছোট কিন্তু ক্রণ ঘটা একটা বাগ। pageIndex এক কমিয়ে দিলে ব্যবহারকারী খালি পেজ দেখে না ছুটে অপেক্ষা করতে হয় না।

চূড়ান্ত চেকলিস্ট

কাজমনে রাখবেন
basegetCoreRowModel() + getRowId
client-side৪টি row model যোগ করুন
server-sidemanual* + rowCount + শুধু core
filter UIখালি মানে return true
columnenableSorting/Hiding: false display-এ
performancecolumns বাইরে, keepPreviousData, debounce
UXisPending আর isFetching আলাদা জায়গায়
a11yaria-sort, aria-live, caption
storagevisibility/order/pinning localStorage-এ

সাধারণ ভুল সংক্ষেপে

ভুলফলসমাধান
getRowId নেইselection ভাঙে(row) => row.id
manual + row model দুটোইকিছু হয় নাmanual-এ model বাদ
rowCount: 0"পেজ 1 / 0"?? -1
pageIndex সরাসরি API০ পেজ+ 1
filter-এ page reset নেইখালি পেজuseEffect
sticky-তে bg নেইলেখা ভেসে যায়bg-background
colSpan={columns.length}লুকানো কলামে ভাঙাgetVisibleLeafColumns().length
Object.keys(rowSelection)ভুল idgetSelectedRowModel()
a > b দিয়ে বাংলা sortভুল ক্রমlocaleCompare("bn-BD")

শেষ কথা

TanStack Table শিখতে গুরুত্বপূর্ণ তিনটি ব্যাপার আছে।

প্রথম — headless মানে দায়িত্ব আপনার। state ও logic লাইব্রেরি দেয়, markup, loading state, empty state, accessibility সব আপনাকে লিখতে হয়। এই অধ্যায়গুলোতে যত code দেখলেন, বাস্তবে সবটাই আপনার লেখা।

দ্বিতীয় — row model-এর পাইপলাইন মনে রাখুন। filter → sort → expand → paginate। ক্রম গুলিয়ে গেলে বাগ আসে, আর কেন আসে তা বুঝতে পারবেন না।

তৃতীয় — controlled state-ই বাস্তব। ছোট ডেমোতে initialState যথেষ্ট, কিন্তু যোগানোর সাথে সাথে state + on*Change লাগবেই — কারণ তখনই sorting/filtering server-এ যায়, URL-এ যায়, আর অন্য component-ও পড়তে পারে।

এরপর কী পড়বেন

TanStack Table-এর TanStack Form ও TanStack Virtual একই লেখকের তৈরি, একই মনোভাব — একটা state ধরে রাখে, বাকিটা আপনার। এগুলো শিখলে TanStack-এর পুরো টুলকিট হাতে চলে আসবে।

শেষ প্যারায় নিজে practice করুন — কোড কপি করে নিজের এডিটরে চালিয়ে দেখুন।
আগেরServer-side Mode — সার্ভার থেকে ডেটাTanStack Table ডকুমেন্টেশন হোম
এই পেজে
Tech Tutor

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

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

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

mdanamolhasan1998@gmail.com