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

React Server Components Olgunlaşması: "use client" ve "use server" Paradigmasını Derinlemesine Anlamak

React Server Components Olgunlaşması: "use client" ve "use server" Paradigmasını Derinlemesine Anlamak

React ekosistemi, son birkaç yılda belki de tarihinin en büyük paradigma değişimini yaşıyor. React Server Components (RSC), ilk duyurulduğu 2020 yılından bu yana uzun bir olgunlaşma süreci geçirdi. React 19 ile birlikte artık kararlı (stable) hale gelen bu mimari, "use client" ve "use server" direktifleri etrafında şekilleniyor. Peki bu iki basit görünümlü satır, React geliştirme deneyimimizi nasıl dönüştürüyor?


Eski Dünya: Her Şey İstemcide

Klasik React uygulamalarında tüm bileşenler istemci tarafında çalışıyordu. Bir veri çekmeniz gerektiğinde useEffect içinde bir API çağrısı yapıyor, loading state yönetiyor ve sonucu render ediyordunuz:

// Eski yaklaşım - Her şey istemcide
import { useState, useEffect } from 'react';

function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(data => {
        setProducts(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <Spinner />;

  return (
    <ul>
      {products.map(p => (
        <li key={p.id}>{p.name} - {p.price}₺</li>
      ))}
    </ul>
  );
}

Bu yaklaşımın sorunları açıktı: büyük JavaScript bundle'ları, istemcide gereksiz veri çekme waterfall'ları, SEO zorlukları ve kullanıcıya boş bir sayfa gösterildikten sonra içeriğin yüklenmesini bekleme. Server Components, bu sorunların tamamını köküne inerek çözmeyi hedefliyor.


Server Components Nedir?

React Server Components, sunucu üzerinde çalışan ve istemciye JavaScript göndermeyen React bileşenleridir. HTML döndürmezler; bunun yerine React'in anlayabileceği özel bir serileştirilmiş format (RSC Payload) üretirler. Bu payload, istemcideki React ağacıyla birleştirilir.

Temel fark şudur: Server Component'ler useState, useEffect gibi istemci hook'larını kullanamazlar. Bunun yerine doğrudan veritabanına erişebilir, dosya sistemiyle çalışabilir ve async/await kullanabilirler.

// app/products/page.jsx — Bu bir Server Component (varsayılan)
import { db } from '@/lib/database';

async function ProductPage() {
  // Doğrudan veritabanından veri çekiyoruz — API katmanı yok!
  const products = await db.product.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20,
  });

  return (
    <main>
      <h1>Ürünlerimiz</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <h2>{product.name}</h2>
            <p>{product.description}</p>
            <span className="price">{product.price}₺</span>
          </li>
        ))}
      </ul>
    </main>
  );
}

export default ProductPage;

Dikkat edin: useEffect yok, loading state yok, API route yok. Bileşen sunucuda çalışıyor, veritabanıyla doğrudan konuşuyor ve istemciye sıfır JavaScript gönderiyor.


"use client" Direktifi: İstemci Sınırını Çizmek

Next.js App Router'da (ve RSC destekleyen diğer framework'lerde) tüm bileşenler varsayılan olarak Server Component'tir. Bir bileşenin istemcide çalışması gerektiğinde, dosyanın en üstüne "use client" direktifini eklemeniz gerekir.

'use client';

import { useState } from 'react';

function AddToCartButton({ productId }) {
  const [isAdding, setIsAdding] = useState(false);
  const [quantity, setQuantity] = useState(1);

  const handleAddToCart = async () => {
    setIsAdding(true);
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId, quantity }),
    });
    setIsAdding(false);
  };

  return (
    <div>
      <select value={quantity} onChange={e => setQuantity(Number(e.target.value))}>
        {[1, 2, 3, 4, 5].map(n => (
          <option key={n} value={n}>{n}</option>
        ))}
      </select>
      <button onClick={handleAddToCart} disabled={isAdding}>
        {isAdding ? 'Ekleniyor...' : 'Sepete Ekle'}
      </button>
    </div>
  );
}

