İçeriğe geç
Sedat Demir
Geri dön

React Server Components: Derinlemesine Teknik İnceleme ve Pratik Rehber

React Server Components: Derinlemesine Teknik İnceleme ve Pratik Rehber

React dünyası 2020'den bu yana sessiz ama köklü bir dönüşüm geçiriyor. React Server Components (RSC), ilk olarak React ekibi tarafından bir RFC olarak tanıtıldığında, birçok geliştirici bunun ne anlama geldiğini tam olarak kavrayamadı. Bugün, React 19 ve Next.js App Router ile birlikte RSC artık üretim ortamlarında aktif olarak kullanılıyor. Peki bu teknoloji gerçekten ne vaat ediyor ve nasıl çalışıyor?

Bu yazıda, React Server Components'ı yüzeysel bir tanıtımın çok ötesinde, mimari düzeyde inceleyeceğiz.


React Server Components Nedir?

React Server Components, yalnızca sunucuda çalışan ve istemciye hiçbir zaman JavaScript bundle'ı olarak gönderilmeyen React bileşenleridir. Geleneksel React bileşenlerinden farklı olarak, bu bileşenler:

Geleneksel SSR ile RSC Arasındaki Fark

Bu noktada çoğu geliştirici şu soruyu sorar: "Bu SSR'dan farklı mı?" Kesinlikle evet.

Özellik Geleneksel SSR React Server Components
Render yeri Sunucuda HTML üretilir Sunucuda React ağacı serileştirilir
Hydration Tüm bileşenler hydrate edilir Sadece Client Components hydrate edilir
JS Bundle Tüm bileşen kodu istemciye gönderilir Server Components kodu istemciye gönderilmez
State yönetimi Hydration sonrası istemcide Server Components'ta state yoktur
Yeniden render İstemcide yeniden render olur Sunucuya yeni istek ile güncellenir

Geleneksel SSR şöyle çalışır: Sunucu HTML üretir, istemciye gönderir, ardından tüm JavaScript yüklenir ve "hydration" ile sayfa interaktif hale gelir. Yani tüm bileşen kodu yine istemciye gönderilir.

RSC ise farklıdır: Server Components'ın JavaScript kodu hiçbir zaman istemciye ulaşmaz. Sunucu, bu bileşenleri render eder ve sonucu özel bir format (RSC Payload) olarak istemciye gönderir. İstemci bu payload'ı alır ve React ağacına yerleştirir.


RSC Mimarisi: İç İşleyiş

RSC Payload Formatı

Server Components render edildiğinde, sonuç HTML değil, RSC Wire Format adı verilen özel bir streaming formatında üretilir. Bu format, React ağacının serileştirilmiş bir temsilidir:

0:["$","div",null,{"children":[["$","h1",null,{"children":"Merhaba Dünya"}],["$","$Lclient-component",null,{"data":"sunucudan gelen veri"}]]}]

Bu formatta $L prefiksi, bir Client Component referansını temsil eder. React, istemci tarafında bu referansı gerçek Client Component modülüyle eşleştirir.

Bileşen Ağacı ve Sınır Kavramı

RSC mimarisinde bileşen ağacı iki katmana ayrılır:

        [Server Component]        ← Sunucuda render
              |
    ┌─────────┼─────────┐
    |         |         |
[Server]  [Client]  [Server]      ← Client = "use client" sınırı
              |
        ┌─────┼─────┐
        |           |
    [Client]    [Client]          ← Client sınırının altı otomatik Client

Kritik kural: Bir Client Component'ın import ettiği her bileşen otomatik olarak Client Component olur. Ancak bir Client Component, children prop'u aracılığıyla Server Component alabilir.


Pratikte React Server Components

Temel Bir Server Component

Next.js App Router'da, varsayılan olarak tüm bileşenler Server Component'tır:

// app/posts/page.tsx — Bu bir Server Component
import { db } from '@/lib/database';

interface Post {
  id: string;
  title: string;
  content: string;
  createdAt: Date;
}

