Loading UI va Suspense — skeleton, fallback

~15 daq · Next.js · Ma'lumot Olish

Muammodan boshlaylik

Ma'lumot yuklanayotganda foydalanuvchi bo'sh sahifa ko'radi. 1-2 soniya o'tadi — hech narsa yo'q. Sayt ishlamayaptimi? Internet uzildimi? Ko'pchilik shu paytda sahifani yopib ketadi.

Yaxshi loading UI — foydalanuvchiga "nimadir yuklanmoqda, kuting" degan signal. Bu kichik narsa ko'rinadi, lekin foydalanuvchi qolish ehtimolini sezilarli oshiradi.


Uch xil yondashuv

1. loading.tsx — butun sahifa uchun

Oldingi darslarda ko'rgan edik. page.tsx yuklanayotganda butun sahifa o'rniga loading.tsx ko'rsatiladi:

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div style={{ padding: '24px' }}>
      <p>Yuklanmoqda...</p>
    </div>
  )
}

Oddiy, lekin yaxshi emas — foydalanuvchi bo'sh sahifa ko'radi.

2. Skeleton UI — kontent shakliga o'xshash

Skeleton — kontent joylashadigan joyda kulrang bloklar. Foydalanuvchi sahifa tuzilmasini ko'radi, kontent kelguncha kutadi.

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div style={{ padding: '24px' }}>
      {/* Sarlavha skeleton */}
      <div style={{
        height: '32px',
        width: '200px',
        background: '#e5e7eb',
        borderRadius: '6px',
        marginBottom: '24px',
      }} />

      {/* Karta skeleton — 3 ta */}
      {[1, 2, 3].map(i => (
        <div key={i} style={{
          height: '80px',
          background: '#e5e7eb',
          borderRadius: '8px',
          marginBottom: '12px',
        }} />
      ))}
    </div>
  )
}

Tailwind bilan yanada oson:

export default function Loading() {
  return (
    <div className="p-6 animate-pulse">
      <div className="h-8 w-48 bg-gray-200 rounded mb-6" />
      {[1, 2, 3].map(i => (
        <div key={i} className="h-20 bg-gray-200 rounded mb-3" />
      ))}
    </div>
  )
}

animate-pulse — Tailwind ning miltillash animatsiyasi. Skeleton "tirik" ko'rinadi.

3. Suspense — komponent darajasida

Sahifaning faqat bir qismi yuklanayotganda:

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { Stats } from './Stats'
import { RecentOrders } from './RecentOrders'

export default function DashboardPage() {
  return (
    <div className="p-6">
      <h1>Dashboard</h1>

      {/* Stats tez yuklanadi */}
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>

      {/* Orders sekin yuklanadi */}
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  )
}

Skeleton komponentlarini alohida yozish

Har bir komponent uchun alohida skeleton — kodni tartibli saqlaydi:

// components/skeletons.tsx
export function StatsSkeleton() {
  return (
    <div className="grid grid-cols-3 gap-4 animate-pulse">
      {[1, 2, 3].map(i => (
        <div key={i} className="h-24 bg-gray-200 rounded-lg" />
      ))}
    </div>
  )
}

export function OrdersSkeleton() {
  return (
    <div className="animate-pulse space-y-3">
      {[1, 2, 3, 4, 5].map(i => (
        <div key={i} className="flex items-center gap-4">
          <div className="h-10 w-10 bg-gray-200 rounded-full" />
          <div className="flex-1">
            <div className="h-4 bg-gray-200 rounded mb-2" />
            <div className="h-3 bg-gray-200 rounded w-2/3" />
          </div>
          <div className="h-4 w-16 bg-gray-200 rounded" />
        </div>
      ))}
    </div>
  )
}

loading.tsx va Suspense qachon qaysinisi

loading.tsx  → butun sahifa birinchi marta yuklanayotganda
               navigatsiya qilinganda (bir sahifadan boshqasiga)

Suspense     → sahifaning bir qismi sekin yuklanayotganda
               boshqa qismlar tez yuklanib ko'rinishi kerak bo'lganda

Amalda ikkalasini birga ishlatish:

app/dashboard/
├── loading.tsx   ← /dashboard ga birinchi kirganda
└── page.tsx      ← ichida Suspense — sekin qismlar uchun

Navigatsiya paytida loading

Link bosilganda yangi sahifa yuklanayotganda foydalanuvchiga signal berish uchun useTransition yoki NProgress kabi kutubxona ishlatiladi.

Oddiy usul — useRouter va useState bilan:

'use client'

import { useRouter } from 'next/navigation'
import { useState } from 'react'
import Link from 'next/link'

export function NavLink({ href, children }) {
  const router = useRouter()
  const [loading, setLoading] = useState(false)

  return (
    <button
      onClick={() => {
        setLoading(true)
        router.push(href)
      }}
      disabled={loading}
    >
      {loading ? 'Yuklanmoqda...' : children}
    </button>
  )
}

Tez-tez uchraydigan xatolar

1. Skeleton va haqiqiy kontent o'lchami mos emas

// ❌ — skeleton 100px, kontent 200px — layout shift bo'ladi
<div className="h-24 bg-gray-200 rounded" />  // skeleton
// ...
<div className="h-48">Kontent</div>           // haqiqiy kontent

Skeleton haqiqiy kontent o'lchamiga yaqin bo'lishi kerak — aks holda kontent kelganda sahifa "sakraydi".

2. Suspense ni async bo'lmagan komponent uchun ishlatish

// ❌ — bu komponent async emas, Suspense kutmaydi
'use client'
function Orders() {
  return <div>Buyurtmalar</div>  // darhol render bo'ladi
}

<Suspense fallback={<Skeleton />}>
  <Orders />  // fallback hech qachon ko'rinmaydi
</Suspense>

Suspense faqat async Server Component yoki React.lazy bilan ishlaydi.

3. Har joyga animate-pulse qo'yish

// ❌ — har bir element alohida pulse — chalkash ko'rinadi
<div className="animate-pulse h-4 bg-gray-200" />
<div className="animate-pulse h-4 bg-gray-200" />
<div className="animate-pulse h-4 bg-gray-200" />

// ✅ — ota elementga bir marta
<div className="animate-pulse">
  <div className="h-4 bg-gray-200" />
  <div className="h-4 bg-gray-200" />
  <div className="h-4 bg-gray-200" />
</div>

Xulosa

Loading UI — foydalanuvchiga "yuklanmoqda" signali. Kichik narsa, lekin tajribani yaxshilaydi.

loading.tsx      → butun sahifa uchun, navigatsiyada ko'rsatiladi
Suspense         → komponent darajasida, sekin qismlar uchun
Skeleton UI      → kontent shakliga o'xshash kulrang bloklar
animate-pulse    → Tailwind, ota elementga bir marta

Skeleton o'lchami haqiqiy kontent o'lchamiga yaqin bo'lsin — layout shift oldini oladi.


Keyingi dars

20-dars: Error Handling — error.tsx, global-error.tsx, try/catch

Xatolar har doim bo'ladi. Ularni to'g'ri ushlash va foydalanuvchiga tushunarli xabar ko'rsatish.