Client Component Sınırını Yanlış Çizince
use client'ı layout'a yazınca ağacın tamamı zehirlenir

Bundle analyzer'ı ilk açtığımda layout.js şişmişti. Tema anahtarı için layout'un tepesine 'use client' yazmıştım. Anahtar 20 satırdı; yanına başlık, nav, çocuk sayfalar ve bir tarih süsü de binmişti. Analyzer yalan söylemiyordu: client sınırı o dosyadan itibaren aşağıyı boyuyordu.
App Router'da 'use client' bir virüs gibi çalışır. Dosyayı client yapar, o dosyanın import ettiği her yerel modülü de. children istisnadır: sunucu sayfası children olarak gelirse sunucu kalır. Bunu okumadan layout'u client yapmak, çocuk sayfaları da zehirlemez ama layout'un kendi import ağacını zehirler. Nav içindeki kart listesini layout'tan import ettiysen liste de client olur.
Zehirlenen ağaç
Yanlış çizim genelde üç yerde duruyor. Bir: app/layout.tsx tepesinde direktif. İki: page.tsx tepesinde direktif, çünkü bir modal var. Üç: paylaşılan Card.tsx tepesinde direktif, çünkü bir yerde onClick lazım oldu. Üçüncüsü en sinsi. Kartı bir kez client yapınca onu kullanan bütün sunucu listeleri de kartın JS'ini yükler.
import { ThemeToggle } from "@/components/theme-toggle";
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
Thinote
{children}
);
}
"use client";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const [theme, setTheme] = useState<"light" | "dark">("light");
useEffect(() => {
const stored = window.localStorage.getItem("theme");
if (stored === "dark" || stored === "light") setTheme(stored);
}, []);
function toggle() {
const next = theme === "light" ? "dark" : "light";
setTheme(next);
document.documentElement.dataset.theme = next;
window.localStorage.setItem("theme", next);
}
return (
);
}
Layout sunucu. Toggle client. localStorage ve document.documentElement yalnızca toggle'da duruyor. Nav linkleri düz veya next/link; hook yok. Bu ayrım, 'layout etkileşimli olsun' diye tüm kabuğu client yapmaktan daha sıkıcı ve daha doğru.
Kartı ikiye bölmek
Beğeni, kaydet, menü — bunlar kartın köşesinde durur. Kartın başlığı, özeti, kapak görseli hook istemez. Kartı tek dosyada client yapmak, liste sayfasının her satırına hidrasyon faturası keser. Başlığı sunucu kartında bırakıp köşeye küçük bir client menü koymak, aynı görüntüyü daha az JS ile verir.
Direktifi kaldırmak korkutucu gelebilir: 'o zaman onClick nerede duracak?'. Cevap: ayrı dosyada. card-menu.tsx 30 satır, "use client" orada. article-card.tsx sunucu, menüyü import eder. Import yönü client ← sunucu olamaz; sunucu ← client olur. Bu tek yön, sınırı dosya sisteminde görünür kılar.
page.tsx'i client yapmak
Sayfa dosyasını client yapmak, generateMetadata ile aynı dosyada durmayı da bozar. Metadata sunucuda üretilir. Sayfa client olunca veri çekimini useEffect'e kaydırırsın; birinci yazıdaki paket şişer, beşinci yazıdaki SEO boşalır. Modal veya sekme için sayfayı client yapmak yerine sayfayı sunucu bırakıp modalı children veya paralel slot olarak takıyorum.
Sınırı yanlış çizmenin semptomu bazen runtime hatası değil, sessiz kilodur. Sayfa çalışır. Analyzer ve bir hidrasyon süresi olmadan 'çalışıyor' dersin. Layout'taki tek direktifi silmek, üç sayfanın JS'ini birden düşürdü. Bunu bir kez görünce 'use client'ı varsayılan şablon olmaktan çıkarıyorum.
Paylaşılan bileşen tuzağı
ui/button.tsx bir kez client olursa, onu çağıran her sunucu formu da o JS'i ister. Buton onClick alıyorsa client doğrudur; yalnızca type ve children alıyorsa sunucu kalmalıdır. İkisini tek dosyada birleştirmek 'API basit olsun' diye cazip. Basit API, pahalı ağaçtır. İki export: Button sunucu, IconButton client. İsim çirkin, fatura küçük.
Aynı şey cn() helper'ı için geçerli değil. clsx ve tailwind-merge tarayıcıda da, sunucuda da durabilir; bunlar sır taşımaz. Asıl mesele hook ve tarayıcı nesnesi. Helper'ı client diye işaretlemek, onu import eden her sayfayı client yapmaz — 'use client' dosya sınırıdır, import edilen sunucu-uyumlu modül sunucuda da çalışır. Tersini sanmak, her utility'yi ayrı client paketine taşımaktır. Direktif, bağımlılığın kendisi değil, o dosyanın runtime'ıdır.
children istisnasını yanlış okumak
Layout client olsa bile children sunucu kalabilir. Bu cümle 'o zaman layout'u client yapayım' diye okunmamalı. Layout'un kendi import ettiği Header, Footer, Sidebar client olur. children kurtulur; kabuk kurtulmaz. Kabuk her sayfada hidrate olur. Tema anahtarı 20 satır, header 200 satırsa, 200'ü de client yapmak her gezinmede ödenir.
Daha temiz okuma: layout sunucu, header sunucu, yalnızca anahtarın durduğu 20 satır client. children zaten sunucu. İstisnayı 'layout client olabilir' diye değil, 'çocukları zehirlemeden kabukta bir ada açabilirsin' diye kullanıyorum. Ada küçükse açılır. Ada header'ın tamamıysa, yine yanlış yerdesin.
Hidrasyon uyarısı bir ipucu
Sunucu HTML'i ile client ilk render'ı ayrılınca React uyarır. Tipik sebep: layout client, Date.now() veya window.innerWidth ilk render'da. Bunu bastırmak için suppressHydrationWarning eklemek, sınırı düzeltmez. Tarihi sunucuda format et, genişliği effect'te oku. Uyarı, 'use client'ı üste yazmanın faturasıdır çoğu zaman; bileşenin kendisi değil.
Sınırı indirdikten sonra baktığım üç dosya: app/layout.tsx, app/page.tsx, components altında tepesinde direktif olan her kart. Üçünde de gerekçe 'bir yerde onClick vardı' ise gerekçe yetersiz. onClick bir dosya ister, bir ağaç değil. Serinin birinci yazısı yaprağı anlattı. Bu yazı, yaprağı köke yapıştırmanın bedelini anlattı.
Import grafiğini okumak
Analyzer'da bir chunk'ın neden şiştiğini görmek için import zincirine bakıyorum. layout → Header → SearchBox → useDebouncedValue. SearchBox client olmak zorunda; Header değil. Header'ı client yapan, SearchBox'ı aynı dosyaya yazmak. Ayırınca Header sunucu kalır, kutu 80 satır client olur. Zinciri dosya dosya yürümek, 'neden her sayfa 200 KB' sorusuna slayttan daha kısa cevap verir.
Aynı zincirde bir date-fns formatı, bir ikon paketi, bir markdown önizleyici duruyorsa suç 'use client' değil, o üç import. Direktifi indirdikten sonra importları da budamak gerekir. Sınır doğru, paket hâlâ şişman olabilir. İkinci yazı sınırı anlatır; budama ayrı bir PR. İkisini aynı günde yapınca analyzer yeşile döner, hangisinin işe yaradığı belirsiz kalır. Önce sınır, sonra import.
Test ve hikaye
Sunucu layout'u test etmek, client header'ı test etmekten farklı kurulum ister. İkisini tek dosyada eritmek, testi de 'jsdom'da her şey'e çevirir. Ayırınca yaprak için user-event, kök için render ve HTML iddiası. Bu, sınırın görünmeyen faydası. 'use client'ı üste yazmanın bedeli yalnızca bundle değil, her şeyin tarayıcıymış gibi test edilmesidir.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap