İçeriğe geç
nextjs

Next.js Loading UI ve Suspense Boundaries

Tarık Tunç
Next.js Loading UI ve Suspense Boundaries

Next.js Loading UI ve Suspense Boundaries

Kullanıcı deneyiminde yükleme durumlarının yönetimi kritik önem taşır. Next.js App Router, loading.tsx dosyası ve React Suspense boundary'leri ile yükleme durumlarını zarif şekilde yönetmenizi sağlar. Bu rehberde instant loading state'ler, streaming ve skeleton UI pattern'lerini detaylı inceleyeceğiz.

loading.tsx ile Otomatik Loading State

loading.tsx dosyası, ilgili route segmenti yüklenirken otomatik olarak gösterilir:

// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="animate-pulse space-y-4 p-6">
      <div className="h-8 bg-gray-200 rounded w-1/3"></div>
      <div className="grid grid-cols-4 gap-4">
        {[1, 2, 3, 4].map((i) => (
          <div key={i} className="h-32 bg-gray-200 rounded"></div>
        ))}
      </div>
      <div className="h-64 bg-gray-200 rounded"></div>
    </div>
  );
}

Next.js, loading.tsx'i otomatik olarak bir Suspense boundary'sine sarar:

// Next.js'in arka planda yaptığı:
<Suspense fallback={<Loading />}>
  <Page />
</Suspense>

Skeleton UI Bileşenleri

// components/skeletons.tsx
export function CardSkeleton() {
  return (
    <div className="animate-pulse border rounded-lg p-4">
      <div className="h-48 bg-gray-200 rounded mb-4"></div>
      <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
      <div className="h-4 bg-gray-200 rounded w-1/2"></div>
    </div>
  );
}

export function TableSkeleton({ rows = 5 }: { rows?: number }) {
  return (
    <div className="animate-pulse">
      <div className="h-10 bg-gray-200 rounded mb-2"></div>
      {Array.from({ length: rows }).map((_, i) => (
        <div key={i} className="h-12 bg-gray-100 rounded mb-1"></div>
      ))}
    </div>
  );
}

export function ProfileSkeleton() {
  return (
    <div className="animate-pulse flex items-center gap-4">
      <div className="w-16 h-16 bg-gray-200 rounded-full"></div>
      <div className="space-y-2">
        <div className="h-4 bg-gray-200 rounded w-32"></div>
        <div className="h-3 bg-gray-200 rounded w-48"></div>
      </div>
    </div>
  );
}

Suspense ile Granüler Loading

Sayfa içindeki bağımsız bölümleri ayrı Suspense boundary'leriyle sararak kısmi yükleme sağlayabilirsiniz:

import { Suspense } from 'react';
import { CardSkeleton, TableSkeleton } from '@/components/skeletons';

// Yavaş veri çeken bileşenler
async function RevenueChart() {
  const data = await getRevenue(); // 3 saniye sürer
  return <Chart data={data} />;
}

async function RecentOrders() {
  const orders = await getRecentOrders(); // 2 saniye sürer
  return <OrderTable orders={orders} />;
}

async function TopProducts() {
  const products = await getTopProducts(); // 1 saniye sürer
  return <ProductList products={products} />;
}

// Sayfa — her bölüm bağımsız yüklenir
export default function DashboardPage() {
  return (
    <div className="grid grid-cols-2 gap-6">
      <Suspense fallback={<CardSkeleton />}>
        <TopProducts /> {/* İlk yüklenen */}
      </Suspense>

      <Suspense fallback={<CardSkeleton />}>
        <RevenueChart /> {/* En son yüklenen */}
      </Suspense>

      <div className="col-span-2">
        <Suspense fallback={<TableSkeleton />}>
          <RecentOrders />
        </Suspense>
      </div>
    </div>
  );
}
İpucu: Her async bileşeni ayrı Suspense ile sarmak, streaming SSR'dan maksimum fayda sağlar. Hazır olan bölümler anında gösterilir, yavaş olanlar skeleton ile beklenir.

Nested Loading States

// Dosya yapısı
app/dashboard/
├── loading.tsx          # Dashboard genel loading
├── page.tsx
├── analytics/
│   ├── loading.tsx      # Analytics özel loading
│   └── page.tsx
└── settings/
    └── page.tsx         # Dashboard loading'i kullanır

// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return <div>Dashboard yükleniyor...</div>;
}

// app/dashboard/analytics/loading.tsx
export default function AnalyticsLoading() {
  return (
    <div className="space-y-4">
      <div className="h-8 bg-gray-200 rounded w-48 animate-pulse"></div>
      <div className="grid grid-cols-3 gap-4">
        {[1, 2, 3].map((i) => (
          <div key={i} className="h-40 bg-gray-200 rounded animate-pulse"></div>
        ))}
      </div>
    </div>
  );
}

Streaming SSR ile Progressive Rendering

// Streaming otomatik olarak çalışır
// loading.tsx veya Suspense kullandığınızda Next.js streaming'i aktifleştirir

export default async function Page() {
  return (
    <div>
      {/* Bu anında render edilir */}
      <h1>Dashboard</h1>
      <nav>Navigasyon</nav>

      {/* Bu stream edilir — hazır olunca gönderilir */}
      <Suspense fallback={<p>Yükleniyor...</p>}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}
Uyarı: Streaming, headers() veya cookies() gibi dinamik fonksiyonlar kullanıldığında otomatik olarak devreye girer. Ancak tüm hosting ortamları streaming'i desteklemez; Vercel ve Node.js sunucuları destekler, bazı static hosting'ler desteklemez.

useTransition ile Client-Side Loading

'use client';

import { useTransition } from 'react';
import { useRouter } from 'next/navigation';

export function NavigationButton({ href, children }: {
  href: string;
  children: React.ReactNode;
}) {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();

  return (
    <button
      onClick={() => {
        startTransition(() => {
          router.push(href);
        });
      }}
      disabled={isPending}
      className={isPending ? 'opacity-50' : ''}
    >
      {isPending ? 'Yükleniyor...' : children}
    </button>
  );
}

Loading Spinner Bileşeni

// components/Spinner.tsx
export function Spinner({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) {
  const sizeClasses = {
    sm: 'w-4 h-4',
    md: 'w-8 h-8',
    lg: 'w-12 h-12',
  };

  return (
    <div className="flex items-center justify-center p-4">
      <div
        className={`${sizeClasses[size]} border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin`}
      />
    </div>
  );
}
Bilgi: Skeleton UI, spinner'lara göre daha iyi bir kullanıcı deneyimi sunar çünkü sayfa yapısını önceden gösterir. Bu, algılanan yükleme süresini azaltır.

Sonuç

Next.js'in loading UI sistemi, kullanıcıya her zaman anlamlı bir geri bildirim sunmanızı sağlar. loading.tsx ile route bazlı loading, Suspense ile granüler loading ve streaming SSR ile progressive rendering kombinasyonu, mükemmel bir kullanıcı deneyimi oluşturur. Skeleton UI kullanarak algılanan performansı iyileştirin ve her async işlem için uygun bir loading state tanımlayın.

Kaynaklar

İlgili Yazılar