export default AddToCartButton;

Ne Zaman "use client" Kullanmalısınız?

"use client" direktifini yalnızca şu durumlarda kullanmalısınız:

Kritik Kural: "use client" Bir Sınır Çizer

"use client" yalnızca o dosyayı değil, o dosyadan import edilen tüm bileşenleri de istemci bileşeni yapar. Bu nedenle, istemci sınırınızı mümkün olduğunca bileşen ağacının yaprak düğümlerine (leaf nodes) yakın tutmalısınız.

// ❌ KÖTÜ: Tüm sayfayı istemci bileşeni yapmak
'use client';
function ProductPage() { ... }

// ✅ İYİ: Sadece interaktif kısmı istemci bileşeni yapmak
// ProductPage → Server Component
//   └── ProductDetails → Server Component
//       └── AddToCartButton → 'use client'

"use server" Direktifi: Server Actions

"use server" direktifi, Server Actions tanımlamak için kullanılır. Server Actions, istemciden çağrılabilen ancak sunucuda çalışan asenkron fonksiyonlardır. Form gönderimi, veri mutasyonu ve diğer sunucu taraflı işlemler için tasarlanmıştır.

Dosya Düzeyinde Kullanım

Bir dosyanın en üstüne "use server" ekleyerek, o dosyadaki tüm export edilen fonksiyonları Server Action haline getirebilirsiniz:

// app/actions/product.js
'use server';

import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createProduct(formData) {
  const name = formData.get('name');
  const price = parseFloat(formData.get('price'));
  const description = formData.get('description');

  // Validasyon
  if (!name || !price) {
    return { error: 'İsim ve fiyat zorunludur.' };
  }

  // Veritabanına kaydet
  await db.product.create({
    data: { name, price, description },
  });

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

export async function deleteProduct(productId) {
  await db.product.delete({
    where: { id: productId },
  });

  revalidatePath('/products');
}

Server Actions'ı Form ile Kullanmak

Server Actions, HTML <form> elementinin action prop'una doğrudan geçirilebilir. Bu, progresif iyileştirme (progressive enhancement) sağlar — JavaScript devre dışı olsa bile form çalışır:

// app/products/new/page.jsx — Server Component
import { createProduct } from '@/app/actions/product';

export default function NewProductPage() {
  return (
    <form action={createProduct}>
      <div>
        <label htmlFor="name">Ürün Adı</label>
        <input type="text" id="name" name="name" required />
      </div>
      <div>
        <label htmlFor="price">Fiyat (₺)</label>
        <input type="number" id="price" name="price" step="0.01" required />
      </div>
      <div>
        <label htmlFor="description">Açıklama</label>
        <textarea id="description" name="description" rows={4} />
      </div>
      <button type="submit">Ürün Oluştur</button>
    </form>
  );
}

İstemci Bileşenlerinde Server Actions

Server Actions, istemci bileşenlerinde de kullanılabilir. React 19 ile gelen useActionState hook'u bu entegrasyonu zarif bir şekilde sağlar:

'use client';

import { useActionState } from 'react';
import { createProduct } from '@/app/actions/product';

function ProductForm() {
  const [state, formAction, isPending] = useActionState(createProduct, null);

  return (
    <form action={formAction}>
      {state?.error && (
        <div className="error-banner">{state.error}</div>
      )}
      <input type="text" name="name" placeholder="Ürün adı" />
      <input type="number" name="price" placeholder="Fiyat" step="0.01" />
      <textarea name="description" placeholder="Açıklama" />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Kaydediliyor...' : 'Kaydet'}
      </button>
    </form>
  );
}

Bileşen Kompozisyon Desenleri

RSC mimarisinin en güçlü yanlarından biri, Server ve Client Component'lerin birlikte çalışma biçimidir.

Desen 1: Server Component İçinde Client Component

Bu en yaygın ve doğal desendir:

// app/products/[id]/page.jsx — Server Component
import { db } from '@/lib/database';
import AddToCartButton from '@/components/AddToCartButton';
import ProductImageGallery from '@/components/ProductImageGallery';

export default async function ProductDetailPage({ params }) {
  const { id } = await params;
  const product = await db.product.findUnique({ where: { id } });

  return (
    <article>
      <h1>{product.name}</h1>
      <p className="price">{product.price}₺</p>
      <p>{product.description}</p>

      {/* Client Components — sadece interaktif kısımlar */}
      <ProductImageGallery images={product.images} />
      <AddToCartButton productId={product.id} />
    </article>
  );
}

Desen 2: Children Prop ile Server Component'i Client Component'e Geçirmek

Bir Client Component, children prop'u aracılığıyla Server Component'leri render edebilir:

'use client';

import { useState } from 'react';

export function Accordion({ title, children }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="accordion">
      <button onClick={() => setIsOpen(!isOpen)}>
        {title} {isOpen ? '▼' : '▶'}
      </button>
      {isOpen && <div className="accordion-content">{children}</div>}
    </div>
  );
}
// Server Component'de kullanım
import { Accordion } from '@/components/Accordion';
import { db } from '@/lib/database';

