Mundarija (28)
- Bu darsda
- 1. Nega bu kerak?
- 2. Klientmi yoki server: o'lchab ko'ramiz
- 2.1 Tarmoq: qancha bayt keladi
- 2.2 Protsessor: filtr, saralash va DOM
- 2.3 Qoida
- 3. Server tomonli model
- 3.1 manual* bayroqlari
- 3.2 Query bilan to'liq misol
- 3.3 Uch muhim detal
- 4. Holat — URL'da
- 4.1 Nega URL
- 4.2 Hujumchi nigohi: URL'dagi tartib — serverga
- 5. Qator tanlash
- 5.1 Id bo'yicha tanlash
- 5.2 Sahifalar orasida tanlov
- 6. Ustunlar: qotirish va kenglik
- 7. Klientda ko'p qator: virtualizatsiya
- 8. Tayyor grid'lar: AG Grid va MUI DataGrid
- 9. Tajriba: vazifalar da server tomonli jadval bormi?
- 10. Ko'p uchraydigan xatolar
- 11. Mashqlar
- 1-mashq (oson): qaysi sozlama?
- 2-mashq (o'rta): server tomonli qidiruv
- 3-mashq (qiyin): sahifalar orasida tanlov
- 12. Real ishda
- Xulosa
- Manbalar
Server tomonli jadval (Jadvallar II): manualPagination, URL holati, qator tanlash va katta ma'lumot
Qisqacha: Ma'lumot ko'p bo'lsa (o'n minglab qator), uni to'liq yuklab klientda saralash qimmat. O'lchovimizda 100 000 qator JSON — qariyb 4 MB (3 967 KB), faqat filtr va saralash — ~0.4 soniya. Server tomonli jadvalda saralash, filtr va sahifalashni server qiladi: TanStack Table'ga
manualSorting,manualPagination,manualFilteringvarowCountberiladi, u faqat holatni boshqaradi. Holat (sahifa, tartib) — so'rov kalitida va URL'da. Qator tanlash —getRowIdbilan id bo'yicha, sahifalar orasida ham. Ustunlarni yashirish, qotirish (pinning) va o'lchamini o'zgartirish — alohida imkoniyatlar. Klientda ko'p qator chizish kerak bo'lsa — virtualizatsiya.
Bu darsda
- Klient va server tomonli jadval orasidagi chegarani o'lchov bilan aniqlaysiz.
- TanStack Query va
manualPagination/manualSortingbilan server sahifalarini ko'rsatasiz. - Sahifa va tartibni URL'da saqlaysiz — havolani ulashsa, o'sha ko'rinish ochiladi.
- Qatorlarni id bo'yicha tanlaysiz, "hammasini tanlash" va yarim tanlangan holatni to'g'ri qilasiz.
- Ustunlarni yashirish, qotirish va kengligini o'zgartirishni bilasiz.
- Virtualizatsiya, AG Grid va MUI DataGrid qachon kerakligini aytasiz.
Oldin bilishingiz kerak: Jadvallar I: TanStack Table asoslari, TanStack Query IV: sahifalash va cheksiz ro'yxat, URL — holat saqlash joyi, Uzun ro'yxatlar: virtualizatsiya.
1. Nega bu kerak?
O'tgan darsda «Bahor» menyusining jadvali besh-o'n qatordan iborat edi: hammasi brauzerda, saralash va filtrni TanStack Table o'zi qildi. Endi buyurtmalar jadvali. «Bahor» kuniga ~300 buyurtma oladi — bir yilda 100 000 dan ortiq.
Sardor o'tgan darsdagi kodni ko'chirdi: GET /buyurtmalar — hammasi, keyin klientda filtr, saralash, sahifa. Jasur aka telefonida ochdi va "sahifa ochilmayapti" dedi. Aslida ochilgan — faqat juda uzoq.
Muammo kodda emas, qarorda: kim saralaydi — brauzermi yoki server? Hayotiy o'xshatish: kutubxona. Bitta kitob kerak bo'lsa, butun fondni uyingizga tashib, o'zingiz qidirmaysiz. Kutubxonachiga "tarix bo'limi, yangilari birinchi, 20 tadan" deysiz va u sizga faqat 20 ta kitob beradi. Server tomonli jadval — o'sha kutubxonachi.
2. Klientmi yoki server: o'lchab ko'ramiz
2.1 Tarmoq: qancha bayt keladi
Avval eng oddiy savol — 100 000 buyurtmani yuborish qancha turadi? Bitta buyurtma — id, taom nomi va summa. Uni JSON qilib, hajmini o'lchaymiz:
const DISHES = ["Osh", "Manti", "Lag'mon", "Somsa", "Ko'k choy"];
const PRICES = [35000, 30000, 28000, 8000, 5000];
function makeOrders(n) {
return Array.from({ length: n }, (_, i) => ({
id: 101 + i,
dish: DISHES[i % 5],
sum: PRICES[i % 5] * (1 + (i % 3)),
}));
}
const kb = (text) => (new Blob([text]).size / 1024).toFixed(1);
const all = JSON.stringify(makeOrders(100000));
const page = JSON.stringify(makeOrders(20));
console.log("100 000 buyurtma:", kb(all), "KB");
console.log("bitta sahifa (20):", kb(page), "KB");Konsolda:
100 000 buyurtma: 3967.4 KB
bitta sahifa (20): 0.8 KBBlob — matnni UTF-8 baytlarida o'lchaydi (length esa belgilarni sanardi; kirill harfi yoki ‘ bitta belgi, lekin ikki-uch bayt). Qariyb 4 MB — har buyurtmada atigi uchta maydon bilan. Haqiqiy javobda har buyurtmada mijoz, manzil, vaqt, holat ham bo'ladi — hajm bir necha baravar katta. gzip uni kichraytiradi, lekin brauzer baribir to'liq JSON'ni o'qib (parse), xotirada saqlashi kerak.
2.2 Protsessor: filtr, saralash va DOM
Endi brauzer ishini o'lchaymiz. O'tgan darsdagi jadval (global filtr «o», narx bo'yicha kamayish) — 1 000, 10 000 va 100 000 qator bilan, ikki variantda: faqat 20 qatorlik sahifa chiziladi yoki filtrdan o'tgan hamma qatorlar. O'lchov — tugma bosilgandan React ekranni yangilaguncha (commit). Kod — pastda, «Ishga tushir» bilan o'zingiz ham sinashingiz mumkin (100 000 hammasi — bir necha soniya qotadi):
- 1 000, sahifaDOM: 20 qator; 1-urinishda 14 ms142 ms
- 1 000, hammasiDOM: 80052 ms
- 10 000, sahifaDOM: 2031 ms
- 10 000, hammasiDOM: 8 000321 ms
- 100 000, sahifaDOM: 20371 ms
- 100 000, hammasiDOM: 80 0002 763 ms
Manba: IlmHamroh kurs tajribasi, 2026-10-17: Chrome 155.0.8059.82 (headless), i5-12500H, React 19.3.0 development build, @tanstack/react-table 9.2.6; quyidagi «O'lchov kodi» bloki, 6 variant ketma-ket, 3 aylana
Grafikni o'qiymiz:
- Sahifa bilan ham 100 000 qator ~0.4 soniya (371 ms). DOM'da atigi 20 qator, lekin filtr va saralash hammasidan o'tadi. Har qidiruv harfida — yana 0.4 soniya. Bu INP uchun yomon (yaxshi chegarasi — 200 ms).
- "Hammasi" — DOM narxi. 10 000 qatorda 8 000 ta
<tr>chizish — 321 ms (sahifa bilan 31 ms edi), 100 000 da — ~2.8 soniya. Sahifalash yoki virtualizatsiya shuning uchun kerak. - 1 000 qatorgacha — klient jadvali bemalol: 52 ms. «1 000, sahifa» ning 142 ms i — o'lchov shovqini: birinchi urinishda 14 ms edi. Keyingi urinishlarda u «100 000, hammasi» dan keyin keldi va oldingi 80 000 qatorni DOM'dan olib tashlash vaqti ham hisobga tushdi.
Raqamlar development build'da — production'da React tezroq. Mashina ham hisobda: Aziz'ning arzon telefoni bu kompyuterdan bir necha baravar sekin. Hatto shu kompyuterning o'zida ham: boshqa paytda, protsessor boshqa ishlar bilan band bo'lganda, xuddi shu kod 2–3 baravar katta raqamlar bergan. Shuning uchun aniq millisekundga emas, nisbatga qarang: qator 10 baravar ko'payganda vaqt ham taxminan 6–12 baravar o'sadi — bu har mashinada takrorlanadi.
O'lchov kodi:
import { useLayoutEffect, useState } from "react";
import {
columnFilteringFeature,
createColumnHelper,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFn_includesString,
globalFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
sortFn_basic,
tableFeatures,
useTable,
} from "@tanstack/react-table";
const DISHES = ["Osh", "Manti", "Lag'mon", "Somsa", "Ko'k choy"];
function makeOrders(n) {
return Array.from({ length: n }, (_, i) => ({
id: i + 1,
dish: DISHES[i % 5],
price: 5000 + ((i * 7919) % 40000),
}));
}
const base = {
columnFilteringFeature,
globalFilteringFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { basic: sortFn_basic },
};
const PAGED = tableFeatures({
...base,
rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel(),
});
const ALL = tableFeatures(base);
const col = createColumnHelper();
const columns = col.columns([
col.accessor("id", { header: "№", sortFn: "basic" }),
col.accessor("dish", { header: "Taom", sortFn: "basic" }),
col.accessor("price", { header: "Narx", sortFn: "basic" }),
]);
const SORTING = [{ id: "price", desc: true }];
function Bench({ run }) {
const table = useTable({
features: run.paged ? PAGED : ALL,
columns,
data: run.data,
globalFilterFn: "includesString",
state: { sorting: SORTING, globalFilter: "o" },
initialState: { pagination: { pageIndex: 0, pageSize: 20 } },
});
const rows = table.getRowModel().rows;
useLayoutEffect(() => {
const ms = Math.round(performance.now() - run.t0);
const mode = run.paged ? "sahifa" : "hammasi";
console.log(
`${run.data.length} | ${mode} | ${ms} ms | DOM: ${rows.length}`,
);
}, [run, rows.length]);
return (
<table>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
{row.getAllCells().map((cell) => (
<td key={cell.id}>{String(cell.getValue())}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
export default function App() {
const [run, setRun] = useState(null);
function start(n, paged) {
const data = makeOrders(n);
setRun({ data, paged, t0: performance.now() });
}
return (
<div>
{[1000, 10000, 100000].map((n) => (
<p key={n}>
<button onClick={() => start(n, true)}>
{n} sahifa
</button>{" "}
<button onClick={() => start(n, false)}>
{n} hammasi
</button>
</p>
))}
{run && <Bench key={run.t0} run={run} />}
</div>
);
}Ma'lumotni yaratish (makeOrders) o'lchovga kirmaydi — vaqt undan keyin boshlanadi.
2.3 Qoida
| Qatorlar (taxminan) | Kim qayta ishlaydi | Nima chiziladi |
|---|---|---|
| ~1 000 gacha | klient (o'tgan dars) | sahifa yoki hammasi |
| 1 000 – 10 000 | klient ham bo'ladi, server yaxshiroq | sahifa yoki virtualizatsiya |
| 10 000 dan ko'p | server | faqat sahifa |
Chegaralar qat'iy emas: ustunlar soni, qurilma va qidiruv chastotasiga bog'liq. Bitta narsa aniq — ma'lumot o'sib boradigan jadval (buyurtmalar, loglar, to'lovlar) boshidan server tomonli loyihalanadi. Keyin o'tkazish — API'ni ham, UI'ni ham qayta yozish.
Tekshirib ko'ring: «Bahor» menyusida 40 ta taom. Jasur aka "server tomonli qilaylik, zamonaviy" dedi. Nima deysiz?
Javob
Kerak emas. 40 qator — o'lchovimizdagi eng kichik variantdan ham 25 baravar kam: klient jadvali millisekundlarda ishlaydi. Server tomonli jadval esa har saralash va har sahifada tarmoq so'rovi, kutish holati va murakkabroq API demak. Menyu tez-tez o'smaydi — klient jadvali to'g'ri tanlov. "Zamonaviy" — o'lchov emas.
3. Server tomonli model
3.1 manual* bayroqlari
O'tgan darsdagi qator modellari zanjirini eslang: filtr → saralash → sahifa. Server tomonli jadvalda bu zanjirni server bajaradi. TanStack Table'ga buni manual* bayroqlari bilan aytamiz:
sequenceDiagram
participant U as Foydalanuvchi
participant T as Jadval holati
participant Q as Query
participant S as Server
U->>T: «Narx» ni bosdi
T->>Q: kalit: [sahifa 1, narx kamayish]
Q->>S: GET ?sahifa=1&tartib=sum.desc
S-->>Q: 5 qator + jami: 23
Q-->>T: data, rowCount
T-->>U: shu 5 qatorni chizadiNimaga qarang: jadval o'zi hech narsa saralamaydi va kesmaydi. U faqat holatni (qaysi sahifa, qaysi tartib) saqlaydi va tugmalarni boshqaradi. Haqiqiy ish — serverda.
| Bayroq | Nima qiladi |
|---|---|
manualPagination: true |
qatorlarni kesmaydi — data allaqachon bitta sahifa deb oladi |
manualSorting: true |
saralamaydi — data allaqachon saralangan |
manualFiltering: true |
filtrlamaydi — data allaqachon filtrlangan |
rowCount |
serverdagi jami qatorlar — sahifalar sonini shundan hisoblaydi |
TanStack hujjatidagi eng ko'p takrorlanadigan ogohlantirish: manual* so'rov yubormaydi. U faqat klientdagi bosqichni o'chiradi. So'rovni siz yuborasiz — Query bilan.
3.2 Query bilan to'liq misol
Soxta server 23 ta buyurtmani saqlaydi, saralash va kesishni o'zi qiladi. Konsolga har "so'rov" yoziladi:
import { useState } from "react";
import {
QueryClient,
QueryClientProvider,
keepPreviousData,
useQuery,
} from "@tanstack/react-query";
import {
createColumnHelper,
rowPaginationFeature,
rowSortingFeature,
tableFeatures,
useTable,
} from "@tanstack/react-table";
// --- Soxta server: saralash va kesish SERVERDA ---
const DISHES = ["Osh", "Manti", "Lag'mon", "Somsa", "Ko'k choy"];
const PRICES = [35000, 30000, 28000, 8000, 5000];
const DB = Array.from({ length: 23 }, (_, i) => ({
id: 101 + i,
dish: DISHES[i % 5],
sum: PRICES[i % 5] * (1 + (i % 3)),
}));
function fetchOrders({ pageIndex, pageSize }, sorting) {
const s = sorting[0];
const order = s ? `${s.id}.${s.desc ? "desc" : "asc"}` : "—";
console.log(`GET sahifa=${pageIndex + 1} tartib=${order}`);
return new Promise((resolve) =>
setTimeout(() => {
const rows = [...DB];
const dir = s?.desc ? -1 : 1;
if (s) rows.sort((a, b) => (a[s.id] - b[s.id]) * dir);
const from = pageIndex * pageSize;
resolve({
rows: rows.slice(from, from + pageSize),
total: DB.length,
});
}, 200),
);
}
// --- Jadval: holat bor, qayta ishlash yo'q (manual) ---
const features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
});
const col = createColumnHelper();
const columns = col.columns([
col.accessor("id", { header: "№", enableSorting: false }),
col.accessor("dish", { header: "Taom", enableSorting: false }),
col.accessor("sum", { header: "Summa" }),
]);
const EMPTY = [];
const queryClient = new QueryClient();
function Orders() {
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 5,
});
const [sorting, setSorting] = useState([]);
const q = useQuery({
queryKey: ["orders", pagination, sorting],
queryFn: () => fetchOrders(pagination, sorting),
placeholderData: keepPreviousData,
});
const table = useTable({
features,
columns,
data: q.data?.rows ?? EMPTY,
rowCount: q.data?.total,
manualPagination: true,
manualSorting: true,
getRowId: (row) => String(row.id),
state: { pagination, sorting },
onPaginationChange: setPagination,
onSortingChange: (updater) => {
setSorting(updater);
// yangi tartib — birinchi sahifadan
setPagination((p) => ({ ...p, pageIndex: 0 }));
},
});
if (q.isPending) return <p role="status">Yuklanmoqda…</p>;
const header = table.getHeaderGroups()[0].headers[2];
return (
<div aria-busy={q.isFetching}>
<button onClick={header.column.getToggleSortingHandler()}>
Summa
</button>
<ul>
{table.getRowModel().rows.map((row) => (
<li key={row.id}>
№{row.original.id} {row.original.dish} —{" "}
{row.original.sum}
</li>
))}
</ul>
<p>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Oldingi
</button>{" "}
{pagination.pageIndex + 1} / {table.getPageCount()} (jami{" "}
{table.getRowCount()}){" "}
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Keyingi
</button>
</p>
</div>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Orders />
</QueryClientProvider>
);
}Konsolda:
GET sahifa=1 tartib=—
GET sahifa=1 tartib=sum.desc
GET sahifa=2 tartib=sum.descEkranda:
Summa
№111 Osh — 70000
№102 Manti — 60000
№117 Manti — 60000
№108 Lag'mon — 56000
№123 Lag'mon — 56000
Oldingi 2 / 5 (jami 23) KeyingiKonsol — "serverga" ketgan so'rovlar. Uchta so'rov, uchta holat:
- Ochilishda — 1-sahifa, tartibsiz.
- «Summa» bosildi — saralash holati o'zgardi, so'rov kaliti (
queryKey) o'zgardi, Query yangi so'rov yubordi. Jadval o'zi hech narsani saralamadi. - «Keyingi» — 2-sahifa, tartib saqlangan.
Ekranda — serverdan kelgan 5 qator va "2 / 5 (jami 23)". Sahifalar sonini jadval rowCount dan hisobladi: 23 ÷ 5 = 4.6 → 5.
3.3 Uch muhim detal
Holat — so'rov kalitida. queryKey: ["orders", pagination, sorting] — har holat bo'lagi kalitda. Birortasini unutsangiz, Query eski keshni ko'rsatadi: tartib o'zgardi, ma'lumot — yo'q (so'rov kalitlari darsidagi qoida).
Eski sahifa yangisi kelguncha turadi. placeholderData: keepPreviousData (TanStack Query IV dan tanish) — «Keyingi» bosilganda jadval bo'shab qolmaydi, 200 ms davomida eski 5 qator ko'rinib turadi. Shu paytda isFetching — true, biz uni aria-busy ga berdik. Ko'rinadigan belgi ham qo'shsa bo'ladi: jadvalni biroz xiralashtirish (Yuklanish holatlari darsidagi "eskirgan" holat).
Tartib o'zgarsa — 1-sahifa. Klient jadvalida buni autoResetPageIndex o'zi qilardi. manualPagination da esa u o'chiq: jadval sahifalar haqiqatan o'zgarganini bilmaydi. Shuning uchun onSortingChange ichida pageIndex: 0 ni o'zimiz qo'ydik. Busiz: 5-sahifada turib tartibni o'zgartirsangiz, yangi tartibning 5-sahifasi ochiladi — foydalanuvchi "eng qimmat" larni ko'rmaydi.
getRowId: (row) => String(row.id) — qator identifikatori. Standart bo'yicha TanStack qatorni indeksi bilan ataydi ("0", "1"…). Server tomonli jadvalda har sahifada indeks yana 0 dan boshlanadi: 1-sahifaning "0" i va 2-sahifaning "0" i — boshqa-boshqa buyurtmalar. Qator tanlashda bu xatoga olib keladi — «Qator tanlash» bo'limida ko'ramiz.
Tekshirib ko'ring: Kodda
manualSorting: trueni olib tashlasak, nima bo'ladi? Ekranda xato bormi?
Javob
Xato chiqmaydi, lekin natija noto'g'ri bo'ladi. Bizning features da sortedRowModel yo'q, shuning uchun bu yerda jadval baribir saralamasdi. Lekin uni qo'shgan bo'lsangiz, jadval serverdan kelgan bitta sahifani yana o'zi saralaydi — faqat shu 5 qator ichida. Bu Sardorning birinchi darsdagi xatosi: "eng qimmat" sahifa ichida qidiriladi. Server tomonli jadvalda klient qator modellari (sortedRowModel, paginatedRowModel) umuman ulanmaydi.
4. Holat — URL'da
4.1 Nega URL
Jasur aka "eng qimmat buyurtmalar, 3-sahifa" ni hisobchiga yubormoqchi. Havolani nusxalaydi — hisobchi esa 1-sahifani, tartibsiz ko'radi. Holat React state'da edi, URL'da emas.
URL — holat saqlash joyi darsidagi qoida: ulashsa, orqaga qaytsa yoki sahifa yangilansa saqlanishi kerak bo'lgan holat — URL'da. Jadval uchun bu sahifa, tartib, filtr. Natija oynasida manzil satri yo'q, shuning uchun xotiradagi router va manzil ekranga chiqariladi:
import {
createMemoryRouter,
RouterProvider,
useLocation,
useSearchParams,
} from "react-router";
import {
createColumnHelper,
createPaginatedRowModel,
createSortedRowModel,
functionalUpdate,
rowPaginationFeature,
rowSortingFeature,
sortFn_basic,
tableFeatures,
useTable,
} from "@tanstack/react-table";
const DISHES = ["Osh", "Manti", "Lag'mon", "Somsa", "Ko'k choy"];
const PRICES = [35000, 30000, 28000, 8000, 5000];
const DB = Array.from({ length: 23 }, (_, i) => ({
id: 101 + i,
dish: DISHES[i % 5],
sum: PRICES[i % 5] * (1 + (i % 3)),
}));
const features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
sortedRowModel: createSortedRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortFns: { basic: sortFn_basic },
});
const col = createColumnHelper();
const columns = col.columns([
col.accessor("id", { header: "№", sortFn: "basic" }),
col.accessor("sum", { header: "Summa", sortFn: "basic" }),
]);
const SORTABLE = new Set(["id", "sum"]); // faqat ma'lum ustunlar
// URL → holat (tekshirib):
// "sum.desc" → [{ id: "sum", desc: true }]
function readSorting(value) {
const [id, dir] = (value ?? "").split(".");
return SORTABLE.has(id) ? [{ id, desc: dir === "desc" }] : [];
}
function readPage(value) {
const page = Number(value);
return Number.isInteger(page) && page >= 1 ? page - 1 : 0;
}
function Orders() {
const [params, setParams] = useSearchParams();
const location = useLocation();
const sorting = readSorting(params.get("tartib"));
const pagination = {
pageIndex: readPage(params.get("sahifa")),
pageSize: 5,
};
const table = useTable({
features,
columns,
data: DB,
autoResetPageIndex: false,
state: { sorting, pagination },
onPaginationChange: (updater) => {
const next = functionalUpdate(updater, pagination);
setParams((p) => {
p.set("sahifa", String(next.pageIndex + 1));
return p;
});
},
onSortingChange: (updater) => {
const [s] = functionalUpdate(updater, sorting);
setParams((p) => {
if (s) p.set("tartib", `${s.id}.${s.desc ? "desc" : "asc"}`);
else p.delete("tartib");
p.delete("sahifa");
return p;
});
},
});
return (
<div>
<p>Manzil: {location.search}</p>
<ul>
{table.getRowModel().rows.map((row) => (
<li key={row.id}>
№{row.original.id} — {row.original.sum}
</li>
))}
</ul>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Keyingi
</button>
</div>
);
}
const router = createMemoryRouter(
[{ path: "/buyurtmalar", element: <Orders /> }],
{ initialEntries: ["/buyurtmalar?sahifa=2&tartib=sum.desc"] },
);
export default function App() {
return <RouterProvider router={router} />;
}Ekranda:
Manzil: ?sahifa=3&tartib=sum.desc
№101 — 35000
№116 — 35000
№107 — 30000
№122 — 30000
№113 — 28000
KeyingiHavola ?sahifa=2&tartib=sum.desc bilan ochildi — jadval darhol 2-sahifani summa bo'yicha kamayish tartibida ko'rsatdi. «Keyingi» — URL'da sahifa=3. Loyihada (createBrowserRouter) bu manzil brauzer satrida turadi: nusxalang, yuboring — hisobchi aynan shuni ko'radi. Orqaga tugmasi esa oldingi sahifaga qaytaradi.
Kodning asosiy g'oyasi: holatning yagona manbai — URL. sorting va pagination har renderda URL'dan o'qiladi, ularni useState da saqlamaymiz. Jadval o'zgartirmoqchi bo'lganda (on…Change) biz URL'ni yangilaymiz, router qayta chizadi, jadval yangi qiymatni o'qiydi. Ikki joyda saqlanmagani uchun ular hech qachon ajralib ketmaydi.
Ikki yangi narsa:
functionalUpdate(updater, eski)— jadvalon…Changega ba'zan tayyor qiymat, ba'zan "eskisidan yangisini yasa" funksiyasini beradi.useStatesetter ikkalasini tushunardi, URL esa yo'q.functionalUpdateikkalasini ham oddiy qiymatga aylantiradi.autoResetPageIndex: false— sahifa endi URL'da, uni jadval o'zi 0 ga tushirmasin. Tartib o'zgarganda sahifani biz o'chiramiz (p.delete("sahifa")— birinchi sahifa).
4.2 Hujumchi nigohi: URL'dagi tartib — serverga
readSorting nega SORTABLE ro'yxatini tekshiradi? URL — foydalanuvchi yozadigan joy. Unga istalgan narsani yozish mumkin: ?tartib=parol.asc yoki undan ham yomoni.
Server tomonli jadvalda bu qiymat serverga boradi. Ma'lumotlar bazasida ustun nomini so'rov parametri qilib bo'lmaydi (qiymatlarni bo'ladi). Shuning uchun dangasa backend uni to'g'ridan-to'g'ri satrga qo'shadi: ORDER BY + tartib. Bu — SQL injection uchun klassik teshik: hujumchi ustun nomi o'rniga SQL bo'lagini yuboradi va bazadan ma'lumot "sug'urib" oladi (sekin, harfma-harf, javob vaqtiga qarab).
Himoya — ikki joyda ham oq ro'yxat (oq ro'yxat g'oyasi):
- klientda — noma'lum ustunni e'tiborsiz qoldirish (bizning
SORTABLE): UI buzilmaydi; - serverda — majburiy:
{ sum: "sum", id: "id" }[tartib]kabi xaritadan olish, topilmasa — 400 javob. Klient tekshiruvi qulaylik, xavfsizlik — server ishi (backend qismida batafsil).
Sahifa raqami ham shunday: readPage ?sahifa=-5 yoki ?sahifa=abc ni 1-sahifaga aylantiradi. Serverda esa hajm ga chegara qo'yiladi — aks holda ?hajm=1000000 bitta so'rov bilan butun bazani tortadi.
5. Qator tanlash
5.1 Id bo'yicha tanlash
Jasur aka bir nechta buyurtmani belgilab, birdaniga "bekor qilish" ni so'radi. Buning uchun qator tanlash (row selection) imkoniyati — rowSelectionFeature. Tanlov holati — obyekt: { "101": true, "103": true }. Kalitlar — qator id'lari, shuning uchun getRowId majburiy. Shu misolda ustunlarni yashirish ham bor — columnVisibilityFeature:
import { useEffect } from "react";
import {
columnVisibilityFeature,
createColumnHelper,
rowSelectionFeature,
tableFeatures,
useTable,
} from "@tanstack/react-table";
const features = tableFeatures({
rowSelectionFeature,
columnVisibilityFeature,
});
const col = createColumnHelper();
const columns = col.columns([
col.display({
id: "select",
header: ({ table }) => (
<input
type="checkbox"
aria-label="Sahifadagi hammasini tanlash"
checked={table.getIsAllPageRowsSelected()}
ref={(el) => {
if (el) {
el.indeterminate = table.getIsSomePageRowsSelected();
}
}}
onChange={table.getToggleAllPageRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
aria-label={`№${row.original.id} ni tanlash`}
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
enableHiding: false,
}),
col.accessor("id", { header: "№", enableHiding: false }),
col.accessor("dish", { header: "Taom" }),
col.accessor("sum", { header: "Summa" }),
]);
const data = [
{ id: 101, dish: "Osh", sum: 35000 },
{ id: 102, dish: "Manti", sum: 60000 },
{ id: 103, dish: "Somsa", sum: 24000 },
];
export default function App() {
const table = useTable({
features,
columns,
data,
getRowId: (row) => String(row.id),
});
const selected = table.state.rowSelection;
useEffect(() => {
console.log("tanlov:", JSON.stringify(selected));
}, [selected]);
const count = Object.keys(selected).length;
return (
<div>
<fieldset>
<legend>Ustunlar</legend>
{table
.getAllLeafColumns()
.filter((c) => c.getCanHide())
.map((c) => (
<label key={c.id}>
<input
type="checkbox"
checked={c.getIsVisible()}
onChange={c.getToggleVisibilityHandler()}
/>{" "}
{c.columnDef.header} ustuni
</label>
))}
</fieldset>
<table>
<thead>
{table.getHeaderGroups().map((g) => (
<tr key={g.id}>
{g.headers.map((h) => (
<th key={h.id} scope="col">
<table.FlexRender header={h} />
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} aria-selected={row.getIsSelected()}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
<table.FlexRender cell={cell} />
</td>
))}
</tr>
))}
</tbody>
</table>
<p role="status">
{count > 0
? `${count} ta tanlandi`
: "Hech narsa tanlanmagan"}
</p>
<button disabled={count === 0}>
Tanlanganlarni bekor qilish
</button>
</div>
);
}Konsolda:
tanlov: {}
tanlov: {"101":true}
tanlov: {"101":true,"103":true}Ekranda:
Ustunlar
Taom ustuni Summa ustuni
№ Taom
101 Osh
102 Manti
103 Somsa
2 ta tanlandi
Tanlanganlarni bekor qilishKodni o'qiymiz:
- Tanlov ustuni —
displayustun. Sarlavhasida "hammasini tanlash", har katakda o'z belgisi.headervacellfunksiya bo'lib,tablevarowni oladi. - Yarim tanlangan holat (indeterminate). Uchtadan ikkitasi tanlangan — sarlavhadagi belgi na "tanlangan", na "bo'sh": unda chiziqcha. HTML'da bu faqat JavaScript xususiyati (
el.indeterminate), atributi yo'q. Shuning uchun callback ref (useRef II). Malika "aralash" (mixed) deb eshitadi. - Har belgining nomi —
aria-label="№101 ni tanlash". "Belgilash katagi, belgilanmagan" — qaysi qator ekani noma'lum bo'lardi. getVisibleCells(),getAllCells()emas. O'tgan darsdagetAllCellsishlatgandik. Ko'rinishni boshqarish ulangan bo'lsa,getAllCellsyashirin ustunlarni ham beradi — "Summa" yashirilgan bo'lsa ham chiziladi. Sarlavhalar esa (getHeaderGroups) ko'rinishni allaqachon hisobga oladi.enableHiding: false— bu ustunni yashirib bo'lmaydi (tanlov va raqam). Ro'yxatdagetCanHide()bilan faqat yashirsa bo'ladiganlar.
5.2 Sahifalar orasida tanlov
Server tomonli jadvalda tanlov sahifalar orasida saqlanadi — chunki u id'lar ro'yxati, qatorlar emas. 1-sahifada №101 ni, 2-sahifada №107 ni tanlasangiz, holatda ikkalasi bor. Lekin ikki savolga javob bering:
| Savol | To'g'ri API |
|---|---|
| hammasi nechta tanlangan? | table.getSelectedRowIds() yoki Object.keys(rowSelection) — id'lar |
| tanlangan qatorlarning ma'lumoti? | table.getSelectedRowModel().rows — faqat yuklangan (shu sahifadagi) qatorlar |
| "hammasini tanlash" nimani tanlaydi? | getToggleAllPageRowsSelectedHandler — shu sahifani; serverdagi hammasini emas |
Uchinchi qator ko'p chalg'itadi. Gmail kabi "shu sahifadagi 50 tasi tanlandi. Hammasini (12 345) tanlashmi?" — bu alohida holat: serverga "filtr bo'yicha hammasi" degan buyruq yuboriladi, id'lar ro'yxati emas.
Diqqat: Tanlov o'z-o'zidan tozalanmaydi. Buyurtma o'chirilsa yoki filtr o'zgarsa, uning id'si tanlovda qoladi va "3 ta tanlandi" yolg'on bo'ladi. O'chirgandan keyin
table.resetRowSelection()yoki tanlovdan o'sha id'ni o'chiring.
6. Ustunlar: qotirish va kenglik
Keng jadvalda (o'nlab ustun) ikki imkoniyat tez-tez kerak bo'ladi. Ikkalasi ham faqat hisob beradi — CSS va dastaklar sizniki (headless).
Qotirish (column pinning) — gorizontal aylantirganda "№" ustuni chapda, "Amallar" o'ngda turib qolsin. v9 da tomonlar mantiqiy: start va end (v8 da left/right edi). Arab tili kabi o'ngdan chapga yoziladigan interfeysda start o'zi o'ng tomonga aylanadi:
const features = tableFeatures({
columnSizingFeature,
columnPinningFeature,
});
// initialState: {
// columnPinning: { start: ["id"], end: ["actions"] },
// }
function pinnedStyle(column) {
const pin = column.getIsPinned(); // false | "start" | "end"
return {
position: pin ? "sticky" : "relative",
insetInlineStart:
pin === "start" ? column.getStart("start") : undefined,
insetInlineEnd:
pin === "end" ? column.getAfter("end") : undefined,
width: column.getSize(),
zIndex: pin ? 1 : 0,
background: "Canvas", // ostidagi kataklar ko'rinmasin
};
}getStart("start") — qotirilgan ustun chap chetdan necha piksel (oldingi qotirilganlar enining yig'indisi). Jadval position: sticky ni o'zi qo'ymaydi — siz style ga berasiz. Bu style xususiyati (CSSOM) — CSP bilan muammosiz.
Kenglikni o'zgartirish (column resizing) — sarlavha chetidagi dastakni tortish. Ikki imkoniyat kerak va tartib muhim: columnSizingFeature (o'lchamlar) oldin, columnResizingFeature (tortish) keyin:
const features = tableFeatures({
columnSizingFeature,
columnResizingFeature,
});
// useTable({ …, columnResizeMode: "onChange" })
<th style={{ width: header.getSize() }}>
<table.FlexRender header={header} />
<div
className="resizer"
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
/>
</th>Hujjatdagi tuzoq: dastakni onPointerDown ga ulash yetmaydi — kutubxona sichqoncha va sensorli ekranni alohida hodisalar bilan farqlaydi. Ikkalasini ham ulang. A11y'ni ham unutmang: tortish — faqat sichqoncha/barmoq. Bobur uchun muqobil yo'l kerak: masalan, ustun menyusida "Kengaytirish / Toraytirish" bandlari (column.setSize, ya'ni table.setColumnSizing).
7. Klientda ko'p qator: virtualizatsiya
Ba'zan server tomonli yo'l mos kelmaydi: ma'lumot oflayn kerak yoki foydalanuvchi bitta uzun ro'yxatni aylantirishni xohlaydi. O'lchovdagi "10 000, hammasi" — 321 ms, shundan ~290 ms DOM chizish (sahifa bilan — 31 ms). Virtualizatsiya bu narxni olib tashlaydi: DOM'da faqat ko'rinadigan ~20 qator.
18-qismda o'z useOynaVirtual hookimizni yozganmiz. Jadvallar bilan ko'pincha TanStack Virtual (@tanstack/react-virtual, 3.14) ishlatiladi. Jadval va virtualizator — ikki alohida kutubxona: jadval qatorlarni beradi, virtualizator ulardan qaysilarini chizishni:
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualRows({ table, scrollRef }) {
const rows = table.getRowModel().rows;
const v = useVirtualizer({
count: rows.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => 36, // qator balandligi, px
overscan: 5,
});
const items = v.getVirtualItems();
const top = items[0]?.start ?? 0;
const bottom = v.getTotalSize() - (items.at(-1)?.end ?? 0);
return (
<tbody>
{top > 0 && <tr style={{ height: top }} aria-hidden="true" />}
{items.map((item) => {
const row = rows[item.index];
return (
<tr key={row.id} aria-rowindex={item.index + 2}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
<table.FlexRender cell={cell} />
</td>
))}
</tr>
);
})}
{bottom > 0 && (
<tr style={{ height: bottom }} aria-hidden="true" />
)}
</tbody>
);
}
// <table aria-rowcount={rows.length + 1}> … </table>Uch nuqta:
- Bo'sh "to'ldiruvchi" qatorlar (tepada va pastda) — aylantirish chizig'i to'g'ri uzunlikda bo'lsin. Ular
aria-hidden. Bu usul<table>semantikasini saqlaydi; ba'zi misollardadivvaposition: absoluteishlatiladi — u holdarole="table",role="row"ni qo'lda yozasiz. aria-rowcountvaaria-rowindex— DOM'da 20 qator bor, lekin Malika "10 000 qatordan 5 001-qator" ni eshitishi kerak. 18-qismdagiaria-setsize/aria-posinsetning jadvaldagi juftligi.+ 1va+ 2— sarlavha qatori ham sanaladi (u 1-qator).- Qayta renderni toraytirish. 10 000 qatorli jadvalda har holat o'zgarishida (masalan, bitta belgi tanlandi) butun komponent qayta chizilmasin:
useTable(options, selector)ning ikkinchi argumenti va<table.Subscribe>faqat kerakli bo'lakka obuna qiladi.
8. Tayyor grid'lar: AG Grid va MUI DataGrid
TanStack Table — headless: hamma narsani o'zingiz chizasiz. Ba'zi loyihalarga esa Excel'ga o'xshash tayyor grid kerak: katakni joyida tahrirlash, ustunlarni sudrab joylashtirish, guruhlash, pivot jadval, Excel'ga eksport, million qatorlik virtualizatsiya — hammasi qutidan.
| AG Grid | MUI DataGrid | |
|---|---|---|
| Paket | ag-grid-react (36.2) |
@mui/x-data-grid (8.29) |
| Bepul qismi | Community — MIT | Data Grid — MIT |
| Pullik qismi | Enterprise: guruhlash, pivot, Excel, server modeli | Pro / Premium: guruhlash, Excel, katta ma'lumot |
| Mos keladi | murakkab moliya, logistika, ERP jadvallari | MUI dizaynidagi loyihalar (MUI darsi) |
Kanon ularni o'lchamadi: vazifalar da bunday jadval yo'q. Tanlashdan oldin o'z sharoitingizda o'lchang: to'plam hajmi, litsenziya narxi va Radix Primitives chuqur darsidagi savol — "kutubxona mening CSP'm bilan ishlaydimi?". Katta grid'lar ko'pincha o'z mavzu CSS'i bilan keladi — uning qanday ulanishini tekshiring.
Qachon headless, qachon tayyor grid? Jadval — dizayn tizimining bir qismi va funksiyalari oddiy (saralash, filtr, sahifa, tanlov) bo'lsa — TanStack Table. Jadval — mahsulotning asosiy ishi (moliya hisoboti, ombor) va foydalanuvchi Excel'dagidek ishlashni kutsa — tayyor grid va uning litsenziyasi o'zini oqlaydi.
9. Tajriba: vazifalar da server tomonli jadval bormi?
Bu darsda vazifalar kodi o'zgarmaydi.
Nega kanonda yo'q. vazifalar API'sida sahifalash yo'q: server butun ro'yxatni bitta javobda beradi. Bu ataylab — ilova butun ro'yxatga muhtoj: qidiruv hamma vazifa bo'yicha ishlaydi, statistika hammasini sanaydi, teglar hammasidan yig'iladi (TanStack Query IV darsidagi sabab). Uzun ro'yxat muammosini esa 18-qismdagi virtual ro'yxat hal qilgan. Teglar jadvali — o'nlab qator, klient jadvali yetarli (o'tgan dars).
Biz sinab ko'rdik. Kanon server tomonli modelni alohida tajribada o'lchadi — TanStack Table saralash va manualPagination bilan (tajriba/kitlar/src/k/jadval-server.tsx, kanondan aynan, qatorlar o'qish uchun bo'lingan):
// #15 TanStack Table 9.2.6: saralash + manualPagination
// (server tomoni modeli)
const f = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
sortedRowModel: createSortedRowModel(),
sortFns: { basic: sortFn_basic },
});
const h = createColumnHelper<typeof f, V>();
const cols = h.columns([
h.accessor("id", { header: "Id", sortFn: "basic" }),
h.accessor("matn", { header: "Matn", sortFn: "basic" }),
]);
const data = Array.from({ length: 10 }, (_, i) => ({
id: i,
matn: "V" + i,
}));
function T() {
const [pagination, setP] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const t = useTable({
features: f,
columns: cols,
data,
rowCount: 1000,
manualPagination: true,
state: { pagination },
onPaginationChange: setP,
});
// … jadval, «Keyingi» tugmasi,
// {pagination.pageIndex + 1}/{t.getPageCount()}
}rowCount: 1000, data esa 10 ta — server "1 000 tadan 10 tasini" bergandek. Natija (natijalar/tajriba/kutubxonalar.txt, aynan):
asos JS 67719 (+ 0.0 KB) CSS 0 | CSP 0 | xato: yo'q
jadval-server JS 80446 (+ 12.4 KB) CSS 0 | CSP 0 | xato: yo'q+12.4 KB gzip — TanStack Table, saralash va sahifalash bilan. vazifalar dagi teglar jadvali (faqat saralash) statistika chunk'ini +12.2 KB oshirgan edi — deyarli bir xil: sahifalash imkoniyati arzon, asosiy narx — jadval yadrosi. CSP — 0 buzilish: headless kutubxona <style> qo'shmaydi.
10. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| saralash faqat shu sahifa ichida | server tomonli jadvalda sortedRowModel ulangan |
manualSorting: true, klient modellarisiz |
| "1 / 1", «Keyingi» o'chiq, serverda 23 ta | rowCount (yoki pageCount) berilmagan |
serverdan total → rowCount |
| tartib o'zgardi, ma'lumot — eski | holat queryKey da yo'q |
queryKey: [..., pagination, sorting] |
| tartib o'zgarganda 5-sahifada qoldi | manualPagination da avtomatik tiklash yo'q |
onSortingChange da pageIndex: 0 |
| «Keyingi» da jadval bir lahza bo'shaydi | yangi kalit — yangi kesh | placeholderData: keepPreviousData |
| 2-sahifada tanlangan "0" — boshqa buyurtma | id o'rniga indeks | getRowId: (row) => String(row.id) |
| yashirilgan ustun baribir chiziladi | getAllCells() |
getVisibleCells() |
| o'chirilganlar ham "tanlangan" | tanlov o'zi tozalanmaydi | resetRowSelection() |
| havola ulashilsa — 1-sahifa | holat useState da |
URL — yagona manba |
?tartib=… bilan server xatosi yoki SQL injection |
URL qiymati tekshirilmagan | oq ro'yxat: klientda va serverda |
11. Mashqlar
Mashqlar kurs/mashqlar/20/15-server-jadval/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): qaysi sozlama?
- Server ma'lumotni allaqachon saralab berdi, jadval qayta saralamasin →
- Server "jami 12 345 ta" dedi, jadval sahifalar sonini bilsin →
- Qatorlar id'si bazadagi
idbo'lsin, indeks emas → - Sahifa URL'da, jadval uni o'zi 0 ga tushirmasin →
Yechim
manualSorting: true— klient saralash bosqichi o'chadi.rowCount: 12345—getPageCount()vagetCanNextPage()shundan hisoblanadi.getRowId: (row) => String(row.id)— tanlov vakeybarqaror.autoResetPageIndex: false— sahifa egasi URL.
2-mashq (o'rta): server tomonli qidiruv
«Query bilan to'liq misol» bo'limidagi jadvalga taom nomi bo'yicha qidiruv qo'shing — u ham serverda ishlasin. Konsoldagi har "so'rov" qidiruv qiymatini ham ko'rsatsin: GET sahifa=1 qidiruv=manti. Qidiruv o'zgarsa — 1-sahifa.
Ishora: qidiruv — alohida useState (yoki jadvalning globalFilter i — globalFilteringFeature va manualFiltering: true bilan). U queryKey ga ham, fetchOrders ga ham kiradi. Sahifani nolga tushirish — onChange ichida. Haqiqiy loyihada qidiruvni debounce qiling: har harfga so'rov ketmasin.
Yechim
import { useState } from "react";
import {
QueryClient,
QueryClientProvider,
keepPreviousData,
useQuery,
} from "@tanstack/react-query";
import {
createColumnHelper,
rowPaginationFeature,
tableFeatures,
useTable,
} from "@tanstack/react-table";
const DISHES = ["Osh", "Manti", "Lag'mon", "Somsa", "Ko'k choy"];
const DB = Array.from({ length: 23 }, (_, i) => ({
id: 101 + i,
dish: DISHES[i % 5],
}));
function fetchOrders({ pageIndex, pageSize }, search) {
const shown = search || "—";
console.log(`GET sahifa=${pageIndex + 1} qidiruv=${shown}`);
return new Promise((resolve) =>
setTimeout(() => {
const q = search.trim().toLowerCase();
const rows = DB.filter((o) => o.dish.toLowerCase().includes(q));
const from = pageIndex * pageSize;
resolve({
rows: rows.slice(from, from + pageSize),
total: rows.length,
});
}, 200),
);
}
const features = tableFeatures({ rowPaginationFeature });
const col = createColumnHelper();
const columns = col.columns([
col.accessor("id", { header: "№" }),
col.accessor("dish", { header: "Taom" }),
]);
const EMPTY = [];
const queryClient = new QueryClient();
function Orders() {
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 5,
});
const [search, setSearch] = useState("");
const q = useQuery({
queryKey: ["orders", pagination, search],
queryFn: () => fetchOrders(pagination, search),
placeholderData: keepPreviousData,
});
const table = useTable({
features,
columns,
data: q.data?.rows ?? EMPTY,
rowCount: q.data?.total,
manualPagination: true,
getRowId: (row) => String(row.id),
state: { pagination },
onPaginationChange: setPagination,
});
return (
<div>
<input
aria-label="Qidiruv"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
/>
<ul>
{table.getRowModel().rows.map((row) => (
<li key={row.id}>
№{row.original.id} {row.original.dish}
</li>
))}
</ul>
<p>
{pagination.pageIndex + 1} / {table.getPageCount()}{" "}
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Keyingi
</button>
</p>
</div>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Orders />
</QueryClientProvider>
);
}Konsolda:
GET sahifa=1 qidiruv=—
GET sahifa=2 qidiruv=—
GET sahifa=1 qidiruv=mantiEkranda:
№102 Manti
№107 Manti
№112 Manti
№117 Manti
№122 Manti
1 / 1 KeyingiKonsolga qarang: 2-sahifada turib «manti» yozildi va so'rov 1-sahifa uchun ketdi — onChange da sahifani nolga tushirdik. Bo'lmasa so'rov "manti, 2-sahifa" bo'lardi, server esa bo'sh ro'yxat qaytarardi (beshta Manti bitta sahifaga sig'adi). total endi filtrlangan son (5) — sahifalar soni ham shundan: "1 / 1".
Tekshiruvda «manti» birdaniga qo'yildi — bitta so'rov. Haqiqiy foydalanuvchi esa harfma-harf yozadi: m, ma, man… — beshta so'rov. Shuning uchun haqiqiy loyihada debounce kerak.
3-mashq (qiyin): sahifalar orasida tanlov
Buyurtmalar jadvali (23 ta, sahifada 5 ta, klient sahifalash yetadi). Talablar:
- Har qatorda tanlash belgisi,
aria-label— «№101 ni tanlash». - Pastda: «Jami tanlangan: N (shu sahifada: M)».
- «Tanlovni tozalash» tugmasi — hammasini olib tashlaydi.
- 1-sahifada №101 ni, 2-sahifada №106 ni tanlang — jami 2, sahifada 1 bo'lsin.
Ishora: «Qator tanlash» bo'limi. Jami — table.getSelectedRowIds().length; shu sahifada — table.getRowModel().rows ichidan row.getIsSelected() lar soni (filter). Tozalash — table.resetRowSelection(). getRowId ni unutmang.
Yechim
import {
createColumnHelper,
createPaginatedRowModel,
rowPaginationFeature,
rowSelectionFeature,
tableFeatures,
useTable,
} from "@tanstack/react-table";
const features = tableFeatures({
rowSelectionFeature,
rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel(),
});
const col = createColumnHelper();
const columns = col.columns([col.accessor("id", { header: "№" })]);
const data = Array.from({ length: 23 }, (_, i) => ({ id: 101 + i }));
export default function App() {
const table = useTable({
features,
columns,
data,
getRowId: (row) => String(row.id),
initialState: { pagination: { pageIndex: 0, pageSize: 5 } },
});
const rows = table.getRowModel().rows;
const total = table.getSelectedRowIds().length;
const onPage = rows.filter((row) => row.getIsSelected()).length;
return (
<div>
<ul>
{rows.map((row) => (
<li key={row.id}>
<input
type="checkbox"
aria-label={`№${row.original.id} ni tanlash`}
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>{" "}
№{row.original.id}
</li>
))}
</ul>
<p role="status">
Jami tanlangan: {total} (shu sahifada: {onPage})
</p>
<button onClick={() => table.nextPage()}>Keyingi</button>{" "}
<button onClick={() => table.resetRowSelection()}>
Tanlovni tozalash
</button>
</div>
);
}Ekranda:
№106
№107
№108
№109
№110
Jami tanlangan: 2 (shu sahifada: 1)
Keyingi Tanlovni tozalashEkranda 2-sahifa. Unda №106 tanlangan (belgining o'zi ekran matnida ko'rinmaydi). Birinchi sahifadagi №101 ham hisobda. getSelectedRowIds() id'larni sanaydi, shuning uchun sahifa almashsa ham tanlov yo'qolmaydi. getRowId bo'lmaganda 2-sahifadagi birinchi qatorning id'si "5" bo'lardi va bitta sahifadan ikkinchisiga o'tganda chalkashlik boshlanardi.
12. Real ishda
- Admin panellar va CRM — buyurtmalar, to'lovlar, foydalanuvchilar, loglar: deyarli doim server tomonli jadval. Backend API'si
?sahifa=&hajm=&tartib=&qidiruv=ni qabul qiladi va{ rows, total }qaytaradi (backend qismida o'zingiz yozasiz). - Ulashiladigan havolalar — "filtrlangan hisobot"ni boshliqqa yuborish real ehtiyoj: holat URL'da bo'lmasa, havola ishlamaydi.
- Ommaviy amallar — tanlab o'chirish, eksport, holatni o'zgartirish. Tanlov id'lari serverga yuboriladi; "hammasini tanlash" esa filtr bilan.
- Intervyu savollari: "Klient va server tomonli jadval qachon?", "
manualPaginationnima qiladi va nima qilmaydi?", "Jadval holatini URL'da qanday saqlaysiz?", "Server tomonli jadvalda qator tanlash qanday ishlaydi?", "ORDER BYga foydalanuvchi qiymatini qo'shsa nima bo'ladi?".
Xulosa
- O'lchov: 100 000 qator — JSON ~4 MB, klientda filtr + saralash ~0.4 s (dev build); to'liq chizish 10 000 qatorda ~0.3 s, 100 000 da ~2.8 s. Ma'lumot o'sadigan jadval — server tomonli.
manualSorting,manualPagination,manualFiltering— klient bosqichini o'chiradi, so'rov yubormaydi;rowCount— sahifalar soni uchun. Holat —queryKeyda, eski sahifa —keepPreviousData, tartib o'zgarsa — 1-sahifa.- URL — yagona manba: holat o'qiladi,
on…ChangeURL'ni yangilaydi (functionalUpdate); URL qiymati oq ro'yxat bilan tekshiriladi (klientda va serverda). - Qator tanlash —
getRowIdbilan id'lar; jami —getSelectedRowIds(), yuklanganlari —getSelectedRowModel(); yarim holat —indeterminate; o'chirganda — tozalash. - Ustunlar: ko'rinish (
getVisibleCells), qotirish (start/end, sticky — sizda), kenglik (sichqoncha + sensor, klaviatura muqobili). - Ko'p qator klientda — virtualizatsiya (
aria-rowcount/aria-rowindex); Excel'ga o'xshash grid kerak bo'lsa — AG Grid, MUI DataGrid (litsenziya va CSP'ni tekshiring).
Keyingi dars: Cheksiz scroll va Intersection Observer — sahifa tugmalari o'rniga ro'yxat oxiriga yetganda yuklash: react-intersection-observer, rootMargin, qorovul element va cheksiz lentaning a11y narxi.
Manbalar
- TanStack Table v9: Pagination, Sorting (manual), Row Selection, Column Visibility, Column Pinning, Column Sizing/Resizing, with TanStack Query, Virtualization — tanstack.com/table (9.2.6); paket ichidagi
skills/hujjatlari - TanStack Query: Paginated Queries (
placeholderData) — tanstack.com/query (5.104.1) - TanStack Virtual — tanstack.com/virtual (3.14)
- WAI-ARIA:
aria-rowcount,aria-rowindex,aria-selected— w3.org/TR/wai-aria-1.2 - OWASP: SQL Injection Prevention Cheat Sheet (ORDER BY va oq ro'yxat) — cheatsheetseries.owasp.org
- AG Grid (ag-grid.com) va MUI X Data Grid (mui.com/x) — litsenziya sahifalari
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!