Mundarija (23)
- Bu darsda
- 1. Nega bu kerak?
- 2. Raqamli sahifalar
- 2.1 Sahifa — kalitning bir qismi
- 2.2 placeholderData: keepPreviousData
- 2.3 Sahifa raqami — URL'da
- 3. Cheksiz ro'yxat: useInfiniteQuery
- 3.1 Nega oddiy useQuery yetmaydi
- 3.2 «Yana yuklash»
- 3.3 Tuzoq: cheksiz ro'yxatni qayta yuklash
- 4. Offset yoki kursor
- 4.1 Offset tuzog'i — takror element
- 4.2 Kursor
- 5. Cheksiz aylantirish: IntersectionObserver
- 6. Tajriba: vazifalar da sahifalash bormi?
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): offset yoki kursor?
- 2-mashq (o'rta): sakramaydigan sahifalar
- 3-mashq (qiyin): kursorli «Yana yuklash»
- 9. Real ishda
- Xulosa
- Manbalar
TanStack Query sahifalash: keepPreviousData, useInfiniteQuery va cheksiz ro'yxat
Qisqacha: Ma'lumot ko'p bo'lsa, server uni bo'laklab beradi. Raqamli sahifalarda
useQueryning kalitiga sahifa raqami qo'yiladi vaplaceholderData: keepPreviousDatayangi sahifa kelguncha eskisini ekranda ushlab turadi. "Yana yuklash" va cheksiz aylantirishda —useInfiniteQuery: u hamma bo'laklarni bitta keshdapagesmassivi qilib saqlaydi, keyingisinigetNextPageParamaytadi.
Bu darsda
- Sahifa raqami kalitda bo'lgan
useQueryyozasiz va "Yuklanmoqda…" miltillashinikeepPreviousDatabilan yo'qotasiz. useInfiniteQuerybilan «Yana yuklash» tugmasini qurasiz:pages,fetchNextPage,hasNextPage.- Offset va kursorli sahifalashni solishtirasiz va offset nega takror element berishini ko'rasiz.
IntersectionObserverbilan ro'yxat oxiriga yetganda avtomatik yuklashni yozasiz.vazifalarda nega sahifalash yo'qligini va tajriba nima ko'rsatganini bilasiz.
Oldin bilishingiz kerak: TanStack Query III: mutatsiyalar, TanStack Query II: kesh hayot sikli, IntersectionObserver, URL — holat saqlash joyi.
1. Nega bu kerak?
«Bahor» endi faqat restoran emas — yetkazib berish ham qiladi. Sharhlar sahifasida 2 000 ta sharh to'plandi. Hammasini bitta so'rovda yuborish — telefonda bir necha megabayt va sekundlab kutish. Mehmon esa birinchi o'ntasini o'qiydi, xolos.
Shuning uchun server ma'lumotni bo'laklab beradi. Har bo'lak — sahifa (page). Buni sahifalash (pagination) deyishadi. Siz buni Algoritmlar real frontend va backend ishida darsida ham ko'rgansiz: offset va kursor.
Interfeysda ikki xil ko'rinish bor:
- raqamli sahifalar — "1 2 3 … Keyingi". Qidiruv natijalari, admin jadvallari. Foydalanuvchi aniq sahifaga qaytishi mumkin.
- cheksiz ro'yxat — pastga aylantirasiz, yangilari qo'shilib boradi. Telegram kanali, Instagram lentasi. Yoki «Yana yuklash» tugmasi.
O'xshatish: kutubxonadagi katalog. Raqamli sahifalar — kitob katalogining varaqlari: 5-varaqni ochasiz, 4-varaq yopiladi. Cheksiz ro'yxat — o'ralgan qog'oz: tortgan sari uzayadi, boshi qo'lingizda qoladi.
2. Raqamli sahifalar
2.1 Sahifa — kalitning bir qismi
TanStack Query I dagi qoida: funksiya nimaga bog'liq bo'lsa, o'sha kalitda bo'ladi. Sahifa raqami ham shunday. Natija oynasida server — 7 ta taom, har sahifada 3 ta:
import { useState } from "react";
import {
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
const MENU = [
"Osh", "Lag'mon", "Manti", "Somsa",
"Sho'rva", "Norin", "Chuchvara",
];
const SIZE = 3;
function fetchPage(page) {
console.log("sahifa so'raldi:", page);
return new Promise((resolve) =>
setTimeout(
() =>
resolve({
items: MENU.slice((page - 1) * SIZE, page * SIZE),
hasMore: page * SIZE < MENU.length,
}),
200,
),
);
}
function Menu() {
const [page, setPage] = useState(1);
const { isPending, data } = useQuery({
queryKey: ["menu", page],
queryFn: () => fetchPage(page),
});
return (
<>
<p>{isPending ? "Yuklanmoqda…" : data.items.join(", ")}</p>
<p>
<button onClick={() => setPage(page - 1)}>
Oldingi
</button>{" "}
{page}-sahifa{" "}
<button onClick={() => setPage(page + 1)}>Keyingi</button>
</p>
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Menu />
</QueryClientProvider>
);
}Konsolda:
sahifa so'raldi: 1
sahifa so'raldi: 2Ekranda:
Somsa, Sho'rva, Norin
Oldingi 2-sahifa KeyingiIshlaydi. Lekin «Keyingi» ni bosganda ro'yxat butunlay yo'qolib, "Yuklanmoqda…" ga almashdi (oraliq nuqta buni tekshirdi). Kalit ["menu", 2] — yangi so'rov, keshda hali hech narsa yo'q, demak isPending. Har sahifada ekran "sakraydi": ro'yxat yo'qoladi, tugmalar yuqoriga ko'tariladi, keyin yana tushadi. Bu CLS ga ham yomon.
Yana ikki kamchilik: 1-sahifada «Oldingi» va oxirgi sahifada «Keyingi» ishlayveradi. Ularni keyingi misolda tuzatamiz.
2.2 placeholderData: keepPreviousData
placeholderData — so'rov tugaguncha ekranda turadigan "vaqtinchalik" ma'lumot. U keshga yozilmaydi, faqat ko'rsatiladi. Unga funksiya berilsa, Query unga oldingi kalitning ma'lumotini beradi. keepPreviousData — Query'ning tayyor funksiyasi: "oldingisini qaytar".
Natija: 2-sahifa kelguncha 1-sahifa ekranda qoladi, isPending emas, isPlaceholderData rost bo'ladi:
import { useState } from "react";
import {
keepPreviousData,
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
const MENU = [
"Osh", "Lag'mon", "Manti", "Somsa",
"Sho'rva", "Norin", "Chuchvara",
];
const SIZE = 3;
function fetchPage(page) {
return new Promise((resolve) =>
setTimeout(
() =>
resolve({
items: MENU.slice((page - 1) * SIZE, page * SIZE),
hasMore: page * SIZE < MENU.length,
}),
200,
),
);
}
function Menu() {
const [page, setPage] = useState(1);
const { isPending, data, isPlaceholderData } = useQuery({
queryKey: ["menu", page],
queryFn: () => fetchPage(page),
placeholderData: keepPreviousData,
});
if (isPending) return <p>Yuklanmoqda…</p>;
return (
<>
<p style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
{data.items.join(", ")}
{isPlaceholderData && " (yuklanmoqda…)"}
</p>
<p>
<button
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
Oldingi
</button>{" "}
{page}-sahifa{" "}
<button
disabled={isPlaceholderData || !data.hasMore}
onClick={() => setPage(page + 1)}
>
Keyingi
</button>
</p>
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Menu />
</QueryClientProvider>
);
}Ekranda:
Chuchvara
Oldingi 3-sahifa Keyingi«Keyingi» bosilgach 1-sahifaning taomlari xira holda va "(yuklanmoqda…)" bilan qoldi — sakrash yo'q. 3-sahifada faqat bitta taom va «Keyingi» o'chiq (hasMore: false).
Tugmadagi ikki shartga e'tibor bering:
!data.hasMore— server "boshqa yo'q" desa, keyingi sahifa yo'q. Bu ma'lumotni server beradi; mijozMENU.lengthni bilmaydi.isPlaceholderData— ekranda hali eski sahifa. Bu paytdadata.hasMoreham eski sahifaniki. Uni tekshirmasak, tez-tez bosgan foydalanuvchi mavjud bo'lmagan sahifaga o'tib ketishi mumkin.
placeholderData va oldingi darsdagi initialData farqi muhim:
initialData |
placeholderData |
|
|---|---|---|
| keshga yoziladimi | ha | yo'q |
staleTime ga ta'siri |
hisoblanadi | yo'q, so'rov doim ketadi |
| qachon | ma'lumot haqiqatan bor (localStorage, server) | "hozircha shuni ko'rsat" |
vazifalar dagi initialData — localStorage'dagi haqiqiy ro'yxat. Sahifalashdagi eski sahifa esa yangi sahifaning ma'lumoti emas — shuning uchun placeholderData.
2.3 Sahifa raqami — URL'da
Hozir page — useState. Mehmon 3-sahifani do'stiga yubormoqchi bo'lsa — havolada sahifa yo'q. Yangilasa — 1-sahifaga qaytadi. URL — holat saqlash joyi darsidagi qoida: foydalanuvchi ulashishi yoki «Orqaga» bilan qaytishi mumkin bo'lgan holat — URL'da. Raqamli sahifalashda ?sahifa=3 — useSearchParams bilan:
const [params, setParams] = useSearchParams();
const page = Number(params.get("sahifa") ?? "1");
const { data } = useQuery({
queryKey: ["menu", page],
queryFn: () => fetchPage(page),
placeholderData: keepPreviousData,
});
// «Keyingi»: setParams({ sahifa: String(page + 1) })"Hujumchi nigohi" o'sha darsdagidek: ?sahifa=-5 yoki ?sahifa=abc ham keladi. Number.isInteger(page) && page >= 1 bilan tekshiring, aks holda 1.
Tekshirib ko'ring:
keepPreviousDatabilan 1-sahifadan 2-ga o'tdingiz, keyin 1-ga qaytdingiz. Qaytganda nima ko'rinadi va so'rov ketadimi?
Javob
1-sahifa darhol, xira emas — ["menu", 1] keshda bor (gcTime 5 daqiqa), isPlaceholderData yolg'on. So'rov esa ketadi: staleTime sukutda 0, ma'lumot eskirgan — fonda yangilanadi. staleTime ni oshirsangiz, sahifalar orasida yurish so'rovsiz bo'ladi.
3. Cheksiz ro'yxat: useInfiniteQuery
3.1 Nega oddiy useQuery yetmaydi
«Yana yuklash» da 1-sahifa yo'qolmasligi kerak: ekranda 1 + 2 + 3-sahifa birga. useQuery bilan har sahifa alohida kalitda — ularni qo'lda yig'ish, tartibini saqlash, qaysi biri oxirgi ekanini bilish kerak bo'lardi.
useInfiniteQuery — hamma bo'laklarni bitta kalitda saqlaydi. Keshdagi ma'lumot shakli:
flowchart LR
K["kalit: ['feed']"] --> D["data"]
D --> P["pages"]
D --> PP["pageParams"]
P --> P1["1-bo'lak: 3 ta"]
P --> P2["2-bo'lak: 3 ta"]
PP --> X1["0"]
PP --> X2["3"]pages — kelgan bo'laklar tartib bilan. pageParams — har bo'lak qaysi parametr bilan so'ralgani. Parametr — offset, sahifa raqami yoki kursor, qaysi biri ekanini o'zingiz hal qilasiz.
3.2 «Yana yuklash»
Uch yangi sozlama:
initialPageParam— birinchi bo'lak parametri (bu yerda offset 0).queryFn: ({ pageParam }) => …— Query parametrni kontekstda beradi.getNextPageParam(oxirgiBolak, hammaBolaklar)— keyingi parametrni hisoblaydi.undefinedyokinullqaytarsa — boshqa bo'lak yo'q.
import {
QueryClient,
QueryClientProvider,
useInfiniteQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
const MENU = [
"Osh", "Lag'mon", "Manti", "Somsa",
"Sho'rva", "Norin", "Chuchvara",
];
function fetchChunk(offset) {
console.log("offset:", offset);
return new Promise((resolve) =>
setTimeout(
() =>
resolve({ items: MENU.slice(offset, offset + 3), total: 7 }),
200,
),
);
}
function Feed() {
const query = useInfiniteQuery({
queryKey: ["feed"],
queryFn: ({ pageParam }) => fetchChunk(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
const loaded = allPages.reduce((n, p) => n + p.items.length, 0);
return loaded < lastPage.total ? loaded : undefined;
},
});
if (query.isPending) return <p>Yuklanmoqda…</p>;
const items = query.data.pages.flatMap((p) => p.items);
return (
<>
<ol>
{items.map((name) => (
<li key={name}>{name}</li>
))}
</ol>
<button
disabled={!query.hasNextPage || query.isFetchingNextPage}
onClick={() => query.fetchNextPage()}
>
{query.isFetchingNextPage
? "Yuklanmoqda…"
: query.hasNextPage
? "Yana yuklash"
: "Hammasi ko'rsatildi"}
</button>
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Feed />
</QueryClientProvider>
);
}Konsolda:
offset: 0
offset: 3
offset: 6Ekranda:
Osh
Lag'mon
Manti
Somsa
Sho'rva
Norin
Chuchvara
Hammasi ko'rsatildiQatorma-qator:
| Bosish | pageParam |
pages |
hasNextPage |
|---|---|---|---|
| ochilish | 0 | 1 bo'lak (3) | rost: 3 < 7 |
| 1-«Yana» | 3 | 2 bo'lak (6) | rost: 6 < 7 |
| 2-«Yana» | 6 | 3 bo'lak (7) | yolg'on: undefined |
Yangi maydonlar:
data.pages— bo'laklar massivi. Ekranga bitta ro'yxat kerak —flatMap(flat va flatMap) bilan yoyamiz.fetchNextPage()— keyingi bo'lakni so'raydi.hasNextPage—getNextPageParamundefinedqaytarmadimi.isFetchingNextPage— aynan keyingi bo'lak yo'lda.isFetchingdan farqi:isFetchingfonda butun ro'yxat yangilanganda ham rost.
Teskari yo'nalish ham bor: getPreviousPageParam, fetchPreviousPage, hasPreviousPage. Chat ilovasida kerak — yuqoriga aylantirsangiz, eski xabarlar yuklanadi.
3.3 Tuzoq: cheksiz ro'yxatni qayta yuklash
Cheksiz ro'yxat eskirsa (oynaga qaytdingiz, invalidatsiya), Query uni qanday yangilaydi? Har bo'lakni alohida emas — birinchisidan boshlab ketma-ket, getNextPageParam ni qayta hisoblab. Nega: o'rtada element qo'shilgan bo'lsa, eski parametrlar endi to'g'ri emas.
Oqibati — 20 ta bo'lak yuklangan bo'lsa, oynaga qaytish 20 ta ketma-ket so'rov. Ikki himoya bor:
staleTimeni oshirish — lenta har tabga qaytganda yangilanmasin.maxPages— keshda nechta bo'lak saqlansin.maxPages: 5— oltinchisi kelganda birinchisi tashlanadi. Xotira ham, qayta yuklash ham cheklanadi. Shart —getPreviousPageParamham yozilgan bo'lishi: foydalanuvchi yuqoriga qaytsa, tashlangan bo'lak qayta so'raladi.
4. Offset yoki kursor
4.1 Offset tuzog'i — takror element
Offset (siljish) — "nechtasini o'tkazib yubor": ?offset=3&limit=3. Oddiy va tushunarli. Lekin ro'yxat o'zgarib tursa, muammo bor.
Sharhlar yangisidan eskisiga tartiblangan. Mehmon 1-bo'lakni (3 ta) ko'rdi. Shu payt boshqa mehmon yangi sharh yozdi — u ro'yxat boshiga tushdi. Endi "3 tani o'tkazib yubor" — eski 3-sharh 4-o'ringa surildi va ikkinchi bo'lakda yana keladi:
import {
QueryClient,
QueryClientProvider,
useInfiniteQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
// Server: yangisi boshida
const reviews = [
"Sharh 6", "Sharh 5", "Sharh 4", "Sharh 3", "Sharh 2",
];
function fetchByOffset(offset) {
return new Promise((resolve) =>
setTimeout(() => resolve(reviews.slice(offset, offset + 3)), 200),
);
}
function Reviews() {
const query = useInfiniteQuery({
queryKey: ["reviews"],
queryFn: ({ pageParam }) => fetchByOffset(pageParam),
initialPageParam: 0,
getNextPageParam: (last, all) =>
last.length === 3 ? all.length * 3 : undefined,
});
if (query.isPending) return <p>Yuklanmoqda…</p>;
const items = query.data.pages.flat();
return (
<>
<p>{items.join(", ")}</p>
<button onClick={() => reviews.unshift("Sharh 7")}>
Yangi sharh yozildi
</button>{" "}
<button onClick={() => query.fetchNextPage()}>
Yana yuklash
</button>
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Reviews />
</QueryClientProvider>
);
}Ekranda:
Sharh 6, Sharh 5, Sharh 4, Sharh 4, Sharh 3, Sharh 2
Yangi sharh yozildi Yana yuklash"Sharh 4" ikki marta. Teskari holat ham bor: sharh o'chirilsa, bitta element hech qachon ko'rinmaydi. React'da bir xil key bilan ikkita element — konsolda ogohlantirish va noto'g'ri yangilanishlar (Ro'yxatlar va key). Bu misolda kalit sifatida indeks emas, matn ishlatsak — React xato beradi.
4.2 Kursor
Kursor (cursor) — "mana shu elementdan keyingilarini ber": ?keyin=sharh-4. Server oxirgi ko'rilgan elementning belgisini (id, sana) oladi va undan keyingilarni qaytaradi. Boshiga qo'shilgan element kursorga ta'sir qilmaydi.
import {
QueryClient,
QueryClientProvider,
useInfiniteQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
const reviews = [
{ id: 6, text: "Sharh 6" },
{ id: 5, text: "Sharh 5" },
{ id: 4, text: "Sharh 4" },
{ id: 3, text: "Sharh 3" },
{ id: 2, text: "Sharh 2" },
];
// Server: kursor — oxirgi ko'rilgan sharhning id'si
function fetchAfter(cursor) {
const start =
cursor === null
? 0
: reviews.findIndex((r) => r.id === cursor) + 1;
const items = reviews.slice(start, start + 3);
const next = start + 3 < reviews.length ? items.at(-1).id : null;
return new Promise((resolve) =>
setTimeout(() => resolve({ items, next }), 200),
);
}
function Reviews() {
const query = useInfiniteQuery({
queryKey: ["reviews"],
queryFn: ({ pageParam }) => fetchAfter(pageParam),
initialPageParam: null,
getNextPageParam: (last) => last.next,
});
if (query.isPending) return <p>Yuklanmoqda…</p>;
const items = query.data.pages.flatMap((p) => p.items);
return (
<>
<p>{items.map((r) => r.text).join(", ")}</p>
<button
onClick={() => reviews.unshift({ id: 7, text: "Sharh 7" })}
>
Yangi sharh yozildi
</button>{" "}
<button
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}
>
Yana yuklash
</button>
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Reviews />
</QueryClientProvider>
);
}Ekranda:
Sharh 6, Sharh 5, Sharh 4, Sharh 3, Sharh 2
Yangi sharh yozildi Yana yuklashTakror yo'q. Server "Sharh 4 dan keyingilar" ni berdi — yangi "Sharh 7" boshida, u kursorga xalaqit bermaydi. getNextPageParam endi hisoblamaydi, serverning next maydonini qaytaradi, xolos. null — "boshqa yo'q", hasNextPage yolg'on.
| Offset | Kursor | |
|---|---|---|
| so'rov | ?offset=20&limit=10 |
?keyin=a8f3&limit=10 |
| ixtiyoriy sahifaga sakrash | bor ("5-sahifa") | yo'q |
| ro'yxat o'zgarsa | takror yoki tushib qolish | barqaror |
| katta jadvalda tezlik | sekinlashadi (OFFSET 100000) |
tez (indeks bo'yicha) |
| qayerda | admin jadvali, qidiruv natijalari | lenta, chat, bildirishnomalar |
Oxirgi qatordagi "sekinlashadi" — server tomoni: ma'lumotlar bazasi 100 000 ta qatorni o'qib, tashlab yuborishi kerak. Buni Keyset pagination va katta OFFSET darsida o'lchaymiz. Frontendchi sifatida bilishingiz kerak bo'lgan qism: API kursor bersa — getNextPageParam: (last) => last.nextCursor ?? undefined.
Tekshirib ko'ring: Telegram kanali uchun qaysi sahifalash to'g'ri va nega?
Javob
Kursor. Kanalga doim yangi postlar qo'shiladi va eskilari ba'zan o'chiriladi. Offset bilan aylantirib o'tirgan foydalanuvchi takror postlarni yoki tushib qolganlarni ko'rardi. "5-sahifaga sakrash" esa lentada kerak emas.
5. Cheksiz aylantirish: IntersectionObserver
«Yana yuklash» tugmasi o'rniga — ro'yxat oxiriga yetganda o'zi yuklasin. IntersectionObserver darsidan bilasiz: u element ekranga kirganini xabar qiladi. Ro'yxat oxiriga ko'rinmas "qorovul" element qo'yamiz. U ko'rindi — fetchNextPage().
Natija oynasi brauzerda ishlaydi — IntersectionObserver bor. Lekin kursning tekshiruv muhitida (jsdom) u yo'q, shuning uchun bu blok tekshirilmagan: «Ishga tushir» ni bosing va natija oynasini pastga aylantiring.
import { useEffect, useRef } from "react";
import {
QueryClient,
QueryClientProvider,
useInfiniteQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
function fetchChunk(offset) {
return new Promise((resolve) =>
setTimeout(() => {
const items = Array.from(
{ length: 10 },
(_, i) => `Buyurtma №${offset + i + 1}`,
);
resolve({ items, next: offset < 40 ? offset + 10 : undefined });
}, 300),
);
}
function Orders() {
const query = useInfiniteQuery({
queryKey: ["orders"],
queryFn: ({ pageParam }) => fetchChunk(pageParam),
initialPageParam: 0,
getNextPageParam: (last) => last.next,
});
const sentinel = useRef(null);
const { hasNextPage, isFetchingNextPage, fetchNextPage } = query;
useEffect(() => {
const el = sentinel.current;
if (el === null || !hasNextPage) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !isFetchingNextPage) {
fetchNextPage();
}
},
{ rootMargin: "200px" },
);
observer.observe(el);
return () => observer.disconnect();
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
if (query.isPending) return <p>Yuklanmoqda…</p>;
return (
<>
<ul>
{query.data.pages
.flatMap((p) => p.items)
.map((name) => (
<li key={name} style={{ padding: 8 }}>
{name}
</li>
))}
</ul>
<p ref={sentinel}>
{isFetchingNextPage
? "Yuklanmoqda…"
: hasNextPage
? ""
: "Hammasi shu"}
</p>
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Orders />
</QueryClientProvider>
);
}Kodda uch muhim joy:
rootMargin: "200px"— qorovul ekranga kirishidan 200 px oldin yuklash boshlanadi. Foydalanuvchi oxiriga yetganda yangi qatorlar allaqachon bor.!isFetchingNextPage— bo'lak yo'lda bo'lsa, yana so'ramaymiz. Aks holda tez aylantirishda bitta bo'lak ikki marta so'raladi.- Cleanup —
observer.disconnect()(useEffect III: cleanup). Bog'liqliklar o'zgarganda eski kuzatuvchi o'chadi, yangisi ulanadi.
Diqqat: Cheksiz aylantirishning qulaylik narxi bor. Sahifa pastida turgan narsalar (footer, aloqa havolalari) hech qachon ko'rinmaydi. Klaviatura bilan yuradigan Bobur Tab bosib ro'yxatdan chiqa olmaydi — har safar yangi qatorlar qo'shiladi. Ekran o'quvchi (Malika) qancha element borligini bilmaydi. Ko'p jamoalar shuning uchun «Yana yuklash» tugmasini afzal ko'radi yoki bir necha avtomatik yuklashdan keyin tugmaga o'tadi.
Juda uzun ro'yxatda (minglab qator) DOM'dagi elementlar soni ham muammo. Buning davosi — virtualizatsiya: ekranda faqat ko'rinadigan qatorlar chiziladi. Cheksiz so'rov va virtualizatsiya ko'pincha birga ishlatiladi.
6. Tajriba: vazifalar da sahifalash bormi?
Bu darsda vazifalar kodi o'zgarmaydi. Sababi o'lchov va kanon qarorida.
Nega kanonda yo'q. vazifalar API'sida sahifalash yo'q: server butun ro'yxatni bitta javobda beradi. Undan muhimrog'i — ilovaning o'zi butun ro'yxatni talab qiladi. Qidiruv hamma vazifa bo'yicha ishlaydi, statistika hammasini sanaydi, teg daraxti hamma teglardan quriladi. Bo'lak-bo'lak yuklansa, qidiruv faqat yuklangan qismda topardi — bu xato. Uzun ro'yxat muammosini esa 18-qismdagi virtual ro'yxat allaqachon hal qilgan.
Biz sinab ko'rdik. useInfiniteQuery ni «Bahor» mashq serverining menyusida tekshirdik — u yerda sahifalash bor: ?sahifa=N&hajm=2, javobda keyingi maydoni (kursorga o'xshash: server keyingi sahifa raqamini o'zi aytadi). Tajriba kodi (tajriba/variantlar/server-cheksiz.tsx, kanondan aynan, qatorlar o'qish uchun bo'lingan):
type Sahifa = {
elementlar: { id: number; nom: string; narx: number }[];
sahifa: number;
keyingi: number | null;
jami: number;
};
function Ilova() {
const s = useInfiniteQuery({
queryKey: ["menyu"],
initialPageParam: 1,
queryFn: async ({ pageParam, signal }): Promise<Sahifa> =>
(await fetch(
`https://ilmhamroh.uz/api/mashq/menyu?sahifa=${pageParam}&hajm=2`,
{ signal },
)).json() as Promise<Sahifa>,
getNextPageParam: (oxirgi) => oxirgi.keyingi ?? undefined,
});
// ... ro'yxat va «Yana» tugmasi
}keyingi ?? undefined ga qarang: server oxirgi sahifada keyingi: null beradi. Query null ni ham "boshqa yo'q" deb tushunadi, lekin TypeScript turi uchun undefined aniqroq.
Chrome 155 da haqiqiy server bilan natija (natijalar/tajriba/16-17-server-ishlaydi.txt, aynan):
Chrome: Chrome/155.0.8059.40 | variant: server-cheksiz
menyu: ["Osh — 35000","Lag'mon — 28000","Manti — 30000","Ko'k choy — 5000"] | tugma: Hammasi
so'rovlar: GET ?sahifa=1&hajm=2, GET ?sahifa=2&hajm=2Ikki sahifa, to'rtta taom, ikkinchi GET'dan keyin tugma "Hammasi" — hasNextPage yolg'on. To'plam hajmi (gzip, React bilan birga): 77 921 B. Oddiy useQuery varianti — 78 333 B. Ya'ni useInfiniteQuery Query ichida, alohida narx yo'q.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| har sahifada "Yuklanmoqda…" va sakrash | eski sahifa saqlanmaydi | placeholderData: keepPreviousData |
| oxirgi sahifadan keyin bo'sh sahifa | hasMore tekshirilmagan |
serverdan hasMore / next |
| «Yana» hech qachon o'chmaydi | getNextPageParam undefined qaytarmaydi |
oxirgi bo'lakda undefined |
| bir xil element ikki marta | offset + o'zgaruvchan ro'yxat | kursor |
Encountered two children with the same key |
takror element, key — matn yoki id |
kursor; vaqtincha — takrorni olib tashlash |
| bitta bo'lak ikki marta so'raldi | qorovul isFetchingNextPage ni tekshirmaydi |
if (… && !isFetchingNextPage) |
| oynaga qaytganda 20 ta so'rov | cheksiz ro'yxat boshidan qayta yuklanadi | staleTime, maxPages |
data.map is not a function |
useInfiniteQuery da data — { pages, pageParams } |
data.pages.flatMap(…) |
Oxirgisi — useQuery dan useInfiniteQuery ga o'tganda eng birinchi uchraydigan xato. Ma'lumot shakli o'zgaradi: endi u bo'laklar massivi ichida.
8. Mashqlar
Mashqlar kurs/mashqlar/19/16-sahifalash/ da.
1-mashq (oson): offset yoki kursor?
Har holat uchun yozing: "offset" yoki "kursor".
- Admin panel: 50 sahifali buyurtmalar jadvali, "17-sahifaga o'tish" maydoni bilan →
- Chat: yuqoriga aylantirganda eski xabarlar →
- Instagram lentasi →
- Qidiruv natijalari, "1 2 3 … 10" →
Yechim
Ixtiyoriy sahifaga sakrash kerak bo'lsa — offset (admin jadvali, qidiruv natijalari). Ro'yxat doim o'zgaradigan va ketma-ket o'qiladigan joyda — kursor (chat, lenta): yangi element qo'shilganda takror va tushib qolish bo'lmaydi.
2-mashq (o'rta): sakramaydigan sahifalar
Kodda «Keyingi» bosilganda ekran "Yuklanmoqda…" ga sakraydi va oxirgi sahifada ham «Keyingi» ishlayveradi. Tuzating:
placeholderData: keepPreviousData.- Eski sahifa ko'rinayotganda — matn oxirida " (yuklanmoqda…)".
- «Keyingi» —
isPlaceholderDatayoki!data.hasMoreda o'chiq.
import { useState } from "react";
import {
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
const DRINKS = ["Ko'k choy", "Qora choy", "Kompot", "Ayron"];
function fetchPage(page) {
return new Promise((resolve) =>
setTimeout(
() =>
resolve({
items: DRINKS.slice((page - 1) * 2, page * 2),
hasMore: page * 2 < DRINKS.length,
}),
200,
),
);
}
function Drinks() {
const [page, setPage] = useState(1);
const { isPending, data } = useQuery({
queryKey: ["drinks", page],
queryFn: () => fetchPage(page),
});
if (isPending) return <p>Yuklanmoqda…</p>;
return (
<>
<p>{data.items.join(", ")}</p>
<button onClick={() => setPage(page + 1)}>Keyingi</button>
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Drinks />
</QueryClientProvider>
);
}Ekranda:
Kompot, Ayron
KeyingiYechim
import { useState } from "react";
import {
keepPreviousData,
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
const DRINKS = ["Ko'k choy", "Qora choy", "Kompot", "Ayron"];
function fetchPage(page) {
return new Promise((resolve) =>
setTimeout(
() =>
resolve({
items: DRINKS.slice((page - 1) * 2, page * 2),
hasMore: page * 2 < DRINKS.length,
}),
200,
),
);
}
function Drinks() {
const [page, setPage] = useState(1);
const { isPending, data, isPlaceholderData } = useQuery({
queryKey: ["drinks", page],
queryFn: () => fetchPage(page),
placeholderData: keepPreviousData,
});
if (isPending) return <p>Yuklanmoqda…</p>;
return (
<>
<p>
{data.items.join(", ")}
{isPlaceholderData && " (yuklanmoqda…)"}
</p>
<button
disabled={isPlaceholderData || !data.hasMore}
onClick={() => setPage(page + 1)}
>
Keyingi
</button>
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Drinks />
</QueryClientProvider>
);
}Ekranda:
Kompot, Ayron
KeyingiOraliq nuqta "Ko'k choy, Qora choy (yuklanmoqda…)" ni tekshirdi — 2-sahifa kelguncha 1-sahifa turdi. Yakunda «Keyingi» o'chiq: hasMore yolg'on (4 ta ichimlikdan 4 tasi ko'rsatildi).
3-mashq (qiyin): kursorli «Yana yuklash»
«Bahor» buyurtmalar tarixi. Server kursor bilan ishlaydi: fetchOrders(cursor) → { items, nextCursor }, oxirgisida nextCursor: null. Yozing:
useInfiniteQuery:initialPageParam: null,getNextPageParam— serverningnextCursori.- Ekranda: hamma buyurtmalar vergul bilan, ostida «Yana» (yo'lda — "Yuklanmoqda…", tugasa — "Tamom", o'chiq).
Server (boshlang'ich kod):
const ORDERS = ["#101", "#102", "#103", "#104", "#105"];
function fetchOrders(cursor) {
console.log("kursor:", cursor);
const start = cursor === null ? 0 : ORDERS.indexOf(cursor) + 1;
const items = ORDERS.slice(start, start + 2);
const nextCursor =
start + 2 < ORDERS.length ? items.at(-1) : null;
return new Promise((resolve) =>
setTimeout(() => resolve({ items, nextCursor }), 200),
);
}Ishora: «Kursor» bo'limidagi misol. null ni undefined ga aylantirish shart emas — Query ikkalasini ham "boshqa yo'q" deb tushunadi.
Yechim
import {
QueryClient,
QueryClientProvider,
useInfiniteQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
const ORDERS = ["#101", "#102", "#103", "#104", "#105"];
function fetchOrders(cursor) {
console.log("kursor:", cursor);
const start = cursor === null ? 0 : ORDERS.indexOf(cursor) + 1;
const items = ORDERS.slice(start, start + 2);
const nextCursor =
start + 2 < ORDERS.length ? items.at(-1) : null;
return new Promise((resolve) =>
setTimeout(() => resolve({ items, nextCursor }), 200),
);
}
function History() {
const query = useInfiniteQuery({
queryKey: ["history"],
queryFn: ({ pageParam }) => fetchOrders(pageParam),
initialPageParam: null,
getNextPageParam: (last) => last.nextCursor,
});
if (query.isPending) return <p>Yuklanmoqda…</p>;
let label = "Yana";
if (query.isFetchingNextPage) label = "Yuklanmoqda…";
else if (!query.hasNextPage) label = "Tamom";
return (
<>
<p>{query.data.pages.flatMap((p) => p.items).join(", ")}</p>
<button
disabled={!query.hasNextPage || query.isFetchingNextPage}
onClick={() => query.fetchNextPage()}
>
{label}
</button>
</>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<History />
</QueryClientProvider>
);
}Konsolda:
kursor: null
kursor: #102
kursor: #104Ekranda:
#101, #102, #103, #104, #105
TamomgetNextPageParam faqat serverning javobini uzatadi — hisob yo'q. Kursor serverning ishi: u qayerdan davom etishni biladi. Uchinchi bo'lakda bitta buyurtma (#105) va nextCursor: null — hasNextPage yolg'on, tugma "Tamom".
9. Real ishda
- Admin panellar — raqamli sahifalar +
keepPreviousData+ sahifa raqami URL'da. Ko'pincha jadval kutubxonasi bilan (TanStack Table kabi). - Lentalar va chat —
useInfiniteQuery+ kursor +IntersectionObserver+ virtualizatsiya. Telegram, Instagram, X shunday ishlaydi. - API dizayni — backend dasturchi bilan kelishuv: javobda
nextCursor(yokihasMore) bo'lsin. "Jami nechta" (total) ni hisoblash katta jadvalda qimmat — ko'p API uni bermaydi. - Intervyu savollari: "offset va kursor farqi, qachon qaysi?", "
keepPreviousDatanima qiladi?", "useInfiniteQueryma'lumotni qanday saqlaydi?", "cheksiz aylantirishni qanday qilasiz va a11y muammolari?".
Xulosa
- Raqamli sahifalar: sahifa raqami — kalitda (va URL'da).
placeholderData: keepPreviousData— yangi sahifa kelguncha eskisi ekranda,isPlaceholderDatabilan. useInfiniteQuery— hamma bo'laklar bitta kalitda:data.pages,pageParams.initialPageParam,getNextPageParam(undefined— tamom),fetchNextPage,hasNextPage,isFetchingNextPage.- Offset — sakrash mumkin, lekin o'zgaruvchan ro'yxatda takror va tushib qolish. Kursor — barqaror, lentalar uchun.
IntersectionObserverqorovuli — cheksiz aylantirish;rootMargin,isFetchingNextPagetekshiruvi, cleanup. A11y narxini unutmang.vazifalarda sahifalash yo'q: API bermaydi, qidiruv va statistika butun ro'yxatni talab qiladi.
Keyingi dars: TanStack Query V: prefetch, Suspense, bekor qilish va SWR muqobili — oldindan yuklash, router loader bilan ensureQueryData, useSuspenseQuery, signal, DevTools va SWR bilan taqqos.
Manbalar
- TanStack Query v5: Paginated Queries — tanstack.com/query/v5/docs/framework/react/guides/paginated-queries (2026-10, v5.104)
- TanStack Query v5: Infinite Queries,
useInfiniteQuery— …/guides/infinite-queries, …/reference/useInfiniteQuery - TanStack Query v5: Placeholder Query Data — …/guides/placeholder-query-data
- MDN: Intersection Observer API — developer.mozilla.org/docs/Web/API/Intersection_Observer_API
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!