export default async function FAQPage() {
  const faqs = await db.faq.findMany();

  return (
    <div>
      {faqs.map(faq => (
        <Accordion key={faq.id} title={faq.question}>
          {/* Bu içerik Server Component olarak render edilir */}
          <p>{faq.answer}</p>
        </Accordion>
      ))}
    </div>
  );
}

Yaygın Hatalar ve Çözümleri

1. Server Component'e Hook Eklemeye Çalışmak

// ❌ HATA: Server Component'te useState kullanılamaz
async function SearchPage() {
  const [query, setQuery] = useState(''); // Bu hata verir!
  return <input onChange={e => setQuery(e.target.value)} />;
}

// ✅ ÇÖZÜM: Arama input'unu ayrı bir Client Component yapın

2. Server Component'e Serileştirilemez Prop Geçirmek

// ❌ HATA: Fonksiyonlar Client Component'e prop olarak geçirilemez
// (Server Actions hariç)
<ClientComponent onCustomEvent={() => console.log('test')} />

// ✅ ÇÖZÜM: Server Action kullanın veya mantığı Client Component'e taşıyın

3. "use client" Sınırını Çok Yukarıda Tanımlamak

// ❌ KÖTÜ: Layout seviyesinde 'use client'
'use client';
export default function Layout({ children }) {
  const [theme, setTheme] = useState('light');
  return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
}

// ✅ İYİ: Sadece provider'ı Client Component yapın
// ThemeProvider.jsx → 'use client'
// Layout.jsx → Server Component, ThemeProvider'ı import eder

Performans Etkisi: Gerçek Dünyadan Rakamlar

Server Components'in performans üzerindeki etkisi somut ve ölçülebilirdir:

Metrik Geleneksel CSR RSC ile
İlk JavaScript Bundle ~250KB+ ~80KB (sadece client bileşenler)
Time to First Byte Yavaş (boş HTML) Hızlı (içerik dolu HTML)
Largest Contentful Paint ~3-4s ~1-2s
Veri Çekme Waterfall İstemcide çoklu Sunucuda tek geçiş

Sonuç

React Server Components, "use client" ve "use server" direktifleri ile birlikte React'in geleceğini şekillendiriyor. Bu paradigma değişimi ilk bakışta karmaşık görünebilir, ancak temeldeki fikir oldukça basittir:

React 19'un kararlı sürümüyle birlikte RSC artık deneysel bir özellik değil, React'in birincil mimari modelidir. Şimdi bu paradigmayı öğrenmek ve benimsemek, gelecekte yazacağınız her React uygulaması için size güçlü bir temel sağlayacaktır.


Share this post on:

Sonraki Yazı
assistant-ui: shadcn/ui Felsefesiyle Composable AI Chat Bileşenleri Geliştirin