Suspense — data fetching, skeleton UI

~15 daq · React · Ilg'or mavzular

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: Postlar va Dostlar faqat 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 + useState bilan qilingan oddiy fetch Suspense 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 Suspense da; 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 fallback ko'rsatiladi
  • Komponent ichida isPending yo'q — data doim mavjud
  • Qo'llab-quvvatlovchi vositalar: useSuspenseQuery, React 19 use(promise), lazy, framework lar
  • Oddiy useEffect + fetch Suspense ni ishga tushirmaydi
  • Chegaralar — dizayn bo'yicha: birga ko'rinadiganlar bitta Suspense da, mustaqillari alohida
  • Fallback sifatida skeleton; prefers-reduced-motion ni hurmat qiling
  • Waterfall dan saqlaning: parallel so'rovlar, prefetch, loader lar
  • Xatolar — ErrorBoundary + Suspense juftligi

Keyingi dars

82-dars: Concurrent Features — useTransition, useDeferredValue