React Server Components: Zihinsel Modeller, Kalıplar ve Tuzaklar
Başlarken keşke var olsaydı dediğim pratik React Server Components rehberi. Zihinsel modeller, gerçek kalıplar, sınır problemi ve sen yapmayasın diye benim yaptığım hatalar.
React Server Components ile ilk üç ayı onları anladığımı sanarak geçirdim. RFC'leri okumuştum, konferans konuşmalarını izlemiştim, birkaç demo uygulama yapmıştım. Neredeyse her konuda kendinden emin bir şekilde yanılıyordum.
Sorun RSC'nin karmaşık olması değil. Sorun, zihinsel modelin React'te daha önce yaptığımız her şeyden gerçekten farklı olması ve herkesin — ben dahil — onu eski kalıba sığdırmaya çalışması. "SSR gibi." Değil. "PHP gibi." Yakın, ama hayır. "Sadece sunucuda çalışan bileşenler." Teknik olarak doğru, pratik olarak işe yaramaz.
Aşağıdaki, gerçekten bilmem gereken her şey, keşke biri bana bu şekilde açıklasaydı dediğim biçimde yazılmış. Teorik versiyon değil. Gece 11'de bir serileştirme hatasına bakıp nedenini anlamaya çalıştığın versiyon.
Gerçekten İşe Yarayan Zihinsel Model#
Bir an için React render'ı hakkında bildiğin her şeyi unut. İşte yeni resim.
Geleneksel React'te (istemci tarafı), tüm bileşen ağacın JavaScript olarak tarayıcıya gönderilir. Tarayıcı onu indirir, ayrıştırır, çalıştırır ve sonucu render eder. Her bileşen — ister 200 satırlık etkileşimli bir form olsun ister statik bir metin paragrafı — aynı boru hattından geçer.
React Server Components bunu iki dünyaya böler:
Server Components sunucuda çalışır. Bir kez çalışır, çıktılarını üretir ve sonucu istemciye gönderir — kodu değil. Tarayıcı bileşen fonksiyonunu hiçbir zaman görmez, bağımlılıklarını hiçbir zaman indirmez, hiçbir zaman yeniden render etmez.
Client Components geleneksel React gibi çalışır. Tarayıcıya gönderilir, hydrate olur, state'i yönetir, olayları işler. Zaten bildiğin React.
İçselleştirmem utanç verici derecede uzun süren temel içgörü: Server Components varsayılandır. Next.js App Router'da her bileşen, "use client" ile açıkça istemciye dahil etmediğin sürece bir Server Component'tir. Bu, alışık olduğumuzun tam tersi ve kompozisyon hakkında nasıl düşündüğünü değiştiriyor.
Render Şelalesi#
İşte bir kullanıcı bir sayfa talep ettiğinde gerçekte ne olur:
1. İstek sunucuya ulaşır
2. Sunucu, Server Components'ları yukarıdan aşağıya çalıştırır
3. Bir Server Component "use client" sınırına ulaştığında,
durur — o alt ağaç istemcide render edilecek
4. Server Components, RSC Payload üretir (özel bir format)
5. RSC Payload istemciye stream edilir
6. İstemci, Client Components'ları render eder ve sunucu
tarafında render edilmiş ağaca ekler
7. Hydration, Client Components'ları etkileşimli yapar
- adım, çoğu kafa karışıklığının yaşandığı yer. RSC Payload, HTML değil. Bileşen ağacını tanımlayan özel bir streaming formatı — sunucunun neyi render ettiği, istemcinin nerede devralması gerektiği ve sınır boyunca hangi prop'ların geçirileceği.
Kabaca şuna benzer (basitleştirilmiş):
M1:{"id":"./src/components/Counter.tsx","chunks":["272:static/chunks/272.js"],"name":"Counter"}
S0:"$Sreact.suspense"
J0:["$","div",null,{"children":[["$","h1",null,{"children":"Welcome"}],["$","$L1",null,{"initialCount":0}]]}]
Bu formatı ezberlemenize gerek yok. Ama var olduğunu — sunucu ve istemci arasında bir serileştirme katmanı olduğunu — anlamak saatlerce hata ayıklamadan kurtaracak. Her "Props must be serializable" hatası aldığında, bunun nedeni geçirdiğin bir şeyin bu dönüşümden sağ çıkamaması.
"Sunucuda Çalışır" Gerçekten Ne Demek#
Bir Server Component'in "sunucuda çalıştığını" söylediğimde, bunu kelimesi kelimesine kastediyorum. Bileşen fonksiyonu Node.js'de (veya Edge runtime'da) çalışır. Bu şu anlama gelir:
// app/dashboard/page.tsx — bu varsayılan olarak bir Server Component
import { db } from "@/lib/database";
import { headers } from "next/headers";
export default async function DashboardPage() {
const headerList = await headers();
const userId = headerList.get("x-user-id");
// Doğrudan veritabanı sorgusu. API route'a gerek yok.
const user = await db.user.findUnique({
where: { id: userId },
});
const recentOrders = await db.order.findMany({
where: { userId: user.id },
orderBy: { createdAt: "desc" },
take: 10,
});
return (
<div>
<h1>Welcome back, {user.name}</h1>
<OrderList orders={recentOrders} />
</div>
);
}useEffect yok. Yükleme durumu yönetimi yok. İşleri birbirine bağlamak için API route yok. Bileşenin kendisi veri katmanı. Bu RSC'nin en büyük kazancı ve başlangıçta en rahatsız edici hissettiren şey, çünkü sürekli "ama ayrım nerede?" diye düşünüyordum.
Ayrım "use client" sınırı. Onun üstündeki her şey sunucu. Altındaki her şey istemci. Mimarini bu oluşturur.
Sunucu/İstemci Sınırı#
Çoğu insanın anlayışının çöktüğü ve ilk birkaç ayda hata ayıklama zamanımın çoğunu geçirdiğim yer burası.
"use client" Direktifi#
Bir dosyanın en üstündeki "use client" direktifi, o dosyadan dışa aktarılan her şeyi Client Component olarak işaretler. Bu modül-seviyesinde bir anotasyon, bileşen-seviyesinde değil.
// src/components/Counter.tsx
"use client";
import { useState } from "react";
// Bu dosyanın tamamı artık "istemci bölgesi"
export function Counter({ initialCount }: { initialCount: number }) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// Bu da bir Client Component çünkü aynı dosyada
export function ResetButton({ onReset }: { onReset: () => void }) {
return <button onClick={onReset}>Reset</button>;
}Yaygın hata: her şeyi yeniden dışa aktaran bir barrel dosyasına (index.ts) "use client" koymak. Tebrikler, tüm bileşen kütüphaneni istemci tarafı yaptın. Ekiplerin bu şekilde yanlışlıkla 200KB JavaScript gönderdiğini gördüm.
Sınırı Ne Geçer#
İşte seni kurtaracak kural: sunucu-istemci sınırını geçen her şey JSON'a serileştirilebilir olmalıdır.
Serileştirilebilir olanlar:
- String, number, boolean, null, undefined
- Diziler ve düz nesneler (serileştirilebilir değerler içeren)
- Date'ler (ISO string olarak serileştirilir)
- Server Components (JSX olarak — buna geleceğiz)
- FormData
- Typed array'ler, ArrayBuffer
Serileştirilebilir OLMAYANLAR:
- Fonksiyonlar (olay işleyiciler dahil)
- Sınıflar (özel sınıf örnekleri)
- Symbol'ler
- DOM düğümleri
- Stream'ler (çoğu bağlamda)
Bu şu anlama gelir:
// app/page.tsx (Server Component)
import { ItemList } from "@/components/ItemList"; // Client Component
export default async function Page() {
const items = await getItems();
return (
<ItemList
items={items}
// HATA: Fonksiyonlar serileştirilebilir değil
onItemClick={(id) => console.log(id)}
// HATA: Sınıf örnekleri serileştirilebilir değil
formatter={new Intl.NumberFormat("en-US")}
/>
);
}Çözüm sayfayı Client Component yapmak değil. Çözüm etkileşimi aşağı, veri çekmeyi yukarı itmek:
// app/page.tsx (Server Component)
import { ItemList } from "@/components/ItemList";
export default async function Page() {
const items = await getItems();
// Sadece serileştirilebilir veri geçir
return <ItemList items={items} locale="en-US" />;
}// src/components/ItemList.tsx (Client Component)
"use client";
import { useState, useMemo } from "react";
interface Item {
id: string;
name: string;
price: number;
}
export function ItemList({ items, locale }: { items: Item[]; locale: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
// Formatter'ı istemci tarafında oluştur
const formatter = useMemo(
() => new Intl.NumberFormat(locale, { style: "currency", currency: "USD" }),
[locale]
);
return (
<ul>
{items.map((item) => (
<li
key={item.id}
onClick={() => setSelectedId(item.id)}
className={selectedId === item.id ? "selected" : ""}
>
{item.name} — {formatter.format(item.price)}
</li>
))}
</ul>
);
}"Ada" Yanılgısı#
Başlangıçta Client Components'ları "ada"lar olarak düşünüyordum — sunucu tarafında render edilmiş bir içerik denizinde küçük etkileşimli parçalar. Bu kısmen doğru ama kritik bir detayı atlıyor: bir Client Component, children veya prop olarak geçirildiklerinde Server Components'ları render edebilir.
Bu, sınırın sert bir duvar olmadığı anlamına gelir. Daha çok bir zar gibi. Sunucu tarafında render edilmiş içerik, children kalıbı aracılığıyla Client Components'ların içinden akabilir. Bunu kompozisyon bölümünde derinlemesine ele alacağız.
Veri Çekme Kalıpları#
RSC, veri çekmeyi temelden değiştiriyor. Render zamanında bilinen veriler için artık useEffect + useState + yükleme durumları yok. Ama yeni kalıpların kendi tuzakları var.
Temel Fetch ve Önbellekleme#
Bir Server Component'te, sadece fetch yaparsın. Next.js, önbellekleme eklemek için global fetch'i genişletir:
// app/products/page.tsx
export default async function ProductsPage() {
// Varsayılan olarak önbelleğe alınır — aynı URL önbellekten döner
const res = await fetch("https://api.example.com/products");
const products = await res.json();
return (
<div>
{products.map((product: Product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}Önbellekleme davranışını açıkça kontrol et:
// Her 60 saniyede yeniden doğrula (ISR benzeri davranış)
const res = await fetch("https://api.example.com/products", {
next: { revalidate: 60 },
});
// Önbellekleme yok — her zaman taze veri
const res = await fetch("https://api.example.com/user/profile", {
cache: "no-store",
});
// İsteğe bağlı yeniden doğrulama için etiketlerle önbellek
const res = await fetch("https://api.example.com/products", {
next: { tags: ["products"] },
});Ardından bir Server Action'dan etikete göre yeniden doğrulayabilirsin:
"use server";
import { revalidateTag } from "next/cache";
export async function refreshProducts() {
revalidateTag("products");
}Paralel Veri Çekme#
Gördüğüm en yaygın performans hatası: paralel çalışabilecekken sıralı veri çekme.
Kötü — sıralı (şelale):
// app/dashboard/page.tsx — BUNU YAPMA
export default async function Dashboard() {
const user = await getUser(); // 200ms
const orders = await getOrders(); // 300ms
const notifications = await getNotifications(); // 150ms
// Toplam: 650ms — her biri öncekini bekliyor
return (
<div>
<UserInfo user={user} />
<OrderList orders={orders} />
<NotificationBell notifications={notifications} />
</div>
);
}İyi — paralel:
// app/dashboard/page.tsx — BUNU YAP
export default async function Dashboard() {
// Üçü de aynı anda başlatılır
const [user, orders, notifications] = await Promise.all([
getUser(), // 200ms
getOrders(), // 300ms (paralel çalışır)
getNotifications(), // 150ms (paralel çalışır)
]);
// Toplam: ~300ms — en yavaş istekle sınırlı
return (
<div>
<UserInfo user={user} />
<OrderList orders={orders} />
<NotificationBell notifications={notifications} />
</div>
);
}Daha da iyisi — bağımsız Suspense sınırlarıyla paralel:
// app/dashboard/page.tsx — EN İYİSİ
import { Suspense } from "react";
export default function Dashboard() {
// Not: bu bileşen async DEĞİL — çocuklara devrediyor
return (
<div>
<Suspense fallback={<UserInfoSkeleton />}>
<UserInfo />
</Suspense>
<Suspense fallback={<OrderListSkeleton />}>
<OrderList />
</Suspense>
<Suspense fallback={<NotificationsSkeleton />}>
<Notifications />
</Suspense>
</div>
);
}
// Her bileşen kendi verisini çeker
async function UserInfo() {
const user = await getUser();
return <div>{user.name}</div>;
}
async function OrderList() {
const orders = await getOrders();
return <ul>{orders.map(o => <li key={o.id}>{o.title}</li>)}</ul>;
}
async function Notifications() {
const notifications = await getNotifications();
return <span>({notifications.length})</span>;
}Bu son kalıp en güçlüsü çünkü her bölüm bağımsız yüklenir. Kullanıcı, içeriği hazır olduğunda görür, ya hep ya hiç değil. Hızlı bölümler yavaş olanları beklemez.
İstek Tekilleştirme#
Next.js, tek bir render geçişi sırasında aynı URL ve seçeneklere sahip fetch çağrılarını otomatik olarak tekilleştirir. Bu, gereksiz istekleri önlemek için veri çekmeyi yukarı taşımanıza gerek olmadığı anlamına gelir:
// Bu bileşenlerin her ikisi de aynı URL'yi çekebilir
// ve Next.js sadece TEK bir gerçek HTTP isteği yapacaktır
async function Header() {
const user = await fetch("/api/user").then(r => r.json());
return <nav>Welcome, {user.name}</nav>;
}
async function Sidebar() {
// Aynı URL — otomatik tekilleştirilir, ikinci bir istek değil
const user = await fetch("/api/user").then(r => r.json());
return <aside>Role: {user.role}</aside>;
}Önemli uyarı: bu sadece fetch ile çalışır. Bir ORM veya veritabanı istemcisi doğrudan kullanıyorsan, React'in cache() fonksiyonunu kullanman gerekir:
import { cache } from "react";
import { db } from "@/lib/database";
// Veri fonksiyonunu cache() ile sar
// Artık aynı render'daki birden fazla çağrı = tek bir gerçek sorgu
export const getUser = cache(async (userId: string) => {
return db.user.findUnique({ where: { id: userId } });
});cache(), tek bir sunucu isteğinin ömrü boyunca tekilleştirir. Kalıcı bir önbellek değil — istek başına bir memoizasyon. İstek tamamlandıktan sonra önbelleğe alınan değerler çöp toplanır.
Bileşen Kompozisyon Kalıpları#
RSC'nin gerçekten zarif hale geldiği yer burası, kalıpları anladığında. Ve kalıpları anlayana kadar gerçekten kafa karıştırıcı.
"Delik Olarak Children" Kalıbı#
Bu, RSC'deki en önemli kompozisyon kalıbı ve onu tam olarak takdir etmem haftalarımı aldı. İşte problem: bazı düzen veya etkileşim sağlayan bir Client Component'in var ve içinde Server Components render etmek istiyorsun.
Bir Client Component dosyasına Server Component import edemezsin. "use client" eklediğin an, o modüldeki her şey istemci tarafı. Ama Server Components'ları children olarak geçirebilirsin:
// src/components/Sidebar.tsx — Client Component
"use client";
import { useState } from "react";
export function Sidebar({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(true);
return (
<aside className={isOpen ? "w-64" : "w-0"}>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "Close" : "Open"}
</button>
{isOpen && (
<div className="sidebar-content">
{/* Bu children'lar Server Components olabilir! */}
{children}
</div>
)}
</aside>
);
}// app/dashboard/layout.tsx — Server Component
import { Sidebar } from "@/components/Sidebar";
import { NavigationLinks } from "@/components/NavigationLinks"; // Server Component
import { UserProfile } from "@/components/UserProfile"; // Server Component
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<Sidebar>
{/* Bunlar Server Components, bir Client Component içinden geçiriliyor */}
<UserProfile />
<NavigationLinks />
</Sidebar>
<main>{children}</main>
</div>
);
}Bu neden çalışıyor? Çünkü Server Components'lar (UserProfile, NavigationLinks) önce sunucuda render edilir, sonra çıktıları (RSC payload) Client Component'e children olarak geçirilir. Client Component onların Server Component olduğunu bilmek zorunda değil — sadece önceden render edilmiş React düğümleri alır.
children'ı, sunucu tarafında render edilmiş içeriğin akabileceği Client Component'teki bir "delik" olarak düşün.
Server Components'ları Prop Olarak Geçirme#
Children kalıbı, React.ReactNode kabul eden herhangi bir prop'a genelleşir:
// src/components/TabLayout.tsx — Client Component
"use client";
import { useState } from "react";
interface TabLayoutProps {
tabs: { label: string; content: React.ReactNode }[];
}
export function TabLayout({ tabs }: TabLayoutProps) {
const [activeTab, setActiveTab] = useState(0);
return (
<div>
<div className="tab-bar" role="tablist">
{tabs.map((tab, i) => (
<button
key={i}
role="tab"
aria-selected={i === activeTab}
onClick={() => setActiveTab(i)}
className={i === activeTab ? "active" : ""}
>
{tab.label}
</button>
))}
</div>
<div role="tabpanel">{tabs[activeTab].content}</div>
</div>
);
}// app/settings/page.tsx — Server Component
import { TabLayout } from "@/components/TabLayout";
import { ProfileSettings } from "./ProfileSettings"; // Server Component — veri çekebilir
import { BillingSettings } from "./BillingSettings"; // Server Component — veri çekebilir
import { SecuritySettings } from "./SecuritySettings"; // Server Component — veri çekebilir
export default function SettingsPage() {
return (
<TabLayout
tabs={[
{ label: "Profile", content: <ProfileSettings /> },
{ label: "Billing", content: <BillingSettings /> },
{ label: "Security", content: <SecuritySettings /> },
]}
/>
);
}Her ayarlar bileşeni, kendi verisini çeken async bir Server Component olabilir. Client Component (TabLayout) sadece sekme geçişini yönetir. Bu inanılmaz güçlü bir kalıp.
Async Server Components#
Server Components async olabilir. Bu büyük bir mesele çünkü veri çekmenin bir yan etki olarak değil, render sırasında gerçekleştiği anlamına gelir:
// Bu geçerli ve güzel
async function BlogPost({ slug }: { slug: string }) {
const post = await db.post.findUnique({ where: { slug } });
if (!post) return notFound();
const author = await db.user.findUnique({ where: { id: post.authorId } });
return (
<article>
<h1>{post.title}</h1>
<p>By {author.name}</p>
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
</article>
);
}Client Components async olamaz. Bir "use client" bileşenini async yapmaya çalışırsan, React hata fırlatır. Bu kesin bir kısıtlama.
Suspense Sınırları: Streaming İlkesi#
Suspense, RSC'de streaming'i elde etme yöntemin. Suspense sınırları olmadan, sayfanın tamamı en yavaş async bileşeni bekler. Onlarla, her bölüm bağımsız olarak stream edilir:
// app/page.tsx
import { Suspense } from "react";
import { HeroSection } from "@/components/HeroSection";
import { ProductGrid } from "@/components/ProductGrid";
import { ReviewCarousel } from "@/components/ReviewCarousel";
import { RecommendationEngine } from "@/components/RecommendationEngine";
export default function HomePage() {
return (
<main>
{/* Statik — anında render eder */}
<HeroSection />
{/* Hızlı veri — çabuk gösterir */}
<Suspense fallback={<ProductGridSkeleton />}>
<ProductGrid />
</Suspense>
{/* Orta hız — hazır olduğunda gösterir */}
<Suspense fallback={<ReviewsSkeleton />}>
<ReviewCarousel />
</Suspense>
{/* Yavaş (ML destekli) — en son gösterir, geri kalanı engellemez */}
<Suspense fallback={<RecommendationsSkeleton />}>
<RecommendationEngine />
</Suspense>
</main>
);
}Kullanıcı HeroSection'ı anında görür, sonra ProductGrid stream edilir, sonra yorumlar, sonra öneriler. Her Suspense sınırı bağımsız bir streaming noktasıdır.
Suspense sınırlarını iç içe geçirmek de geçerli ve kullanışlıdır:
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentTransactions />
</Suspense>
</Dashboard>
</Suspense>Dashboard hızlı ama RevenueChart yavaşsa, dış Suspense önce çözümlenir (dashboard kabuğunu gösterir) ve grafik için iç Suspense daha sonra çözümlenir.
Suspense ile Error Boundary'ler#
Dayanıklı UI'lar için Suspense'i error.tsx ile eşleştir:
// app/dashboard/error.tsx — Client Component (olmalı)
"use client";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="p-8 text-center">
<h2>Dashboard yüklenirken bir şeyler ters gitti</h2>
<p className="text-gray-500">{error.message}</p>
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">
Tekrar dene
</button>
</div>
);
}error.tsx dosyası, ilgili route segmentini otomatik olarak bir React Error Boundary'ye sarar. O segmentteki herhangi bir Server Component hata fırlatırsa, tüm sayfayı çökertmek yerine hata UI'ı gösterilir.
Hangisini Ne Zaman Kullanmalı: Karar Ağacı#
RSC ile birkaç üretim uygulaması geliştirdikten sonra, net bir karar çerçevesine ulaştım. İşte her bileşen için geçtiğim gerçek düşünce süreci:
Server Components ile Başla (Varsayılan)#
Her bileşen, olamayacağına dair belirli bir neden olmadıkça Server Component olmalı. Bu, en önemli tek kural.
Şu Durumlarda Client Component Yap:#
1. Sadece tarayıcıda çalışan API'ler kullanıyorsa
"use client";
// window, document, navigator, localStorage, vb.
function GeoLocation() {
const [coords, setCoords] = useState<GeolocationCoordinates | null>(null);
useEffect(() => {
navigator.geolocation.getCurrentPosition(
(pos) => setCoords(pos.coords)
);
}, []);
return coords ? <p>Lat: {coords.latitude}</p> : <p>Konum yükleniyor...</p>;
}2. State veya effect gerektiren React hook'ları kullanıyorsa
useState, useEffect, useReducer, useRef (değiştirilebilir ref'ler için), useContext — bunlardan herhangi biri "use client" gerektirir.
"use client";
function SearchBar() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
// Bu bileşen Client Component OLMAK ZORUNDA çünkü
// useState kullanıyor ve kullanıcı girdisini yönetiyor
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Ara..."
/>
);
}3. Olay işleyicileri bağlıyorsa
onClick, onChange, onSubmit, onMouseEnter — herhangi bir etkileşimli davranış istemci tarafı demek.
4. İstemci tarafı kütüphaneler kullanıyorsa
Framer Motion, React Hook Form, Zustand, React Query (istemci tarafı çekme için), canvas veya SVG'ye etkileşimli olarak render eden herhangi bir grafik kütüphanesi.
Şu Durumlarda Server Component Olarak Tut:#
- Sadece veri gösteriyorsa (kullanıcı etkileşimi yok)
- Bir veritabanından veya API'den veri çekiyorsa
- Backend kaynaklarına erişiyorsa (dosya sistemi, gizli env değişkenleri)
- İstemcinin ihtiyaç duymadığı büyük bağımlılıkları import ediyorsa (markdown ayrıştırıcılar, sözdizimi renklendirme, biçimlendirme için tarih kütüphaneleri)
- Statik veya yarı-statik içerik render ediyorsa
Pratikte Gerçek Dünya Kararı#
İşte somut bir örnek. Bir ürün sayfası oluşturuyorum:
ProductPage (Server)
├── ProductBreadcrumbs (Server) — statik navigasyon, etkileşim yok
├── ProductImageGallery (Client) — zoom, kaydırma, lightbox
├── ProductInfo (Server)
│ ├── ProductTitle (Server) — sadece metin
│ ├── ProductPrice (Server) — biçimlendirilmiş sayı, etkileşim yok
│ └── AddToCartButton (Client) — onClick, sepet state'i yönetir
├── ProductDescription (Server) — render edilmiş markdown
├── Suspense
│ └── RelatedProducts (Server) — async veri çekme, yavaş API
└── Suspense
└── ProductReviews (Server)
└── ReviewForm (Client) — doğrulamalı form
Kalıba dikkat et: sayfa kabuğu ve veri yoğun parçalar Server Components. Etkileşimli adalar (ImageGallery, AddToCartButton, ReviewForm) Client Components. Yavaş bölümler (RelatedProducts, ProductReviews) Suspense ile sarılı.
Bu teorik değil. Bileşen ağaçlarım gerçekten böyle görünüyor.
Yaygın Hatalar (Hepsini Yaptım)#
Hata 1: Her Şeyi Client Component Yapmak#
Pages Router veya Create React App'ten geçiş yaparken en az direnç yolu, her şeye "use client" yapıştırmak. Çalışıyor! Hiçbir şey bozulmuyor! Ayrıca tüm bileşen ağacını JavaScript olarak gönderiyorsun ve sıfır RSC faydası alıyorsun.
Kök layout'ta "use client" olan kod tabanları gördüm. O noktada kelimenin tam anlamıyla ekstra adımlarla bir istemci tarafı React uygulaması çalıştırıyorsun.
Çözüm: Server Components ile başla. Sadece derleyici sana gerekli olduğunu söylediğinde (bir hook veya olay işleyici kullandığın için) "use client" ekle. "use client"'ı ağaçta mümkün olduğunca aşağı it.
Hata 2: Sınır Boyunca Prop Drilling#
// KÖTÜ: Server Component'te veri çekip, birden fazla
// Client Component'ten geçirmek
// app/page.tsx (Server)
export default async function Page() {
const user = await getUser();
const settings = await getSettings();
const theme = await getTheme();
return (
<ClientShell user={user} settings={settings} theme={theme}>
<ClientContent user={user} settings={settings}>
<ClientWidget user={user} />
</ClientContent>
</ClientShell>
);
}Sınırdan geçirdiğin her veri parçası RSC payload'a serileştirilir. Aynı nesneyi beş kez geçir? Payload'da beş kez var. Bundan dolayı RSC payload'larının megabaytlara şiştiğini gördüm.
Çözüm: Kompozisyon kullan. Veriyi prop olarak geçirmek yerine Server Components'ları children olarak geçir:
// İYİ: Server Components kendi verisini çeker, children olarak geçer
// app/page.tsx (Server)
export default function Page() {
return (
<ClientShell>
<UserInfo /> {/* Server Component — kendi verisini çeker */}
<Settings /> {/* Server Component — kendi verisini çeker */}
<ClientWidget>
<UserAvatar /> {/* Server Component — kendi verisini çeker */}
</ClientWidget>
</ClientShell>
);
}Hata 3: Suspense Kullanmamak#
Suspense olmadan, sayfanın Time to First Byte'ı (TTFB) en yavaş veri çekme işleminle sınırlı. Bir dashboard sayfam vardı, 4 saniyede yükleniyordu çünkü tek bir analitik sorgusu yavaştı, sayfanın geri kalan verisi 200ms'de hazır olmasına rağmen.
// KÖTÜ: her şey her şeyi bekliyor
export default async function Dashboard() {
const stats = await getStats(); // 200ms
const chart = await getChartData(); // 300ms
const analytics = await getAnalytics(); // 4000ms ← her şeyi engelliyor
return (
<div>
<Stats data={stats} />
<Chart data={chart} />
<Analytics data={analytics} />
</div>
);
}// İYİ: analitik bağımsız yüklenir
export default function Dashboard() {
return (
<div>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<Chart />
</Suspense>
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics /> {/* 4s sürüyor ama geri kalanı engellemez */}
</Suspense>
</div>
);
}Hata 4: Çalışma Zamanında Serileştirme Hataları#
Bu özellikle acı verici çünkü çoğu zaman üretime kadar yakalayamıyorsun. Sınırdan serileştirilebilir olmayan bir şey geçiriyorsun ve kriptik bir hata alıyorsun:
Error: Only plain objects, and a few built-ins, can be passed to
Client Components from Server Components. Classes or null prototypes
are not supported.
Yaygın suçlular:
- Date nesneleri geçirmek (yerine
.toISOString()kullan) - Map veya Set geçirmek (dizilere/nesnelere dönüştür)
- ORM'lerden gelen sınıf örnekleri geçirmek (
.toJSON()kullan veya düz nesnelere spread et) - Fonksiyonlar geçirmek (mantığı Client Component'e taşı veya Server Actions kullan)
Decimalalanlarıyla Prisma model sonuçları geçirmek (numberveyastring'e dönüştür)
// KÖTÜ
const user = await prisma.user.findUnique({ where: { id } });
// user serileştirilebilir olmayan alanlar içerebilir (Decimal, BigInt, vb.)
return <ClientProfile user={user} />;
// İYİ
const user = await prisma.user.findUnique({ where: { id } });
const serializedUser = {
id: user.id,
name: user.name,
email: user.email,
balance: user.balance.toNumber(), // Decimal → number
createdAt: user.createdAt.toISOString(), // Date → string
};
return <ClientProfile user={serializedUser} />;Hata 5: Her Şey İçin Context Kullanmak#
useContext sadece Client Components'da çalışır. Bir Server Component'te React context kullanmaya çalışırsan, çalışmaz. Sadece bir tema context'i kullanmak için tüm uygulamalarını Client Component yapan insanlar gördüm.
Çözüm: Temalar ve diğer global state için, sunucu tarafında ayarlanan CSS değişkenlerini kullan veya cookies() / headers() fonksiyonlarını kullan:
// app/layout.tsx (Server Component)
import { cookies } from "next/headers";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value ?? "light";
return (
<html data-theme={theme} className={theme === "dark" ? "dark" : ""}>
<body>{children}</body>
</html>
);
}Gerçekten istemci tarafı state için (auth token'ları, alışveriş sepetleri, gerçek zamanlı veri), uygun seviyede ince bir Client Component provider oluştur — kök seviyesinde değil:
// src/providers/CartProvider.tsx
"use client";
import { createContext, useContext, useState } from "react";
const CartContext = createContext<CartContextType | null>(null);
export function CartProvider({ children }: { children: React.ReactNode }) {
const [items, setItems] = useState<CartItem[]>([]);
return (
<CartContext.Provider value={{ items, setItems }}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) throw new Error("useCart, CartProvider içinde kullanılmalıdır");
return context;
}// app/shop/layout.tsx (Server Component)
import { CartProvider } from "@/providers/CartProvider";
export default function ShopLayout({ children }: { children: React.ReactNode }) {
// CartProvider bir Client Component, ama children sunucu içeriği olarak akar
return <CartProvider>{children}</CartProvider>;
}Hata 6: Paket Boyutu Etkisini Göz Ardı Etmek#
RSC'nin en büyük kazançlarından biri, Server Component kodunun asla istemciye gönderilmemesi. Ama bunu aktif olarak düşünmen gerekiyor. 50KB'lık bir markdown ayrıştırıcı kullanan ve sadece render edilmiş içerik gösteren bir bileşenin varsa — o bir Server Component olmalı. Ayrıştırıcı sunucuda kalır ve sadece HTML çıktısı istemciye gider.
// Server Component — marked sunucuda kalır
import { marked } from "marked"; // 50KB kütüphane — asla istemciye gönderilmez
async function BlogContent({ slug }: { slug: string }) {
const post = await getPost(slug);
const html = marked(post.markdown);
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}Bunu Client Component yapsaydın, marked tarayıcıya gönderilirdi. Boşuna. Kullanıcı, baştan HTML olabilecek içeriği render etmek için 50KB JavaScript indirirdi.
Paketini @next/bundle-analyzer ile kontrol et. Sonuçlar seni şaşırtabilir.
Önbellekleme Stratejisi#
Next.js 15+ sürümlerinde önbellekleme, önceki sürümlere kıyasla önemli ölçüde basitleştirildi, ama hala anlaşılması gereken farklı katmanlar var.
cache() Fonksiyonu (React)#
React'in cache()'i istek başına tekilleştirme içindir, kalıcı önbellekleme değil:
import { cache } from "react";
export const getCurrentUser = cache(async () => {
const session = await getSession();
if (!session) return null;
return db.user.findUnique({ where: { id: session.userId } });
});
// Bunu tek bir istek sırasında bileşen ağacının herhangi bir yerinde çağır.
// Sadece bir gerçek veritabanı sorgusu çalışacak.Bu, tek bir sunucu isteğine kapsamlıdır. İstek tamamlandığında, önbelleğe alınan değer kaybolur. Bu memoizasyon, önbellekleme değil.
unstable_cache (Next.js)#
İstekler arasında kalıcı önbellekleme için unstable_cache kullan (isim sonsuza dek "unstable" oldu, ama üretimde sorunsuz çalışıyor):
import { unstable_cache } from "next/cache";
const getCachedProducts = unstable_cache(
async (categoryId: string) => {
return db.product.findMany({
where: { categoryId },
include: { images: true },
});
},
["products-by-category"], // önbellek anahtar öneki
{
revalidate: 3600, // her saat yeniden doğrula
tags: ["products"], // isteğe bağlı yeniden doğrulama için
}
);
// Bir Server Component'te kullanım
async function ProductGrid({ categoryId }: { categoryId: string }) {
const products = await getCachedProducts(categoryId);
return <Grid items={products} />;
}Geçersiz kılmak için:
"use server";
import { revalidateTag } from "next/cache";
export async function onProductUpdate() {
revalidateTag("products");
}Statik vs Dinamik Render#
Next.js, bir route'un statik mi yoksa dinamik mi olduğuna içinde kullanılanlarına göre karar verir:
Statik (build zamanında render edilir, önbelleğe alınır):
- Dinamik fonksiyon yok (
cookies(),headers(),searchParams) - Tüm
fetchçağrılarında önbellekleme etkin export const dynamic = "force-dynamic"yok
Dinamik (istek başına render edilir):
cookies(),headers()veyasearchParamskullanırcache: "no-store"ilefetchkullanırexport const dynamic = "force-dynamic"varnext/server'danconnection()veyaafter()kullanır
Hangi route'ların statik veya dinamik olduğunu next build çalıştırarak kontrol edebilirsin — altta bir açıklama gösterir:
Route (app) Size First Load JS
┌ ○ / 5.2 kB 92 kB
├ ○ /about 1.1 kB 88 kB
├ ● /blog/[slug] 2.3 kB 89 kB
├ λ /dashboard 4.1 kB 91 kB
└ λ /api/products 0 B 87 kB
○ Static ● SSG λ Dynamic
Önbellekleme Hiyerarşisi#
Önbelleklemeyi katmanlar halinde düşün:
1. React cache() — istek başına, bellekte, otomatik tekilleştirme
2. fetch() cache — istekler arası, GET istekleri için otomatik
3. unstable_cache() — istekler arası, fetch olmayan işlemler için
4. Full Route Cache — render edilmiş HTML build/yeniden doğrulama zamanında önbelleğe alınır
5. Router Cache (istemci) — tarayıcı içi ziyaret edilmiş route'ların önbelleği
Her katman farklı bir amaca hizmet eder. Her zaman hepsine ihtiyacın olmaz, ama hangisinin aktif olduğunu anlamak "neden verim güncellenmiyor?" sorunlarını hata ayıklamaya yardımcı olur.
Gerçek Dünya Önbellekleme Stratejisi#
İşte üretimde gerçekten yaptığım şey:
// lib/data/products.ts
import { cache } from "react";
import { unstable_cache } from "next/cache";
import { db } from "@/lib/database";
// İstek başına tekilleştirme: bunu bir render'da birden fazla çağır,
// sadece bir DB sorgusu çalışır
export const getProductById = cache(async (id: string) => {
return db.product.findUnique({
where: { id },
include: { category: true, images: true },
});
});
// İstekler arası önbellek: sonuçlar istekler arasında kalıcı,
// her 5 dakikada veya etiketle isteğe bağlı yeniden doğrulama
export const getPopularProducts = unstable_cache(
async () => {
return db.product.findMany({
orderBy: { salesCount: "desc" },
take: 20,
include: { images: true },
});
},
["popular-products"],
{ revalidate: 300, tags: ["products"] }
);
// Önbellekleme yok: her zaman taze (kullanıcıya özel veri için)
export const getUserCart = cache(async (userId: string) => {
// Buradaki cache() sadece istek başına tekilleştirme için, kalıcılık değil
return db.cart.findUnique({
where: { userId },
include: { items: { include: { product: true } } },
});
});Temel kural: seyrek değişen genel veriler unstable_cache alır. Kullanıcıya özel veriler sadece tekilleştirme için cache() alır. Gerçek zamanlı veriler hiç önbelleklenmez.
Server Actions: Geri Dönüş Köprüsü#
Server Actions, RSC hikayesini tamamladıkları için kendi bölümlerini hak ediyor. Client Components'ların API route'ları olmadan sunucuyla iletişim kurma yöntemleri.
// app/actions/newsletter.ts
"use server";
import { db } from "@/lib/database";
import { z } from "zod";
const emailSchema = z.string().email();
export async function subscribeToNewsletter(formData: FormData) {
const email = formData.get("email");
const result = emailSchema.safeParse(email);
if (!result.success) {
return { error: "Geçersiz e-posta adresi" };
}
try {
await db.subscriber.create({
data: { email: result.data },
});
return { success: true };
} catch (e) {
if (e instanceof Error && e.message.includes("Unique constraint")) {
return { error: "Zaten abone" };
}
return { error: "Bir şeyler ters gitti" };
}
}// src/components/NewsletterForm.tsx
"use client";
import { useActionState } from "react";
import { subscribeToNewsletter } from "@/app/actions/newsletter";
export function NewsletterForm() {
const [state, formAction, isPending] = useActionState(
async (_prev: { error?: string; success?: boolean } | null, formData: FormData) => {
return subscribeToNewsletter(formData);
},
null
);
return (
<form action={formAction}>
<input
type="email"
name="email"
required
placeholder="you@example.com"
disabled={isPending}
/>
<button type="submit" disabled={isPending}>
{isPending ? "Abone olunuyor..." : "Abone Ol"}
</button>
{state?.error && <p className="text-red-500">{state.error}</p>}
{state?.success && <p className="text-green-500">Abone oldun!</p>}
</form>
);
}Server Actions, RSC dünyasında "veriyi nasıl değiştiririm?" sorusunun cevabıdır. Form gönderimleri, mutasyonlar ve yan etkiler için çoğu API route'un yerini alır.
Server Actions için temel kurallar:
- Girdiyi her zaman doğrula (fonksiyon istemciden çağrılabilir — bir API endpoint gibi davran)
- Her zaman serileştirilebilir veri döndür
- Sunucuda çalışırlar, yani veritabanlarına, dosya sistemlerine, gizli anahtarlara erişebilirsin
- Mutasyonlardan sonra önbelleğe alınan veriyi güncellemek için
revalidatePath()veyarevalidateTag()çağırabilirler
Geçiş Kalıpları#
Mevcut bir React uygulamanız varsa (Pages Router, Create React App, Vite), RSC'ye geçiş tamamen yeniden yazma olmak zorunda değil. İşte benim yaklaşımım.
Adım 1: Bileşenlerini Haritalandır#
Bileşen ağacını gözden geçir ve her şeyi sınıflandır:
Bileşen State? Effect? Event? → Karar
─────────────────────────────────────────────────────────
Header Hayır Hayır Hayır → Server
NavigationMenu Hayır Hayır Evet → Client (mobil toggle)
Footer Hayır Hayır Hayır → Server
BlogPost Hayır Hayır Hayır → Server
SearchBar Evet Evet Evet → Client
ProductCard Hayır Hayır Evet → Client (onClick) veya böl
UserAvatar Hayır Hayır Hayır → Server
CommentForm Evet Evet Evet → Client
Sidebar Evet Hayır Evet → Client (daraltma toggle)
MarkdownRenderer Hayır Hayır Hayır → Server (büyük bağımlılık kazancı)
DataTable Evet Evet Evet → Client (sıralama, filtreleme)
Adım 2: Veri Çekmeyi Yukarı Taşı#
En büyük mimari değişiklik, veri çekmeyi bileşenlerdeki useEffect'ten async Server Components'a taşımak. Gerçek geçiş çabasının yaşandığı yer burası.
Önce:
// Eski kalıp — Client Component'te veri çekme
"use client";
function ProductPage({ id }: { id: string }) {
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(`/api/products/${id}`)
.then(res => res.json())
.then(data => setProduct(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, [id]);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
if (!product) return <NotFound />;
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} />
</div>
);
}Sonra:
// Yeni kalıp — Server Component çeker, Client Component etkileşime geçer
// app/products/[id]/page.tsx (Server Component)
import { db } from "@/lib/database";
import { notFound } from "next/navigation";
import { AddToCartButton } from "@/components/AddToCartButton";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
if (!product) notFound();
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} price={product.price} />
</div>
);
}Yükleme durumu yönetimi yok. Hata durumu yok. useEffect yok. Framework tüm bunları Suspense ve error boundary'ler aracılığıyla hallediyor.
Adım 3: Bileşenleri Etkileşim Sınırlarında Böl#
Birçok bileşen büyük ölçüde statiktir ve küçük bir etkileşimli kısımları vardır. Onları böl:
Önce (büyük tek bir Client Component):
"use client";
function ProductCard({ product }: { product: Product }) {
const [isFavorite, setIsFavorite] = useState(false);
return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.description}</p>
<span>${product.price}</span>
<button onClick={() => setIsFavorite(!isFavorite)}>
{isFavorite ? "♥" : "♡"}
</button>
</div>
);
}Sonra (küçük bir Client adasıyla Server Component):
// ProductCard.tsx (Server Component)
import { FavoriteButton } from "./FavoriteButton";
function ProductCard({ product }: { product: Product }) {
return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.description}</p>
<span>${product.price}</span>
<FavoriteButton productId={product.id} />
</div>
);
}// FavoriteButton.tsx (Client Component)
"use client";
import { useState } from "react";
export function FavoriteButton({ productId }: { productId: string }) {
const [isFavorite, setIsFavorite] = useState(false);
return (
<button
onClick={() => setIsFavorite(!isFavorite)}
aria-label={isFavorite ? "Favorilerden kaldır" : "Favorilere ekle"}
>
{isFavorite ? "♥" : "♡"}
</button>
);
}Resim, başlık, açıklama ve fiyat artık sunucu tarafında render ediliyor. Sadece küçük favori butonu Client Component. Daha az JavaScript, daha hızlı sayfa yüklemeleri.
Adım 4: API Route'ları Server Actions'a Dönüştür#
Sadece kendi frontend'ine hizmet eden (dış tüketicilere değil) API route'ların varsa, çoğu Server Actions olabilir:
Önce:
// app/api/contact/route.ts
export async function POST(request: Request) {
const body = await request.json();
// doğrula, DB'ye kaydet, e-posta gönder
return Response.json({ success: true });
}
// Client component
const response = await fetch("/api/contact", {
method: "POST",
body: JSON.stringify(formData),
});Sonra:
// app/actions/contact.ts
"use server";
export async function submitContactForm(formData: FormData) {
// doğrula, DB'ye kaydet, e-posta gönder
return { success: true };
}
// Client component — fonksiyonu doğrudan çağır
import { submitContactForm } from "@/app/actions/contact";API route'ları şunlar için tut: webhook'lar, dış API tüketicileri, özel HTTP başlıkları veya durum kodları gerektiren şeyler, streaming ile dosya yüklemeleri.
Adım 5: RSC Bileşenlerini Test Etme#
Server Components'ı test etmek, async olabildikleri için biraz farklı bir yaklaşım gerektirir:
// __tests__/ProductPage.test.tsx
import { render, screen } from "@testing-library/react";
import ProductPage from "@/app/products/[id]/page";
// Veritabanını mock'la
vi.mock("@/lib/database", () => ({
db: {
product: {
findUnique: vi.fn(),
},
},
}));
describe("ProductPage", () => {
it("ürün detaylarını render eder", async () => {
const { db } = await import("@/lib/database");
vi.mocked(db.product.findUnique).mockResolvedValue({
id: "1",
name: "Test Product",
description: "A great product",
price: 29.99,
});
// Server Components async — JSX'i bekle
const jsx = await ProductPage({
params: Promise.resolve({ id: "1" })
});
render(jsx);
expect(screen.getByText("Test Product")).toBeInTheDocument();
expect(screen.getByText("A great product")).toBeInTheDocument();
});
});Temel fark: async olduğu için bileşen fonksiyonunu await edersin. Sonra ortaya çıkan JSX'i render edersin. Geri kalan her şey geleneksel React Testing Library ile aynı çalışır.
RSC Projeleri İçin Dosya Yapısı#
İşte birkaç projeden sonra vardığım yapı. Fikirli, ama işe yarıyor:
src/
├── app/
│ ├── layout.tsx ← Kök layout (Server Component)
│ ├── page.tsx ← Ana sayfa (Server Component)
│ ├── (marketing)/ ← Pazarlama sayfaları için route grubu
│ │ ├── about/page.tsx
│ │ └── pricing/page.tsx
│ ├── (app)/ ← Kimlik doğrulamalı uygulama için route grubu
│ │ ├── layout.tsx ← Auth kontrolüyle uygulama kabuğu
│ │ ├── dashboard/
│ │ │ ├── page.tsx
│ │ │ ├── loading.tsx ← Bu route için Suspense fallback
│ │ │ └── error.tsx ← Bu route için Error boundary
│ │ └── settings/
│ │ └── page.tsx
│ └── actions/ ← Server Actions
│ ├── auth.ts
│ └── products.ts
├── components/
│ ├── ui/ ← Paylaşılan UI ilkelleri (çoğunlukla Client)
│ │ ├── Button.tsx ← "use client"
│ │ ├── Dialog.tsx ← "use client"
│ │ └── Card.tsx ← Server Component (sadece stil)
│ └── features/ ← Özelliğe özel bileşenler
│ ├── products/
│ │ ├── ProductGrid.tsx ← Server (async, veri çeker)
│ │ ├── ProductCard.tsx ← Server (sunumsal)
│ │ ├── ProductSearch.tsx ← Client (useState, girdi)
│ │ └── AddToCart.tsx ← Client (onClick, mutasyon)
│ └── blog/
│ ├── PostList.tsx ← Server (async, veri çeker)
│ ├── PostContent.tsx ← Server (markdown render)
│ └── CommentSection.tsx ← Client (form, gerçek zamanlı)
├── lib/
│ ├── data/ ← Veri erişim katmanı
│ │ ├── products.ts ← cache() sarılı DB sorguları
│ │ └── users.ts
│ ├── database.ts
│ └── utils.ts
└── providers/
├── ThemeProvider.tsx ← "use client" — temaya ihtiyaç duyan kısımları sarar
└── CartProvider.tsx ← "use client" — sadece mağaza bölümünü sarar
Temel prensipler:
- Server Components'ın direktifi yok — varsayılanlar
- Client Components açıkça işaretli — bir bakışta anlayabilirsin
- Veri çekme
lib/data/'da yaşar —cache()veyaunstable_cacheile sarılı - Server Actions
app/actions/'da yaşar — uygulamayla aynı yerde, açıkça ayrılmış - Provider'lar minimum gerekli olanı sarar — tüm uygulamayı değil
Sonuç#
React Server Components sadece yeni bir API değil. Kodun nerede çalıştığı, verinin nerede yaşadığı ve parçaların nasıl bağlandığı hakkında farklı bir düşünce biçimi. Zihinsel model değişimi gerçek ve zaman alıyor.
Ama bir kez yerine oturduğunda — sınırla savaşmayı bırakıp etrafında tasarlamaya başladığında — daha önce yaptıklarımızdan daha hızlı, daha basit ve daha bakımı kolay uygulamalarla karşılaşıyorsun. İstemciye daha az JavaScript gönderilir. Veri çekme seremoni gerektirmez. Bileşen ağacı mimari olur.
Geçiş buna değer. Sadece ilk birkaç projenin rahatsız hissettireceğini bil, ve bu normal. RSC kötü olduğu için zorlanmıyorsun. Gerçekten yeni olduğu için zorlanıyorsun.
Her yerde Server Components ile başla. "use client"'ı yapraklara doğru it. Yavaş şeyleri Suspense ile sar. Veriyi render edildiği yerde çek. Children aracılığıyla kompoze et.
Tüm oyun planı bu. Geri kalan her şey detay.