İçeriğe geç
nextjs

Next.js Server Components vs Client Components

Tarık Tunç
Next.js Server Components vs Client Components

Next.js Server Components vs Client Components

React Server Components (RSC), React ve Next.js ekosistemindeki en önemli paradigma değişikliklerinden biridir. Next.js App Router'da bileşenler varsayılan olarak Server Component'tir. Bu yaklaşım, sunucu tarafında render edilen bileşenlerle istemci tarafında çalışan bileşenleri net bir şekilde ayırarak performans ve geliştirici deneyimini büyük ölçüde iyileştirir.

Server Components Nedir?

Server Components, sunucu tarafında render edilen ve JavaScript bundle'ına dahil edilmeyen React bileşenleridir. Bu bileşenler doğrudan veritabanına erişebilir, dosya sistemiyle çalışabilir ve hassas API anahtarlarını güvenli şekilde kullanabilir.

// Bu bir Server Component (varsayılan)
// 'use client' direktifi YOK
import { db } from '@/lib/database';

export default async function ProductList() {
  // Doğrudan veritabanına erişim
  const products = await db.product.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20,
  });

  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map((product) => (
        <div key={product.id} className="border rounded-lg p-4">
          <h3 className="font-bold">{product.name}</h3>
          <p className="text-gray-600">{product.price} TL</p>
        </div>
      ))}
    </div>
  );
}

Client Components Nedir?

Client Components, tarayıcıda çalışan ve kullanıcı etkileşimi gerektiren bileşenlerdir. 'use client' direktifi ile tanımlanır:

'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="flex items-center gap-4">
      <button
        onClick={() => setCount(count - 1)}
        className="px-4 py-2 bg-red-500 text-white rounded"
      >
        -
      </button>
      <span className="text-2xl font-bold">{count}</span>
      <button
        onClick={() => setCount(count + 1)}
        className="px-4 py-2 bg-blue-500 text-white rounded"
      >
        +
      </button>
    </div>
  );
}
İpucu: 'use client' direktifi bir sınır (boundary) oluşturur. Bu direktifin altındaki bileşen ve onun import ettiği tüm bileşenler client bundle'ına dahil edilir.

Ne Zaman Hangisini Kullanmalı?

Senaryo Server Component Client Component
Veri çekme (fetch) ✅ Tercih edilir Mümkün ama ideal değil
Veritabanı erişimi ✅ Doğrudan ❌ API üzerinden
onClick, onChange ❌ Kullanamaz ✅ Gereklidir
useState, useEffect ❌ Kullanamaz ✅ Kullanabilir
Hassas bilgiler (API key) ✅ Güvenli ❌ Açığa çıkar
Büyük bağımlılıklar ✅ Bundle'a eklenmez ❌ Bundle'ı büyütür

Composition Pattern: Server ve Client Birlikte

En iyi uygulama, Server Component'lerin Client Component'leri sarmasıdır. Veri Server Component'te çekilir, etkileşimli parçalar Client Component'e aktarılır:

// app/products/page.tsx (Server Component)
import { db } from '@/lib/database';
import { ProductFilter } from './ProductFilter';
import { ProductCard } from './ProductCard';

export default async function ProductsPage() {
  const products = await db.product.findMany();
  const categories = await db.category.findMany();

  return (
    <div>
      <h1>Ürünler</h1>
      {/* Client Component - filtreleme etkileşimi */}
      <ProductFilter categories={categories} />

      {/* Server Component - sadece görüntüleme */}
      <div className="grid grid-cols-3 gap-4">
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

// ProductFilter.tsx (Client Component)
'use client';

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

export function ProductFilter({ categories }: { categories: Category[] }) {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [selected, setSelected] = useState(
    searchParams.get('category') || 'all'
  );

  const handleFilter = (category: string) => {
    setSelected(category);
    router.push(`/products?category=${category}`);
  };

  return (
    <div className="flex gap-2 my-4">
      {categories.map((cat) => (
        <button
          key={cat.id}
          onClick={() => handleFilter(cat.slug)}
          className={selected === cat.slug ? 'bg-blue-500 text-white' : 'bg-gray-200'}
        >
          {cat.name}
        </button>
      ))}
    </div>
  );
}
Uyarı: Server Component'i Client Component'in içine import etmeyin. Bunun yerine Server Component'i children veya prop olarak geçirin. Aksi halde Server Component, Client Component gibi davranır.

Children Pattern ile Doğru Kullanım

// ❌ Yanlış: Server Component, Client import'u içinde
'use client';
import ServerComponent from './ServerComponent'; // Client olarak çalışır!

export function ClientWrapper() {
  return <div><ServerComponent /></div>;
}

// ✅ Doğru: Children pattern
'use client';

export function ClientWrapper({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(true);
  return isOpen ? <div>{children}</div> : null;
}

// page.tsx (Server Component)
import { ClientWrapper } from './ClientWrapper';
import { ServerComponent } from './ServerComponent';

export default function Page() {
  return (
    <ClientWrapper>
      <ServerComponent /> {/* Server Component olarak kalır */}
    </ClientWrapper>
  );
}

Async Server Components

Server Components async olabilir, bu da veri çekmeyi son derece basitleştirir:

// Async Server Component
async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`, {
    cache: 'no-store', // Her istekte taze veri
  });
  return res.json();
}

export default async function UserProfile({
  params,
}: {
  params: { id: string };
}) {
  const user = await getUser(params.id);

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-3xl font-bold">{user.name}</h1>
      <p className="text-gray-600">{user.email}</p>
      <p className="mt-4">{user.bio}</p>
    </div>
  );
}

Serialization Kuralları

Server Component'ten Client Component'e prop geçerken, veriler serializable olmalıdır:

// ✅ Geçerli prop türleri
// string, number, boolean, null, undefined
// Array, Object (serializable)
// Date (string'e çevrilir)
// Map, Set — ❌ Geçersiz
// Fonksiyonlar — ❌ Geçersiz
// Class instance'ları — ❌ Geçersiz

// ✅ Doğru
<ClientComponent data={{ name: 'Tarik', age: 30 }} />

// ❌ Yanlış
<ClientComponent onClick={() => console.log('click')} />
// Fonksiyonlar serialize edilemez!
Bilgi: Server Actions, bu sınırlamayı aşmanın bir yoludur. Bir server action fonksiyonunu Client Component'e prop olarak geçirebilirsiniz.

Third-Party Kütüphanelerle Çalışma

Birçok React kütüphanesi henüz Server Components ile uyumlu değildir. Bu durumda wrapper bileşen oluşturabilirsiniz:

// components/ThemeProvider.tsx
'use client';

import { ThemeProvider as NextThemesProvider } from 'next-themes';

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  return (
    <NextThemesProvider attribute="class" defaultTheme="system">
      {children}
    </NextThemesProvider>
  );
}

// app/layout.tsx (Server Component)
import { ThemeProvider } from '@/components/ThemeProvider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="tr">
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

Sonuç

Server Components ve Client Components ayrımı, Next.js uygulamalarında performans ve güvenlik açısından büyük avantajlar sağlar. Temel kural basittir: mümkün olduğunca Server Component kullanın, yalnızca etkileşim gerektiğinde Client Component'e geçin. Composition pattern'lerini doğru uygulayarak her iki dünyanın en iyisinden faydalanabilirsiniz.

Kaynaklar

İlgili Yazılar