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. Server-side Mode — সার্ভার থেকে ডেটা
TanStack TableAdvanced

Server-side Mode — সার্ভার থেকে ডেটা

manualSorting/manualFiltering/manualPagination, rowCount, state থেকে query params, page reset, TanStack Query-এর সাথে সেতু, debounce ও URL sync — বাংলায়।

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

আগের অধ্যায়গুলোতে সব হিসাব browser-এ হতো — তা কয়েকশো row-এর জন্য যথেষ্ট। কিন্তু ৫ লাখ row হলে যা ঘটবে: পুরো ডেটা download, প্রতিবার সাজানো, আর কয়েক সেকেন্ড লাগবে। এই অধ্যায়ে সেটাই বদলাচ্ছি — হিসাব সার্ভারে, table শুধু state ধরে রাখবে।

মূল পার্থক্য

TanStack-এ দুটি পুরো আলাদা জগত আছে। বুঝে নিলে বাকি সব সহজ।

client-sideserver-side
ডেটাপুরোটাশুধু current page
সাজানোgetSortedRowModel()server-এর কাজ
manual*falsetrue
row model৪-৫টিশুধু getCoreRowModel()
rowCountদরকার নেইআবশ্যক
কত row বাকিজানাtotal থেকে

manual flag

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

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

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

  // মোট কত row আছে (API থেকে)
  rowCount: data?.total ?? -1,

  // ⚠️ শুধু core — বাকি model দিলে কাজ হবে না
  getCoreRowModel: getCoreRowModel(),
});
flagstate বদলায়ডেটা transform করে
manualSortingheader-এর অ্যারি বদলায়না
manualFilteringfilter state বদলায়না
manualPaginationপেজ বদলায়না
manual mode-এ row model দেবেন না

manualSorting: true দিলে getSortedRowModel() উপেক্ষা করা হয়। দুটো একসাথে দিলে state বদলাবে কিন্তু কোনো দৃশ্যমান পরিবর্তন হবে না — বাগ খুঁজতে কয়েক ঘণ্টা লাগে। Rule: manual* = true হলে ঐ feature-এর row model বাদ দিন।

State থেকে query params — সেতু

এটাই পুরো server-side table-এর মূল কৌশল। Table-এর state থেকে params বানাই, params থেকে নতুন ডেটা আসে।

export function ProductsTable() {
  const [sorting, setSorting] = useState<SortingState>([
    { id: "createdAt", desc: true },   // নতুনগুলো আগে
  ]);
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 10,
  });
  const [search, setSearch] = useState("");
  const [category, setCategory] = useState<string | undefined>();

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

  // ② params → ডেটা
  const { data, isPending, isFetching } = useProducts(params);

  // ③ ডেটা → table
  const table = useReactTable({
    data: data?.items ?? [],
    columns,
    getCoreRowModel: getCoreRowModel(),
    getRowId: (row) => row.id,
    state: { sorting, pagination, globalFilter: search, columnFilters: … },
    onSortingChange: setSorting,
    onPaginationChange: setPagination,
    onGlobalFilterChange: setSearch,
    manualSorting: true,
    manualFiltering: true,
    manualPagination: true,
    rowCount: data?.total ?? -1,
  });
}

params প্রতি রেন্ডারে নতুন object হলেও TanStack Query-র queryKey গভীরভাবে তুলনা করে — তাই একই মানে নতুন request যায় না।

Query key

// API call
const productKeys = {
  all: ["products"] as const,
  list: (params: ProductQueryParams) =>
    [...productKeys.all, "list", params] as const,
};

export function useProducts(params: ProductQueryParams) {
  return useQuery({
    queryKey: productKeys.list(params),
    queryFn: ({ signal }) =>
      axios
        .get<ProductsResponse>("/api/products", {
          params,
          signal,          // ← request বাতিল হলে query-ও বাতিল
        })
        .then((res) => res.data),
    placeholderData: keepPreviousData,  // ← পরের পেজ আসা পর্যন্ত পুরোনো দেখায়
  });
}
placeholderData: keepPreviousData

এটা ছাড়া পেজ বদলালে table এক মুহূর্তের জন্য ফাঁকা হয়ে যায়। keepPreviousData দিলে পুরোনো ডেটা দেখিয়ে উপরে একটা ছোট loading indicator দেখায় — isFetching ব্যবহার করে। এটা server-side table-এর সবচেয়ে জরুরি একটা সেটিং।

pageIndex কেন reset করতে হয়

Client mode-এ TanStack নিজেই autoResetPageIndex চালু রাখে — filter/sort বদলালে প্রথম পেজে ফিরে যায়। কিন্তু manualPagination: true দিলে এটা নিজে থেকেই `false` হয়ে যায়। ফলে ৫ নম্বর পেজে থেকে filter বদলালে খালি পেজ পান।

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

যে কিছুতে ভ্যালু বদলালে page অর্থহীন হয়, ওগুলোই dependency। sorting একটা array — তাই সরাসরি useEffect dependency হিসেবে দিলে প্রতি রেন্ডারে নতুন identity পেয়ে চলে যায়। sorting[0]?.id ও sorting[0]?.desc বের করে সেগুলো dependency দিন।

