---
title: "Kurumsal Dashboard'da Skeleton Loading ve UX Optimizasyonu"
description: "React'te skeleton loading nasıl uygulanır? CSS-only skeleton, React Suspense, optimistic updates ve error boundary ile dashboard UX optimizasyonu rehberi."
date: 2026-04-13
category: ui-ux
tags: ["skeleton-loading", "react", "ux", "dashboard", "suspense", "css"]
url: https://mikroerp.dev/blog/react-skeleton-loading-ux-optimizasyonu/
---

## Neden Skeleton Loading?

Veri yüklenirken boş ekran veya dönen spinner göstermek **hızlı hissetmez**. Skeleton loading, içeriğin yerleşimini göstererek kullanıcıya "veri geliyor" mesajı verir — deneyim %40 daha hızlı algılanır.

## CSS-Only Skeleton (Framework Bağımsız)

```css
.skeleton {
  background: linear-gradient(
    90deg,
    var(--color-bg-muted) 25%,
    var(--color-border) 50%,
    var(--color-bg-muted) 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.skeleton-text {
  height: 1em;
  margin-bottom: 0.5em;
}

.skeleton-card {
  height: 120px;
  border-radius: 12px;
}

.skeleton-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
}
```

```html
<!-- Yükleniyor durumu -->
<div class="kpi-card">
  <div class="skeleton skeleton-text" style="width: 60%"></div>
  <div class="skeleton skeleton-text" style="width: 40%"></div>
</div>
```

## React Skeleton Bileşeni

```tsx
interface SkeletonProps {
  width?: string;
  height?: string;
  borderRadius?: string;
  count?: number;
}

export function Skeleton({ 
  width = '100%', 
  height = '1em', 
  borderRadius = '4px',
  count = 1 
}: SkeletonProps) {
  return (
    <>
      {Array.from({ length: count }).map((_, i) => (
        <div
          key={i}
          className="skeleton"
          style={{ width, height, borderRadius }}
        />
      ))}
    </>
  );
}

// KPI kartı skeleton'ı
export function KPICardSkeleton() {
  return (
    <div className="kpi-card">
      <Skeleton width="40%" height="0.875em" />
      <Skeleton width="60%" height="2em" />
      <Skeleton width="30%" height="0.75em" />
    </div>
  );
}

// Tablo skeleton'ı
export function TableSkeleton({ rows = 5 }: { rows?: number }) {
  return (
    <div className="table-skeleton">
      <Skeleton height="2.5em" borderRadius="8px" />
      {Array.from({ length: rows }).map((_, i) => (
        <Skeleton key={i} height="3em" />
      ))}
    </div>
  );
}
```

## Skeleton vs Spinner — Ne Zaman Hangisi?

| Durum | Skeleton | Spinner |
|-------|:---:|:---:|
| İlk sayfa yüklemesi | ✅ | ❌ |
| Tablo veri yükleme | ✅ | ❌ |
| Buton tıklama (kaydet) | ❌ | ✅ |
| Modal açılışı | ❌ | ✅ |
| Infinite scroll | ✅ | ✅ |
| Dosya yükleme | ❌ | ✅ (progress bar) |

**Kural**: İçeriğin yerleşimi biliniyorsa → Skeleton. Bilinmiyorsa → Spinner.

## React Suspense ile Entegrasyon

```tsx
import { Suspense, lazy } from 'react';

const DashboardStats = lazy(() => import('./DashboardStats'));

function Dashboard() {
  return (
    <div className="dashboard">
      <Suspense fallback={<KPICardSkeleton />}>
        <DashboardStats />
      </Suspense>
      <Suspense fallback={<TableSkeleton rows={10} />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}
```

## Optimistic Updates

Kullanıcı aksiyonunu **anında** UI'da gösterip, arka planda API'ye gönderin:

```tsx
function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);

  const addTodo = async (text: string) => {
    const optimisticTodo = { id: crypto.randomUUID(), text, done: false };
    
    // 1. Anında UI'da göster
    setTodos(prev => [...prev, optimisticTodo]);
    
    try {
      // 2. Arka planda API'ye gönder
      const saved = await api.createTodo(text);
      // 3. Gerçek ID ile güncelle
      setTodos(prev => prev.map(t => 
        t.id === optimisticTodo.id ? saved : t
      ));
    } catch {
      // 4. Hata olursa geri al
      setTodos(prev => prev.filter(t => t.id !== optimisticTodo.id));
    }
  };
}
```

## Error Boundary

Hata durumunda tüm sayfayı çökertmek yerine ilgili bölümü gösterin:

```tsx
import { Component, ReactNode } from 'react';

interface Props { children: ReactNode; fallback: ReactNode; }
interface State { hasError: boolean; }

class ErrorBoundary extends Component<Props, State> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

// Kullanım
<ErrorBoundary fallback={<p>Bu bölüm yüklenemedi.</p>}>
  <DashboardStats />
</ErrorBoundary>
```

## İlgili Yazılar

- [Dark Mode Implementasyonu](/blog/react-dark-mode-fouc-cozumu/) — Skeleton renkleri tema ile
- [Responsive Tablo Tasarımı](/blog/react-responsive-tablo-tasarimi/) — Tablo skeleton'ı
- [React Query Cache Stratejisi](/blog/react-query-supabase-cache-stratejisi/) — Cache ile loading azaltma

---

*Bu rehber React'in [Suspense](https://react.dev/reference/react/Suspense) ve [Error Boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) dokümantasyonuna dayanarak hazırlanmıştır.*