export default async function PostsPage() {
  // Doğrudan veritabanı sorgusu - istemciye API gerekmiyor!
  const posts: Post[] = await db.query('SELECT * FROM posts ORDER BY created_at DESC');

  return (
    <main className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-8">Blog Yazıları</h1>
      <div className="space-y-6">
        {posts.map((post) => (
          <article key={post.id} className="border rounded-lg p-6">
            <h2 className="text-xl font-semibold">{post.title}</h2>
            <p className="text-gray-600 mt-2">{post.content}</p>
            <time className="text-sm text-gray-400">
              {post.createdAt.toLocaleDateString('tr-TR')}
            </time>
          </article>
        ))}
      </div>
    </main>
  );
}

Bu bileşende dikkat edilmesi gereken noktalar:

Client Component Tanımlama

İnteraktivite gerektiren bileşenler için "use client" direktifini kullanırız:

// components/SearchFilter.tsx
"use client";

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

export default function SearchFilter() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const router = useRouter();
  const searchParams = useSearchParams();

  function handleSearch(value: string) {
    setQuery(value);
    startTransition(() => {
      const params = new URLSearchParams(searchParams.toString());
      if (value) {
        params.set('q', value);
      } else {
        params.delete('q');
      }
      router.push(`/posts?${params.toString()}`);
    });
  }

  return (
    <div className="relative">
      <input
        type="text"
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Yazılarda ara..."
        className="w-full px-4 py-2 border rounded-lg"
      />
      {isPending && (
        <div className="absolute right-3 top-2.5">
          <span className="animate-spin">⏳</span>
        </div>
      )}
    </div>
  );
}

Composition Pattern: Server ve Client'ı Birleştirmek

RSC'nin en güçlü pattern'lerinden biri composition pattern'dır. Bu pattern ile Client Component'lar içinde Server Component'ları children olarak geçirebilirsiniz:

// components/InteractivePanel.tsx
"use client";

import { useState, ReactNode } from 'react';

interface InteractivePanelProps {
  title: string;
  children: ReactNode; // Server Component olabilir!
}

export default function InteractivePanel({ title, children }: InteractivePanelProps) {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div className="border rounded-lg overflow-hidden">
      <button
        onClick={() => setIsExpanded(!isExpanded)}
        className="w-full p-4 text-left font-semibold bg-gray-50 hover:bg-gray-100"
      >
        {title} {isExpanded ? '▼' : '▶'}
      </button>
      {isExpanded && (
        <div className="p-4">
          {children}
        </div>
      )}
    </div>
  );
}
// app/dashboard/page.tsx — Server Component
import InteractivePanel from '@/components/InteractivePanel';
import { db } from '@/lib/database';

async function RecentActivity() {
  // Bu Server Component, doğrudan DB'ye erişiyor
  const activities = await db.query(
    'SELECT * FROM activities ORDER BY created_at DESC LIMIT 10'
  );

  return (
    <ul className="space-y-2">
      {activities.map((activity: any) => (
        <li key={activity.id} className="flex items-center gap-2">
          <span className="text-green-500">●</span>
          {activity.description}
        </li>
      ))}
    </ul>
  );
}

export default function DashboardPage() {
  return (
    <div className="p-6 space-y-4">
      <h1 className="text-2xl font-bold">Dashboard</h1>
      
      {/* Client Component, Server Component'ı children olarak alıyor */}
      <InteractivePanel title="Son Aktiviteler">
        <RecentActivity />
      </InteractivePanel>
    </div>
  );
}

Bu pattern'de RecentActivity bir Server Component olmasına rağmen, InteractivePanel Client Component'ının içinde render ediliyor. Çünkü children olarak önceden render edilmiş haliyle geçiriliyor.


Veri Çekme Stratejileri

Paralel Veri Çekme

Server Components ile birden fazla veri kaynağından paralel olarak veri çekebilirsiniz:

// app/profile/[id]/page.tsx
import { Suspense } from 'react';

