Loading UI va Suspense — skeleton, fallback
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.