Suspense — data fetching, skeleton UI
Muammodan boshlaylik
Foydalanuvchi profili sahifasi uch qismdan iborat: profil, postlar, do'stlar. Har biri o'z ma'lumotini yuklaydi:
function Profil({ id }) {
const { data: user, isPending } = useQuery({ queryKey: ['user', id], queryFn: ... });
if (isPending) return <ProfilSkeleton />;
return (
<>
<ProfilKartochka user={user} />
<Postlar userId={id} /> {/* ichida: if (isPending) return <Spinner /> */}
<Dostlar userId={id} /> {/* ichida: if (isPending) return <Spinner /> */}
</>
);
}
Natija:
- Har bir komponent ichida bir xil
if (isPending) return ...takrorlanadi - Ekranda uchta spinner turli vaqtda paydo bo'lib, yo'qolib — sahifa "sakraydi"
- Waterfall:
PostlarvaDostlarfaqat profil yuklangandan keyin render bo'ladi va shundan keyingina o'z so'rovlarini yuboradi — so'rovlar parallel emas, ketma-ket
69-darsda Suspense ni lazy bilan ishlatgan edik — kod yuklanguncha fallback ko'rsatiladi. Suspense ma'lumot uchun ham ishlaydi.
Suspense g'oyasi
Odatiy yondashuv: har bir komponent o'zi "men yuklanyapman" deydi va o'zi spinner ko'rsatadi.
Suspense yondashuvi: komponent shunchaki ma'lumotni so'raydi. Agar ma'lumot tayyor bo'lmasa, React render ni "to'xtatadi" (suspend) va eng yaqin <Suspense> ning fallback ini ko'rsatadi. Ma'lumot kelganda — qaytadan render qiladi.
function Postlar({ userId }) {
const { data: postlar } = useSuspenseQuery({ // ma'lumot har doim bor!
queryKey: ['postlar', userId],
queryFn: () => postlarniOl(userId),
});
return <ul>{postlar.map((p) => <li key={p.id}>{p.sarlavha}</li>)}</ul>;
}
isPending tekshiruvi yo'q. Komponent ichida data — har doim mavjud. Yuklanish holatini ota-onadagi Suspense boshqaradi:
<Suspense fallback={<PostlarSkeleton />}>
<Postlar userId={id} />
</Suspense>
Suspense ni qo'llab-quvvatlovchi vositalar
Suspense hamma narsa bilan avtomatik ishlamaydi — ma'lumot manbai uni qo'llab-quvvatlashi kerak:
| Vosita | Qanday |
|---|---|
| TanStack Query | useSuspenseQuery, useSuspenseInfiniteQuery |
| React 19 | use(promise) hook i |
React.lazy |
Kod yuklash (69-dars) |
| Next.js, React Router | Server Components (83-dars), route loader lari |
[!WARNING]
useEffect+useStatebilan qilingan oddiyfetchSuspense ni ishga tushirmaydi — komponent baribir avval bo'sh holat bilan render bo'ladi. Suspense uchun ma'lumot manbai render paytida "hali tayyor emasman" signalini berishi kerak — buni qo'lda to'g'ri qilish murakkab, shuning uchun tayyor vositalardan foydalaning.
use() — React 19
import { use, Suspense } from 'react';
function Izohlar({ izohlarPromise }) {
const izohlar = use(izohlarPromise); // Promise tugaguncha — suspend
return izohlar.map((i) => <p key={i.id}>{i.matn}</p>);
}
function Maqola({ id }) {
const [izohlarPromise] = useState(() => izohlarniOl(id)); // promise bir marta yaratiladi
return (
<Suspense fallback={<p>Izohlar yuklanmoqda...</p>}>
<Izohlar izohlarPromise={izohlarPromise} />
</Suspense>
);
}
use ga beriladigan Promise barqaror bo'lishi kerak — har renderda yangi fetch() chaqirilsa, cheksiz yuklash sikli bo'ladi. Odatda promise ota-onada, server komponentda yoki kesh kutubxonasida yaratiladi.
Suspense chegaralarini joylashtirish
Fallback qayerda ko'rinishini siz hal qilasiz — Suspense ni qayerga qo'yishingizga qarab:
Hammasi birga
<Suspense fallback={<ProfilSahifaSkeleton />}>
<ProfilKartochka id={id} />
<Postlar userId={id} />
<Dostlar userId={id} />
</Suspense>
Uchalasi ham tayyor bo'lguncha — bitta skeleton. Keyin hammasi birdaniga paydo bo'ladi. Sahifa sakramaydi.
Bosqichma-bosqich
<Suspense fallback={<ProfilSkeleton />}>
<ProfilKartochka id={id} />
<div className="ikki-ustun">
<Suspense fallback={<PostlarSkeleton />}>
<Postlar userId={id} />
</Suspense>
<Suspense fallback={<DostlarSkeleton />}>
<Dostlar userId={id} />
</Suspense>
</div>
</Suspense>
Avval profil (eng muhimi), keyin postlar va do'stlar — har biri tayyor bo'lishi bilan, bir-birini kutmasdan.
[!TIP] Chegaralarni dizayn asosida joylashtiring, kod tuzilmasi asosida emas. Savol: "Foydalanuvchi qaysi qismlarni birga ko'rishi kerak?" Bir-biriga bog'liq qismlar — bitta
Suspenseda; mustaqil va sekin qismlar (tavsiyalar, izohlar) — alohida.
Skeleton UI
Fallback sifatida spinner emas, skeleton — kontent shaklidagi kulrang bloklar:
function PostlarSkeleton() {
return (
<ul aria-busy="true" aria-label="Postlar yuklanmoqda">
{Array.from({ length: 3 }, (_, i) => (
<li key={i} className="skeleton-qator">
<div className="skeleton" style={{ width: '60%', height: '1.25rem' }} />
<div className="skeleton" style={{ width: '90%', height: '1rem' }} />
</li>
))}
</ul>
);
}
.skeleton {
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
background-size: 200% 100%;
animation: skeleton 1.4s ease-in-out infinite;
border-radius: 6px;
}
@keyframes skeleton {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; }
}
Skeleton:
- Foydalanuvchiga nima kelayotganini ko'rsatadi
- Kontent kelganda sahifa "sakramaydi" (layout shift kamayadi) — o'lchamlar mos bo'lsa
- Kutish vaqtini psixologik jihatdan qisqaroq his qildiradi
Waterfall muammosi
function Profil({ id }) {
const { data: user } = useSuspenseQuery({ queryKey: ['user', id], ... });
return (
<>
<ProfilKartochka user={user} />
<Postlar userId={id} /> {/* faqat user kelgandan keyin render → keyin so'rov */}
</>
);
}
Profil suspend bo'lganda uning bolalari render bo'lmaydi — demak Postlar ning so'rovi ham ketmaydi. Natija: ketma-ket so'rovlar.
Yechimlar:
// 1. So'rovlarni bir komponentda parallel boshlash
const [{ data: user }, { data: postlar }] = useSuspenseQueries({
queries: [
{ queryKey: ['user', id], queryFn: () => userOl(id) },
{ queryKey: ['postlar', id], queryFn: () => postlarOl(id) },
],
});
// 2. Oldindan yuklash — sahifa ochilishidan oldin (54-dars)
queryClient.prefetchQuery({ queryKey: ['postlar', id], queryFn: ... });
// 3. Router loader lari yoki Server Components — ma'lumot render dan oldin
Xatolar — Error Boundary bilan
useSuspenseQuery xatoda throw qiladi — uni eng yaqin Error Boundary (79-dars) ushlaydi:
<ErrorBoundary FallbackComponent={XatoBloki}>
<Suspense fallback={<PostlarSkeleton />}>
<Postlar userId={id} />
</Suspense>
</ErrorBoundary>
- Kutish —
Suspense - Xato —
ErrorBoundary - Muvaffaqiyat — komponentning o'zi
Komponent faqat "baxtli yo'l" ni yozadi; qolgan holatlar — deklarativ ravishda tashqarida.
Nima uchun shunday?
Nima uchun yuklanish holatini komponentdan tashqariga — Suspense ga chiqarish yaxshi g'oya?
Chunki yuklanish holati — bu koordinatsiya muammosi, va uni alohida komponentlar hal qila olmaydi. Har bir komponent o'z spinnerini ko'rsatsa, natija tasodifiy: qaysi biri oldin yuklansa, o'sha oldin paydo bo'ladi va sahifa sakraydi. "Profil va postlar birga paydo bo'lsin" yoki "avval sarlavha, keyin qolgani" degan qaror — sahifa darajasidagi dizayn qarori. Suspense bu qarorni komponentlar kodidan ajratib, daraxt tuzilmasiga chiqaradi: chegarani ko'chirish — tajribani o'zgartirish, va buning uchun birorta komponentning ichini o'zgartirish shart emas. Bu try/catch ga o'xshaydi: xato qayerda tashlanishi — bir narsa, qayerda ushlanishi — boshqa.
Xulosa
Eslab qoling:
- Suspense — komponent ma'lumotni so'raydi; tayyor bo'lmasa, eng yaqin
fallbackko'rsatiladi - Komponent ichida
isPendingyo'q —datadoim mavjud - Qo'llab-quvvatlovchi vositalar:
useSuspenseQuery, React 19use(promise),lazy, framework lar - Oddiy
useEffect+fetchSuspense ni ishga tushirmaydi - Chegaralar — dizayn bo'yicha: birga ko'rinadiganlar bitta
Suspenseda, mustaqillari alohida - Fallback sifatida skeleton;
prefers-reduced-motionni hurmat qiling - Waterfall dan saqlaning: parallel so'rovlar, prefetch, loader lar
- Xatolar —
ErrorBoundary+Suspensejuftligi
Keyingi dars
82-dars: Concurrent Features — useTransition, useDeferredValue