const sortKey = sorting[0]?.id;
const sortDir = sorting[0]?.desc;

useEffect(() => {
  setPagination((old) =>
    old.pageIndex === 0 ? old : { ...old, pageIndex: 0 },
  );
}, [search, category, sortKey, sortDir]); // ✅ পরিষ্কার

old ফেরত দেওয়া জরুরি — তাহলে ইতিমধ্যে 0 থাকলে state unchanged থাকে, re-render হয় না, আর সত্যিই 0 না হলে বদলায়।

debounced search

সার্চ বক্সে প্রতি keystroke-এ request গেলে ১০ অক্ষর লিখতে ১০টা request — আর প্রতিটি আগেরটা বাতিল। Debounce ছাড়া এটা হয়।

import { useEffect, useState } from "react";

export function useDebouncedValue<T>(value: T, delay = 300): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}
const [searchInput, setSearchInput] = useState("");
const search = useDebouncedValue(searchInput, 350);

// query-তে debounced মান যায়
const params = { …, search: search || undefined };

// input-এ তাৎক্ষণিক মান বসে — দ্রুত টাইপ feel হয়
<Input
  value={searchInput}
  onChange={(e) => setSearchInput(e.target.value)}
/>
// input লিখছে, request যাচ্ছে ৩৫০ms পরে
setSearchInput("lap")   // ← তাৎক্ষণিক: input দেখায়
                        // ← ৩৫০ms পরে: search = "lap" → queryKey বদলায়
                        // ← "laptop" লিখলে আগের timer বাতিল

URL-এ state রাখা

Table-এর state URL-এ থাকলে share-যোগ্য, bookmark করা যায়, আর back বাটন কাজ করে। এটা nuqs বা নিজে URLSearchParams দিয়ে করা যায়।

"use client";

import { useQueryState } from "nuqs/sticky";
import type { SortingState } from "@tanstack/react-table";

export function ProductsTable() {
  const [page, setPage] = useQueryState("page", {
    parse: Number,
    defaultValue: 1,
  });
  const [sort, setSort] = useQueryState("sort");
  const [q, setQ] = useQueryState("q");

  // URL → table state
  const [sorting, setSorting] = useState<SortingState>(() =>
    sort ? [{ id: sort.split(":")[0], desc: sort.endsWith(":desc") }] : [],
  );
  const [pagination, setPagination] = useState({
    pageIndex: page - 1,
    pageSize: 10,
  });

  // table state → URL
  const handleSortingChange = (updater: OnChangeFn<SortingState>) => {
    setSorting((old) => {
      const next = typeof updater === "function" ? updater(old) : updater;
      const first = next[0];
      setSort(first ? `${first.id}:${first.desc ? "desc" : "asc"}` : null);
      return next;
    });
  };

sticky মানে ব্যবহারকারী সরালে default value-তে ফিরে যায়, history.replace হয় না (back বাটর কাজ করে)। শুধু search box-এর মতো text input-এ push লাগে, সেটা আলাদা।

rowCount-এর সূক্ষ্মতা

// ✅ মোট ঠিক জানা আছে
rowCount: data?.total ?? -1,

// ⚠️ ফিল্টার বদলালে total নতুন — loading চলাকালীন পুরোনো total থাকে
rowCount: data?.total ?? 0,   // ← 0 হলে getPageCount() = 0 হয়ে UI ভাঙে
const isPageCountSafe = !isPending && data?.total != null;

// পেজ কন্ট্রোল loading-এর সময় hide করা নিরাপদ
{isPageCountSafe && (
  <TablePagination table={table} />
)}
rowCount: 0 দেবেন না

Loading চলাকালীন data undefined, তাই ?? 0 দিলে getPageCount() = 0 হয়ে যায় — "পেজ 1 / 0" দেখাবে। ?? -1 দিন (exact হিসাব হয় না) অথবা পেজ কন্ট্রোলটা loading-এর সময় লুকিয়ে দিন।

loading ও empty state

const rows = table.getRowModel().rows;
const colCount = table.getVisibleLeafColumns().length;

<TableBody>
  {isPending ? (
    // প্রথমবার — পুরো স্কেলেটন
    Array.from({ length: 5 }).map((_, i) => (
      <TableRow key={i}>
        {Array.from({ length: colCount }).map((_, j) => (
          <TableCell key={j}>
            <Skeleton className="h-4 w-full" />
          </TableCell>
        ))}
      </TableRow>
    ))
  ) : rows.length === 0 ? (
    // ফলাফল নেই
    <TableRow>
      <TableCell colSpan={colCount} className="h-32 text-center">
        <SearchX className="mx-auto mb-2 size-8 text-muted-foreground" />
        <p className="font-medium">কোনো পণ্য পাওয়া যায়নি</p>
        <p className="text-sm text-muted-foreground">
          ফিল্টার বদলে দেখুন
        </p>
      </TableCell>
    </TableRow>
  ) : (
    rows.map((row) => (
      <TableRow
        key={row.id}
        data-state={row.getIsSelected() && "selected"}
        className={isFetching ? "opacity-60 transition-opacity" : undefined}
      >
        {row.getVisibleCells().map((cell) => (
          <TableCell key={cell.id}>
            {flexRender(cell.column.columnDef.cell, cell.getContext())}
          </TableCell>
        ))}
      </TableRow>
    ))
  )}
</TableBody>

isPending = প্রথমবার লোড (স্কেলেটন দেখান), isFetching = বারবার রিফেচ (পুরোনো ডেটা রেখে ম্লান করে)। দুটো এক জায়গায় ব্যবহার করবেন না।

API-তে query থেকে rowCount বের করা

import { keepPreviousData, useQuery } from "@tanstack/react-query";

type ProductSortField = "name" | "price" | "stock" | "createdAt";
type SortOrder = "asc" | "desc";

type ProductQueryParams = {
  page: number;
  pageSize: number;
  search?: string;
  category?: string;
  sortBy?: ProductSortField;
  sortOrder?: SortOrder;
};

type ProductsResponse = {
  items: Product[];
  total: number;
  page: number;
  pageSize: number;
};
// Next.js route handler
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const page = Math.max(1, Number(searchParams.get("page") ?? 1));
  const pageSize = Math.min(100, Number(searchParams.get("pageSize") ?? 10));
  const search = searchParams.get("search") ?? undefined;
  const sortBy = (searchParams.get("sortBy") ?? "createdAt") as ProductSortField;
  const sortOrder = (searchParams.get("sortOrder") ?? "desc") as SortOrder;

  const where = search
    ? { name: { contains: search, mode: "insensitive" } }
    : {};

  const [total, items] = await Promise.all([
    prisma.product.count({ where }),
    prisma.product.findMany({
      where,
      skip: (page - 1) * pageSize,
      take: pageSize,
      orderBy: { [sortBy]: sortOrder },
    }),
  ]);

  return Response.json({ items, total, page, pageSize });
}

