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:
- Sunucuda render edilir ve istemciye serileştirilmiş bir UI ağacı olarak gönderilir
- İstemci tarafında hiçbir zaman yeniden render edilmez
- Doğrudan veritabanına, dosya sistemine ve diğer sunucu kaynaklarına erişebilir
- İstemciye gönderilen JavaScript bundle boyutunu önemli ölçüde azaltır
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 ClientKritik 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:
asyncfonksiyon olarak tanımlanmış — Server Components doğrudanasync/awaitkullanabilir- Doğrudan veritabanı erişimi var — API endpoint'e gerek yok
useState,useEffectgibi hook'lar kullanılmamış — çünkü kullanılamaz- Bu bileşenin JavaScript kodu istemciye gönderilmez
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 render3. 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:
- Server Components varsayılandır — Yalnızca interaktivite gerektiğinde
"use client"kullanın - Bundle boyutu doğal olarak küçülür — Ağır kütüphaneler sunucuda kalır
- Veri çekme basitleşir — API katmanı olmadan doğrudan veritabanına erişim
- Composition pattern kritiktir — Server ve Client bileşenlerini doğru birleştirmek performansın anahtarıdır
- 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.