async function UserInfo({ userId }: { userId: string }) {
  const user = await fetch(`https://api.example.com/users/${userId}`, {
    next: { revalidate: 3600 } // 1 saat cache
  }).then(res => res.json());

  return (
    <div className="flex items-center gap-4">
      <img src={user.avatar} alt={user.name} className="w-16 h-16 rounded-full" />
      <div>
        <h2 className="text-xl font-bold">{user.name}</h2>
        <p className="text-gray-500">{user.email}</p>
      </div>
    </div>
  );
}

async function UserPosts({ userId }: { userId: string }) {
  const posts = await fetch(`https://api.example.com/users/${userId}/posts`, {
    next: { revalidate: 60 } // 1 dakika cache
  }).then(res => res.json());

  return (
    <div className="space-y-4">
      {posts.map((post: any) => (
        <article key={post.id} className="p-4 border rounded">
          <h3 className="font-semibold">{post.title}</h3>
          <p className="text-gray-600">{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

export default function ProfilePage({ params }: { params: { id: string } }) {
  return (
    <div className="max-w-4xl mx-auto p-6">
      <Suspense fallback={<div className="animate-pulse h-20 bg-gray-200 rounded" />}>
        <UserInfo userId={params.id} />
      </Suspense>

      <Suspense fallback={<div className="animate-pulse h-64 bg-gray-200 rounded mt-8" />}>
        <UserPosts userId={params.id} />
      </Suspense>
    </div>
  );
}

Burada Suspense sınırları sayesinde her bileşen bağımsız olarak stream edilir. UserInfo hazır olduğunda hemen gösterilir, UserPosts hâlâ yüklenirken bile.

Server Actions ile Mutasyon

React 19 ile gelen Server Actions, form işlemlerini doğrudan sunucuda çalıştırmanızı sağlar:

// app/posts/new/page.tsx
import { redirect } from 'next/navigation';
import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';

export default function NewPostPage() {
  async function createPost(formData: FormData) {
    "use server";

    const title = formData.get('title') as string;
    const content = formData.get('content') as string;

    // Validasyon
    if (!title || title.length < 3) {
      throw new Error('Başlık en az 3 karakter olmalı');
    }

    // Doğrudan veritabanına yazma
    await db.query(
      'INSERT INTO posts (title, content, created_at) VALUES ($1, $2, NOW())',
      [title, content]
    );

    // Cache'i temizle ve yönlendir
    revalidatePath('/posts');
    redirect('/posts');
  }

  return (
    <form action={createPost} className="max-w-2xl mx-auto p-6 space-y-4">
      <h1 className="text-2xl font-bold">Yeni Yazı Oluştur</h1>

      <div>
        <label htmlFor="title" className="block font-medium mb-1">Başlık</label>
        <input
          id="title"
          name="title"
          type="text"
          required
          className="w-full px-4 py-2 border rounded-lg"
        />
      </div>

      <div>
        <label htmlFor="content" className="block font-medium mb-1">İçerik</label>
        <textarea
          id="content"
          name="content"
          rows={10}
          required
          className="w-full px-4 py-2 border rounded-lg"
        />
      </div>

      <button
        type="submit"
        className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
      >
        Yayınla
      </button>
    </form>
  );
}

Performans Etkileri ve Gerçek Dünya Kazanımları

RSC'nin performans üzerindeki etkisi birçok boyutta kendini gösterir:

1. Bundle Boyutu Azalması

Büyük kütüphaneleri yalnızca sunucuda kullanarak istemci bundle'ını dramatik şekilde küçültebilirsiniz:

// Bu Server Component — marked kütüphanesi istemciye GÖNDERİLMEZ
import { marked } from 'marked';       // ~35KB
import DOMPurify from 'isomorphic-dompurify'; // ~60KB
import hljs from 'highlight.js';        // ~290KB

export default async function BlogPost({ slug }: { slug: string }) {
  const post = await db.query('SELECT * FROM posts WHERE slug = $1', [slug]);

  const highlightedContent = marked(post.content, {
    highlight: (code, lang) => hljs.highlight(code, { language: lang }).value,
  });

  const safeHtml = DOMPurify.sanitize(highlightedContent);

  return (
    <article
      className="prose prose-lg max-w-none"
      dangerouslySetInnerHTML={{ __html: safeHtml }}
    />
  );
}

Bu örnekte yaklaşık 385KB'lık kütüphane kodu istemciye hiç gönderilmiyor.

2. Waterfall Eliminasyonu

Geleneksel client-side veri çekmede sık karşılaşılan "waterfall" problemi RSC ile ortadan kalkar:

Geleneksel Client-Side:
İstemci → JS yükle → Render → fetch() → Bekle → Render → fetch() → Bekle → Render

RSC:
Sunucu → Tüm verileri paralel çek → Stream olarak gönder → İstemci render

3. Zero-Bundle Bağımlılıklar

Tarih formatlama, markdown işleme, syntax highlighting gibi ağır işlemleri sunucuda yaparak istemciyi hafif tutarsınız.


Dikkat Edilmesi Gereken Noktalar ve Yaygın Hatalar

1. Gereksiz "use client" Kullanımı

// ❌ YANLIŞ: Bu bileşen interaktif değil, "use client" gereksiz
"use client";
export default function Footer() {
  return <footer>© 2024 Şirketim</footer>;
}

// ✅ DOĞRU: "use client" olmadan Server Component olarak bırakın
export default function Footer() {
  return <footer>© 2024 Şirketim</footer>;
}

2. Seri Hale Getirilemeyen Props

Server Component'tan Client Component'a geçirilen props'lar serileştirilebilir olmalıdır:

// ❌ YANLIŞ: Fonksiyon prop olarak geçirilemez
<ClientComponent onClick={() => console.log('tık')} />

// ❌ YANLIŞ: Class instance'ları geçirilemez
<ClientComponent date={new Date()} />

// ✅ DOĞRU: Primitif değerler ve düz objeler
<ClientComponent dateString={new Date().toISOString()} />

3. Context Sınırlaması

Server Components, React Context kullanamaz. Bunun yerine props drilling veya alternatif pattern'ler kullanmanız gerekir:

// Server Component'larda tema bilgisini prop olarak geçirin
export default async function Layout({ children }: { children: React.ReactNode }) {
  const theme = await getThemeFromDB();

  return (
    <ThemeProvider theme={theme}> {/* Client Component */}
      {children}
    </ThemeProvider>
  );
}

Ne Zaman Server Component, Ne Zaman Client Component?

Senaryo Tercih
Veri çekme ✅ Server Component
Veritabanı/dosya sistemi erişimi ✅ Server Component
onClick, onChange event'leri ✅ Client Component
useState, useEffect kullanımı ✅ Client Component
Statik içerik gösterimi ✅ Server Component
Form interaksiyonları 🔄 Duruma göre (Server Actions ile hybrid)
Üçüncü parti ağır kütüphaneler ✅ Server Component (mümkünse)
Tarayıcı API'leri (localStorage, window) ✅ Client Component

Sonuç

React Server Components, sadece bir performans optimizasyonu değil, web uygulaması mimarisi hakkında düşünme biçimimizi değiştiren bir paradigma dönüşümüdür. Temel çıkarımları özetleyelim:

  1. Server Components varsayılandır — Yalnızca interaktivite gerektiğinde "use client" kullanın
  2. Bundle boyutu doğal olarak küçülür — Ağır kütüphaneler sunucuda kalır
  3. Veri çekme basitleşir — API katmanı olmadan doğrudan veritabanına erişim
  4. Composition pattern kritiktir — Server ve Client bileşenlerini doğru birleştirmek performansın anahtarıdır
  5. Streaming ve Suspense ile UX iyileşir — Kullanıcı kademeli olarak içeriği görür

RSC henüz olgunlaşma sürecinde olsa da, Next.js App Router üzerinden üretimde kullanılabilir durumda. Yeni projelerinizde bu mimariyi benimsemek, hem geliştirici deneyiminizi hem de son kullanıcı performansını önemli ölçüde artıracaktır. Mevcut projelerinizi ise kademeli olarak geçirebilirsiniz — RSC, geriye dönük uyumluluk göz önünde bulundurularak tasarlanmıştır.


Share this post on:

Sonraki Yazı
React 19'un Yeni Özellikleri: Actions, use() Hook ve Daha Fazlası