sortBy গ্রাহক থেকে আসে বলে whitelist করা জরুরি — নইলে যে কেউ ?sortBy=password পাঠিয়ে sensitive field দেখতে পারে।

Selection-এর বিশেষ ঘটনা

Server-এ এক পেজের row বেছে নিলে সেগুলো ইচ্ছাকৃতভাবেই এক পেজেই থাকবে — পরের পেজে ফিল্টার হবে না। কারণ table জানে না মোট কয়টি row মিলেছে। এটা getIsAllRowsSelected()-এর উপর নির্ভর করে।

const totalMatching = data?.total ?? 0;
const onThisPage = table.getRowModel().rows.length;

// পরের পেজেও বেছে থাকবে কিনা
const isSelectingAll = table.getIsAllRowsSelected();

// "সব ১২৮৪টি বেছে নিন" লিংক দেখাতে
{!table.getIsAllPageRowsSelected() && totalMatching > onThisPage && (
  <Button
    variant="link"
    onClick={() => table.toggleAllRowsSelected(true)}
  >
    সব {totalMatching}টি বেছে নিন
  </Button>
)}
সব বেছে নিলে URL পারে না

toggleAllRowsSelected(true) server mode-এ হাজার হাজার id state-এ ঢোকায়। বড় অ্যাপে বদলে একটা পতাকা রাখুন — const [allMatching, setAllMatching] = useState(false) — আর mutation-এ allMatching ? where: {…} : { id: { in: selectedIds } } ব্যবহার করুন।

সম্পূর্ণ চেকলিস্ট

কাজকেন
manual* = trueনিজে transform হবে না
শুধু getCoreRowModel()বাকি model কাজ করবে না
getRowIdselection ও key ঠিক থাকে
rowCount: total ?? -1page count হিসাব
pageIndex + 1off-by-one এড়ানো
filter-এ pageIndex resetখালি পেজ এড়ানো
debounce searchঅপ্রয়োজনীয় request বন্ধ
keepPreviousDataপেজ বদলে ফাঁকা হয় না
isPending/isFetching আলাদাUX ঠিক থাকে
sortBy whitelistনিরাপত্তা

সারসংক্ষেপ

  • সব হিসাব server-এ, table শুধু state ও button
  • manual* + শুধু getCoreRowModel() + rowCount — এই তিনটিই মূল
  • state → params → query → ডেটা → table, এই চক্রটাই সব
  • manual mode-এ auto page reset হয় না, করে দিতে হয়
  • loading-এর দুই ধরন আলাদা জায়গায়

পরবর্তী

সবচেয়ে বেশি ব্যবহৃত কাজ শেষ। শেষ অধ্যায়ে বাকি যা বাকি — row grouping, row expanding, faceted filter, virtualized row, sticky header আর accessibility।

শেষ প্যারায় নিজে practice করুন — কোড কপি করে নিজের এডিটরে চালিয়ে দেখুন।
আগেরColumn Management — visibility, order, pinning ও sizingপরেরAdvanced Features — grouping, expanding, virtualizing
এই পেজে
Tech Tutor

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

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

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

mdanamolhasan1998@gmail.com