Filtering — খোঁজা ও ছাঁকা
globalFilter বনাম columnFilters, filterFns, custom filter function, debounced search, filter UI, manualFiltering — বাংলায়।
সাজানোর পর এলো খোঁজা। TanStack-এ filtering-এর দুটি স্তর আছে — global filter (পুরো টেবিলে একসাথে) আর column filter (নির্দিষ্ট কলামে)। দুটো একসাথেও চলতে পারে।
দুই স্তর
// ১. global filter — একটা সার্চ বক্স, সব কলামে মিলে
const [globalFilter, setGlobalFilter] = useState("");
// ২. column filter — প্রতিটি কলামে আলাদা শর্ত
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(), // ← এটা ছাড়া হবে না
state: { globalFilter, columnFilters },
onGlobalFilterChange: setGlobalFilter,
onColumnFiltersChange: setColumnFilters,
});| global filter | column filter | |
|---|---|---|
| state | globalFilter: string | columnFilters: { id, value }[] |
| UI | একটা search input | কলামভিত্তিক dropdown/range |
filterFns | globalFilterFn | প্রতিটি filterFns-এর নিজস্ব |
| কোন কলামে মিলবে | enableGlobalFilter: true সবগুলো | শুধু ওই কলাম |
দুটো একসাথে চালু থাকলে AND হয় — মানে global-এ "টি" আর category-তে "books" — দুটোই মেলে এমন row বাকি থাকে।
Global filter — search box
export function GlobalFilter({
table,
}: {
table: Table<Product>;
}) {
const value = table.getState().globalFilter ?? "";
return (
<div className="relative max-w-sm">
<Search className="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={value}
onChange={(e) => table.setGlobalFilter(e.target.value)}
placeholder="পণ্য খুঁজুন…"
className="pl-9"
/>
</div>
);
}table.setGlobalFilter() লিখলে onGlobalFilterChange চলে, তাই ওই state-ও বদলায়। অর্থাৎ দুটো একসাথে ব্যবহার করা যায় — নিজের setGlobalFilter বা table-এর method, যেটা আপনার পছন্দ।
কোন কলামে মিলবে
{
accessorKey: "createdAt",
enableGlobalFilter: false, // ISO date সার্চে শুধু ধুলো মিলবে
},
{
accessorKey: "isActive",
enableGlobalFilter: false, // boolean — অর্থহীন
},
{
accessorKey: "name",
enableGlobalFilter: true, // ডিফল্ট
},
{
id: "searchText",
accessorFn: (row) =>
`${row.name} ${row.category} ${row.id}`.toLowerCase(),
enableGlobalFilter: true,
}accessorKey: "name" দিলে global search শুধু নামে মেলে। চাইলে একাধিক কলামে একসাথে মেলাতে একটা সাহায্যকারী কলাম বানান যেটা enableHiding: false — দেখা যাবে না, কিন্তু search-এ কাজ করবে।
বাংলায় সার্চ ঠিক করা
useReactTable({
getFilteredRowModel: getFilteredRowModel(),
globalFilterFn: "includesString",
// বাংলা সম্পূর্ণ সংযুক্তাক্ষরে লেখা হয় — একটু সহজ করে দিই
globalFilterFn: (row, columnId, filterValue) => {
const cell = row.getValue(columnId);
if (cell == null) return false;
return String(cell)
.normalize("NFC")
.toLowerCase()
.includes(String(filterValue).normalize("NFC").toLowerCase());
},
})Column filters
import type { ColumnFiltersState } from "@tanstack/react-table";
// বর্তমান অবস্থা
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
// [
// { id: "category", value: "books" },
// { id: "stock", value: [10, 50] }, // range
// { id: "name", value: "react" },
// ]
// কোনো কলামের filter বদলানো
table.getColumn("category")?.setFilterValue("books");
// সব filter মুছে ফেলা
table.resetColumnFilters();
// একটি column-এর মান পড়া
table.getColumn("category")?.getFilterValue();কোন কলামে কী ধরনের filter
{
accessorKey: "category",
filterFn: "equalsString", // নির্দিষ্ট একটি মান
},
{
accessorKey: "name",
filterFn: "includesString", // অংশ মিললেই হবে
},
{
accessorKey: "price",
filterFn: "inNumberRange", // [min, max]
},
{
accessorKey: "rating",
filterFn: "inNumberRange",
},
{
accessorKey: "isActive",
filterFn: "equalsBoolean",
},
{
id: "multi",
filterFn: (row, columnId, filterValue: string[]) =>
filterValue.includes(row.getValue(columnId)),
}Built-in filterFns
| filterFn | তুলনা | উদাহরণ value |
|---|---|---|
equalsString | হুবহু সমান | "books" |
equalsNumber | সংখ্যা সমান | 0 |
equalsBoolean | true/false | true |
arrayIncludesString | array-তে আছে কিনা | ["a","b"] |
arrIncludesSome | array-র কোনো একটা মিলে | ["a","b"] |
inNumberRange | min ≤ মান ≤ max | [10, 100] |
inStringRange | lexically মধ্যে | ["a", "m"] |
includesString | অংশ মিললেই | "rea" |
includesStringWithOpposites | !rea → যেসব মিলে না | "!rea" |
includesNumber | অংশ সংখ্যা | "1" |
includesString default — মানে "a" লিখলে "Book"-এও মিলবে। এটা অনেকে খারাপ লাগে। চাইলে শুধু শুরুর দিকে মিল করাতে filterFn: "startsWith" লিখে custom বানাতে পারেন।
Custom filter function
import type { FilterFn } from "@tanstack/react-table";
// বাংলায় খোঁজা
const searchBangla: FilterFn<any> = (row, columnId, filterValue) => {
const value = String(filterValue ?? "").trim();
if (!value) return true; // ফাঁকা হলে সব থাকবে
const cell = String(row.getValue(columnId) ?? "")
.normalize("NFC")
.toLowerCase();
const needle = value.normalize("NFC").toLowerCase();
return cell.includes(needle);
};
// price: সবচেয়ে সস্তা আগে, বাকি বাদ
const priceAsc: FilterFn<number> = (row, columnId, filterValue: number) =>
row.getValue<number>(columnId) === filterValue;
// ব্যবহার
{
accessorKey: "name",
filterFn: searchBangla,
}Filter UI — category dropdown
export function CategoryFilter({
table,
}: {
table: Table<Product>;
}) {
const column = table.getColumn("category");
const value = column?.getFilterValue() as string | undefined;
// faceted values — ডেটা থেকেই অপশন বের হবে
const options = column?.getFacetedUniqueValues() ?? new Map();
return (
<Select
value={value ?? "all"}
onValueChange={(v) =>
column?.setFilterValue(v === "all" ? undefined : v)
}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="ক্যাটাগরি" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">সব</SelectItem>
{[...options.keys()].map((option) => (
<SelectItem key={option} value={String(option)}>
{option} ({options.get(option)})
</SelectItem>
))}
</SelectContent>
</Select>
);
}getFacetedUniqueValues() সাধারণ ডেটায় কাজ করে (faceted row model চালু থাকলে)। Server-side mode-এ এটা ফাঁকা ফেরত দেয় — তখন অপশন আলাদা endpoint থেকে আনতে হয়, কারণ table-এর কাছে মোট ডেটাই নেই।
Price range filter
export function PriceFilter({ table }: { table: Table<Product> }) {
const column = table.getColumn("price");
const range = (column?.getFilterValue() as [number, number]) ?? [0, 200000];
return (
<div className="flex items-center gap-2">
<Input
type="number"
value={range[0]}
onChange={(e) =>
column?.setFilterValue([
Number(e.target.value),
range[1],
])
}
className="w-28"
/>
<span className="text-muted-foreground">—</span>
<Input
type="number"
value={range[1]}
onChange={(e) =>
column?.setFilterValue([
range[0],
Number(e.target.value),
])
}
className="w-28"
/>
</div>
);
}ফলাফল দেখানো ও মুছে ফেলা
const filteredCount = table.getFilteredRowModel().rows.length;
const totalCount = table.getCoreRowModel().rows.length;
const hasFilter = table.getState().globalFilter || table.getState().columnFilters.length > 0;
// UI
<footer className="border-t p-3 text-sm text-muted-foreground">
{hasFilter ? (
<div className="flex items-center gap-2">
<span>
{filteredCount}টির মধ্যে {filteredCount}টি পাওয়া গেছে
</span>
<Button
variant="ghost"
size="sm"
onClick={() => {
table.setGlobalFilter("");
table.resetColumnFilters();
}}
>
filter মুছুন
</Button>
</div>
) : (
<span>মোট {totalCount}টি পণ্য</span>
)}
</footer>Debounced search
প্রতিটি keystroke-এ filter চললে বড় ডেটায় lag পড়ে। ছোট টেবিলে দরকার নেই, বড় অ্যাপে debounce করুন।
import { useDeferredValue } from "react"; // React 19-এর সহজ সমাধান
export function DataTable({ data, columns }: DataTableProps) {
const [input, setInput] = useState("");
const deferredInput = useDeferredValue(input);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: { globalFilter: deferredInput },
onGlobalFilterChange: setInput,
});
// input = তাৎক্ষণিক (টাইপ করলেই দেখাবে)
// globalFilter = পিছনে (হিসাব ধীর, ফলাফল একটু পরে আসে)
}// বাইরের লাইব্রেরি ব্যবহার করতে চাইলে
import { useDebounce } from "use-debounce";
const [input, setInput] = useState("");
const [debounced] = useDebounce(input, 300);
// globalFilter: debounced, onGlobalFilterChange: setInputmanualFiltering
Server-এ filter হলে table কে বলে দিতে হয় যে সে নিজে filter করবে না — manualFiltering: true। তখন getFilteredRowModel()-এর ফলাখানা data-র সমান, কারণ transform-ই হয়নি।
useReactTable({
data: data?.items ?? [],
getCoreRowModel: getCoreRowModel(),
manualFiltering: true, // filter API-তে যাচ্ছে
})Server mode-এ getIsAllRowsFiltered() সবসময় false — কারণ table জানে না মোট কতটা ছিল। ফলে "কোনো ফলাফল নেই" আর "লোড হচ্ছে" গুলিয়ে যেতে পারে। isPending আর rows.length === 0 — দুটো আলাদা করে যাচাই করুন।
সাধারণ বাগ
| ঘটনা | কারণ | সমাধান |
|---|---|---|
| filter কাজ করে না | getFilteredRowModel() নেই | যোগ করুন |
| সব কলামে মেলে না | enableGlobalFilter: false | যেটায় খুঁজছেন সেটা true করুন |
| ISO date সার্চে ধুলো মিলে | default global filter | enableGlobalFilter: false |
| খালি box-এ সব row চলে যায় | ""-এর জন্য guard নেই | custom filter-এ if (!value) return true |
| reset হলে state-এ পুরোনো মান | resetColumnFilters vs setFilterValue(undefined) | সব ক্ষেত্রে resetColumnFilters() |
| filter বদলালে খালি পেজ | autoResetPageIndex বন্ধ | অধ্যায় ৮ দেখুন |
| number range কাজ করে না | inNumberRange বসানো হয়নি | filterFn: "inNumberRange" |
সারসংক্ষেপ
getFilteredRowModel()ছাড়া filtering হবে না- global = একটা সার্চ বক্স; column = কলামভিত্তিক শর্ত
filterFnনা দিলেincludesString— শুধু নাম খুঁজলেstartsWithভাবুন- বড় ডেটায়
useDeferredValueবা debounce - server-এ filter =
manualFiltering: true+getFilteredRowModel()বাদ
পরবর্তী
Filtering শেষ। পরের অধ্যায়ে pagination — pageIndex-এর শূন্য-ভিত্তিক গণনা, rowCount, আর সবচেয়ে বেশি ভুল হওয়া off-by-one সমস্যাটা।