useEffect ile Data Fetch Hâlâ Neden Cazip Geliyor
İlk boyamadan sonra fetch, suyu yokuşa akıtır

Network sekmesinde ilk boya, sonra /api/home, sonra kartların dolması. useEffect, CRA ve Pages Router'dan kalan kas. 'Client'ta başla, sonra doldur' deseni App Router'da da derlenir. Kullanıcı spinner görür, bot boş kabuk görür, su şelalesi gizlenir çünkü tek bir await yoktur, hidrasyon sonrası bir fetch vardır.
Cazip gelmesinin üç sebebi var. Bir: örnek kod hâlâ öyle. İki: TypeScript ile useState yazmak, async page'den daha tanıdık. Üç: 'bu veri etkileşimli' diye tüm sayfayı client yapınca fetch de orada kalıyor. Üçü de gerçek. Hiçbiri, ilk HTML'in boş olmasını zorunlu kılmıyor.
Taşıma
// Eskiden: useEffect + useState + spinner
// Şimdi: sunucu sayfa, client yalnızca süzgeç
import { ProductGrid } from "./product-grid";
import { listProducts } from "@/lib/catalog";
export default async function ShopPage() {
const products = await listProducts();
return ;
}
"use client";
import { useMemo, useState } from "react";
type Product = { id: string; name: string; tag: string };
export function ProductGrid({ products }: { products: Product[] }) {
const [tag, setTag] = useState("all");
const visible = useMemo(
() => (tag === "all" ? products : products.filter((p) => p.tag === tag)),
[products, tag],
);
return (
{visible.map((p) => (
- {p.name}
))}
);
}
Liste sunucuda doğar. Süzgeç client. Kullanıcı JS kapalıysa liste yine durur, süzgeç durmaz — bu kabul edilebilir bir kayıp. Eski desende JS kapalıysa liste de yoktu. 'Etkileşim var diye fetch client'ta kalsın' cümlesi, süzgeci ve kaynağı aynı kutuya koyuyor.
Hâlâ bıraktığım yer
Pencere odaklı yenileme, ResizeObserver, ödeme SDK'sı, 'şimdi tıklanınca yükle'. Bunlar useEffect veya event handler ister. İlk ekranın ana listesi istemez. Ayrım: veri olmadan sayfa anlamsız mı? Anlamsızsa sunucu. Anlamlı kabuk var, süs sonra gelecekse effect. Ana listeyi süs sanmak, şelaleyi evcil hayvan gibi beslemek.
React Query, SWR — bunları silmiyorum. İkinci kez odaklanınca tazeleme, iyimser mutasyon, paylaşılan cache. İlk GET için bir sunucu sayfası hâlâ daha dürüst. Client cache'i, sunucunun verdiği ilk liste üzerine kuruyorum; boş state üzerine değil.
Şelalenin görünür hali
useEffect içinde fetch, sonra o sonuca göre ikinci fetch — klasik şelale. Sunucuda da yapılabilir; en azından tek belgede biter. Client'ta birinci yanıt hidrasyonu bekler, ikinci yanıt bir effect daha bekler. Kullanıcı iki boşluk görür. Bunu 'interaktif' diye savunmak, boşluğu özellik sanmak. İlk liste sunucuda, ikinci detay tıklanınca gelsin. Tıklanmayan detayı effect'te önceden çekmek, yedinci yazının birinci günahının tekrarı.
Strict Mode çift effect'i de unutturur. Geliştirmede iki GET, production'da bir. AbortController koymadan 'çalışıyor' demek, cleanup'suz bir alışkanlık. Sunucu sayfada abort yok, istek sınırlı. Effect'te sınır senin işin. Bunu her seferinde doğru yazacağımı iddia etmiyorum; o yüzden ilk GET'i effect'e bırakmıyorum.
Kas neden bırakılmıyor
Kurslar hâlâ useEffect + fetch gösteriyor çünkü tarayıcıda çalışıyor, backend istemiyor, kopyalanıyor. App Router'ı 'React + klasör' sanan biri için async page yabancı. Yabancılık, yanlış olduğu anlamına gelmez. Cazibe, tanıdıklık. Tanıdık desen, bot'a boş, kullanıcıya spinner, pakete API client yükler. Bir kez sunucuya taşıyınca kas zayıflıyor. Zayıflamayan yerleri yukarıda bıraktım; onları da 'eski dünya' diye silmiyorum.
Yükleme state'ini sevmek
isLoading true ile başlayan bileşen, tasarımcıya bir iskelet satar, geliştiriciye bir if. İkisi de tanıdık. Sunucu sayfada isLoading yok; ya HTML var ya örtü var. Bu yokluk, ilk gün rahatsız eder. Rahatsızlık, kasın çekilmesi. Bir hafta sonra isLoading'i yalnızca tıklama sonrası mutasyonda arıyorum. İlk GET'te aramıyorum.
SWR'ın isLoading ve isValidating ayrımı, ikinci isteği konuşur. İlk isteği konuşmuyorum. İlk istek sunucuda bitti. Client cache, revalidateOnFocus ile tazeleyebilir. O tazeleme bir effect'tir, evet; ama boş liste üzerine değil, dolu liste üzerine. Cazibe, boş listeyi normal sanmaktır. Normal olan, HTML'de listenin durmasıdır.
API route'u client'tan çağırmak
/api/home'u useEffect ile çağırmak, sunucunun zaten yapabileceği bir işi tarayıcıya ihale etmek. Aynı fonksiyonu page'de await etmek, bir hop'u siler. Hop: tarayıcı → route → veritabanı. Sunucu: page → veritabanı. Auth çerezi zaten sunucuda. Client'ın çerezi JSON'a çevirmesine gerek yok. Altıncı yazı. Yedinci yazı o hop'un neden hâlâ yazıldığını soruyor: çünkü örnek öyle, çünkü 'ayrım temiz', çünkü page'i ince tutmak istiyoruz. İnce page, şişman network.
Temiz ayrım, lib/catalog.ts. Page onu çağırır, route da çağırabilir, action da. Client çağırmaz. Client, süzgeçler ve tıklar. Bu cümle birinci yazıyla aynı. useEffect cazibesi, cümleyi unutturan şey. Unutmayınca effect kalır; ilk GET gitmez.
use() ve promise
Client'ta use(promise) ile sunucudan geçen bir thenable okumak, effect'siz bir orta yol. Promise'i sayfada yaratıp client'a vermek serileşmez; context veya children ile sarmalanmış bir okuma gerekir. Bunu 'effect öldü' diye satmıyorum. Hâlâ ilk HTML'i sunucu yazar. use(), hidrasyondan sonra akan bir ayrıntı içindir. Ana liste için page await durur.
Cazibenin son katmanı: 'nasıl olsa SEO yok, dashboard'. Dashboard'ta da ilk boya boş olmasın. Kullanıcı kendi verisini spinner ile beklerse, App Router'ı Pages gibi kullanmışsın demektir. Kendi verisi bile sunucuda çerezle okunur. Effect, pencereye özel olanadır. Veri pencereye özel değildir. Bu cümle yetince yedinci yazı kapanır.
Abort ve yarış
Hızlı süzgeç, üst üste effect tetikler. Birinci yanıt ikinciden geç gelirse eski liste boyanır. AbortController bunu keser. Sunucu sayfada yarış yok; her istek kendi belgesi. Effect'te yarış var. Yarışı sevmeyince ilk listeyi effect'e koymuyorum. Süzgeç ya searchParam ya bellekteki dizi. Bellekteki dizi, yedinci yazının taşıdığı listedir. Taşınmamış liste, yarışın evidir.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap