Kısım II · İnşaat · Bölüm 33
Sunucu Verisini Yönetmek: TanStack
Arayüz sunucudan veri çekince (Server state with TanStack)
Neden burada: Arayüz (Bölüm 31–32) ekrandaki her şeyi bir durum (state) değişkeninden çizmeyi öğrendi. O değişkenin içinde kendi ürettiğimiz bir şey durduğu sürece iş kolaydı. Ama leaderboard'daki puanlar bizim değil; uzaktaki bir sunucunun. Arayüz o veriyi çekmeye başlayınca dört yeni sorun doğar: veri henüz gelmedi (yükleniyor), hiç gelmedi (hata), geldi ama bu arada eskidi (bayat veri), aynısını onuncu kez mi istiyoruz (önbellek). Bu bölüm bu dört sorunun adını koyar ve TanStack Query'nin onları nasıl çözdüğünü gösterir.
Neye dayanıyor: Bölüm 26.5: tarayıcıda fetch ile sunucuya istek atmanın ilk hali. Bölüm 31.4: useState ve useEffect kancaları; bu bölümde onlarla veri çekmenin neden yetmediğini göreceksin. Bölüm 8.2: önbellek fikri (sık kullanılanı yakına koy); burada aynı fikrin bayatlama sorunuyla karşılaştığı yeri görürsün. Ayrıca Bölüm 25.6'daki Promise ve async/await ve Bölüm 30'daki tipler kod örneklerinde geçer.
- Kat 10Bizim uygulamamızLeaderboard, puan, seri. Veriyi gösteren kullanıcı arayüzü burada; TanStack Query de bu katta, arayüzün içinde çalışır.
- Kat 9Web ve tarayıcı: HTML, CSS, JavaScript (ve telefonda React Native)Sunucuya isteği
fetchile bu kattaki kod atar. Bu bölüm: gelen verinin bu katta nasıl yönetildiği. - Kat 8Ağ ve internetİstek ve yanıt buradan geçer (HTTP). Bölüm 35'te ayrıntısı.
- Kat 0–7Elektrikten işletim sistemineHepsi hâlâ altımızda; bu bölümde yeni bir şey yok.
Bu bölümün sonunda şunları yapabileceksin: bir verinin istemci durumu mu sunucu durumu mu olduğunu söylemek; useEffect ile veri çekmenin hangi sorunları çözümsüz bıraktığını sayabilmek; TanStack Query'nin useQuery ve useMutation çağrılarını okuyup ne yaptıklarını, ne zaman ağa çıktıklarını ve ne zaman çıkmadıklarını söyleyebilmek; "bayat", "taze", "geçersiz kılmak" kelimelerini bir agent'ın önerdiği koda bakarak yorumlayabilmek. Bölümün sonunda TanStack'in Router ve Start parçalarını kısaca tanıyıp neden bizim yığında Query'nin merkezde olduğunu göreceksin.
TanStack, aynı ekibin çıkardığı bir kütüphane ailesinin adıdır: Query, Router, Start, Table ve başkaları. Hangisini kastettiğimiz bu kitapta açıkça yazılacak. Uygulamamızda hangi TanStack parçasının kullanılacağı henüz netleşmedi; Expo ile yazılan bir mobil uygulamada en olası ve en yararlı parça TanStack Query'dir, bölüm onu merkeze alır. Router ve Start 33.3'te kısaca anlatılır. Emre ile projedeki gerçek kararı kontrol et.
33.1 İstemci durumu ile sunucu durumu
Bölüm 26.3'te durum (state), arayüzün o an hatırlaması gereken şeydi: kaçıncı soruda olduğun, o anki puan, hangi sekmenin açık olduğu. Bölüm 31'de React bu durumu useState ile bir değişkende tutmayı ve değişince ekranı yeniden çizmeyi öğretti. Şimdi bu tek kelimeyi ikiye ayırmamız gerekiyor, çünkü iki ayrı tür durum var ve ikisinin yaşam kuralları birbirine hiç benzemiyor.
İstemci durumu: sahibi sensin
İstemci durumu (client state), yalnızca kullanıcının cihazında, programın içinde yaşayan durumdur. Örnekler: bir metin kutusuna henüz yazılmış ama gönderilmemiş cevap, bir pencerenin açık ya da kapalı olması, karanlık mod seçimi, bir quiz sorusunda şu an hangi şıkkın işaretli olduğu. Bu veri senin elindedir; kimse onu sen farkına varmadan değiştiremez; okumak için beklemen gerekmez (anında, bellekten gelir); kaybolursa en kötü ihtimalle kullanıcı sayfayı yeniler.
Sunucu durumu: ödünç alınmış bir kopya
Sunucu durumu (server state) ise asıl sahibi başka bir yerde olan verinin senin ekranındaki kopyasıdır. Leaderboard'daki sıralama, kullanıcının toplam puanı, bölümlerin listesi: hepsi sunucudaki veritabanında durur, ekrandakiler fotokopidir. TanStack Query'nin kendi belgesi bu farkı dört maddeyle söyler: sunucu durumu uzakta, senin sahip olmadığın ya da kontrol etmediğin bir yerde kalıcı olarak durur; okumak ve güncellemek için eşzamansız (asynchronous) arayüzler gerektirir; ortak sahiplik ima eder, yani başkaları sen bilmeden onu değiştirebilir; ve dikkat etmezsen uygulamanda "güncelliğini yitirmiş" olabilir.1
Bu dört madde, hukukçu için tanıdık bir şeye benziyor: tapu kaydı. Elindeki fotokopi bir an önce doğrudur, ama asıl kayıt tapu müdürlüğündedir ve sen görmeden değişebilir. Önemli bir işlem yapacaksan (satış gibi) fotokopiye güvenmez, güncel kaydı çıkarttırsın. Arayüz de leaderboard'un fotokopisini gösterir; "bu kopya ne kadar eski, ne zaman yenilemeliyim" sorusu, sunucu durumunun bütün zorluğudur.
| Soru | İstemci durumu | Sunucu durumu |
|---|---|---|
| Asıl kopya nerede? | Cihazda, uygulamanın belleğinde | Sunucuda (veritabanında) |
| Okumak ne kadar sürer? | Anında | Ağ gecikmesi kadar; eşzamansız |
| Başkası değiştirebilir mi? | Hayır | Evet, haberin olmadan |
| Yanlış/eski olabilir mi? | Hayır, tanım gereği o doğru | Evet, her an |
| Örnek (uygulamamız) | Seçili şık, açık menü, karanlık mod | Leaderboard, toplam puan, soru listesi, seri |
Naif yol: useEffect ile veri çekmek
Sunucu durumunu yönetecek özel bir araç olmasaydı, Bölüm 31.4'te öğrendiklerinle ne yapardın? Büyük ihtimalle şunu: bileşen ekrana gelince useEffect içinde fetch çağır, sonucu useState'e yaz. Bu kodu tip güvenceli olarak (Bölüm 30) yazalım:
import { useEffect, useState } from 'react'
import { fetchLeaderboard, type LeaderboardEntry } from './api'
function Leaderboard() {
const [rows, setRows] = useState<LeaderboardEntry[] | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetchLeaderboard(10)
.then(setRows)
.catch((e: Error) => setError(e.message))
}, [])
if (error) return <p>Hata: {error}</p>
if (!rows) return <p>Yükleniyor…</p>
return (
<ol>
{rows.map((r) => (
<li key={r.user_id}>{r.name}: {r.points}</li>
))}
</ol>
)
}
Satır satır: rows henüz gelmemiş veriyi (null) ya da gelmiş listeyi tutar; error hatayı tutar. useEffect, ikinci parametresi boş dizi ([]) olduğu için yalnızca bileşen ilk ekrana geldiğinde bir kez çalışır (Bölüm 31.4). Gelen veri setRows ile durum olur, durum değişince bileşen yeniden çizilir. Kod çalışır. Ama her satırı yazarken bir şeyi unuttuk, hem de çok şeyi:
- Yarış durumu: Kullanıcı hızlıca "ilk 10"dan "ilk 50"ye geçerse iki istek yolda olur; hangisi önce dönerse ekrana o yazılır, yanıtlar ters sırada gelirse ekran yanlış veriyi gösterir. Bölüm 14.4'teki yarış durumunun arayüzdeki karşılığı.
- Tekrar: Aynı leaderboard iki ayrı bileşende gösteriliyorsa (ana ekranda ve profilde), iki ayrı ağ isteği çıkar. Kimse "bu veriyi zaten çektim" demez.
- Önbellek yok: Kullanıcı başka ekrana gidip dönünce bileşen yeniden doğar,
rowsyinenull, yeniden "Yükleniyor…". Elimizde dün çektiğimiz kopya olsa da kullanmıyoruz. - Bayatlık yönetimi yok: Veri ekranda dakikalarca durur; ne zaman yenileneceğine karar veren yok. Kullanıcı başka bir uygulamadan dönünce de bayat veri görür.
- Geçici hata: Metro'da bir saniyelik kesinti olsa hemen "Hata" yazarız; yeniden deneme yok.
- Puan gönderdikten sonra: Kullanıcı bir soruyu doğru cevaplayınca leaderboard eskir. Kimin, ne zaman, yeniden çekeceğini her ekran için elle yazmak gerekir.
Bunların her biri tek tek çözülebilir, ama her bileşende, her veri türü için, elle. Uygulama büyüdükçe bu kod, asıl işten (hukuk dersleri) daha çok yer kaplar. TanStack Query'nin belgesi bu durumu şöyle özetler: çekirdek çerçevelerin çoğu veriyi tutarlı biçimde çekmek ve güncellemek için bir yol sunmaz; geliştiriciler ya kendi yöntemlerini icat eder, ya da genel amaçlı durum yönetimi kütüphanelerini eşzamansız veri için kullanır. Oysa bu kütüphaneler istemci durumu için iyidir, sunucu durumu "tamamen farklı" bir iştir.1 Belge, çözülmesi gereken sorunları da sayar: önbellek (belgenin kendi sözleriyle "programlamada belki de yapılması en zor şey"), aynı veri için birden çok isteği tek isteğe indirmek, bayat veriyi arka planda güncellemek, verinin ne zaman bayatladığını bilmek, sayfalama ve tembel yükleme, bellek yönetimi.1
Bölüm 8.2'de önbelleğin fikri şuydu: sık kullanılanı yakına koy, isabet olursa uzak ve yavaş kata gitme. İşlemcinin L1 önbelleği ile RAM arasındaki ilişki, bizim arayüzdeki bellek kopyası ile sunucu arasındaki ilişkiye benzer: yakın olan hızlı, uzak olan yavaş. Ama bir fark var ve bölümün asıl konusu o: işlemci önbelleğindeki kopya, RAM'deki asıl veriyle donanım tarafından tutarlı tutulur. Bizim kopyamızı sunucudaki veriyle kimse otomatik olarak tutarlı tutmaz. Başkası puanını artırdığında telefonundaki leaderboard bunu bilmez. Bu yüzden sunucu durumunun önbelleği, "ne zamana kadar bu kopyaya güvenebilirim?" sorusunu da cevaplamak zorundadır.
33.2 TanStack Query: sorgu, değişiklik, önbellek, yeniden doğrulama
TanStack Query (eski adıyla React Query), belgesinin kendi tanımıyla, web uygulamalarında sunucu durumunu çekmeyi, önbelleğe almayı, eşitlemeyi ve güncellemeyi kolaylaştıran bir kütüphanedir.1 Bir kütüphanedir, çerçeve değil (Bölüm 31.1): çağırdığın araç, seni çağıran iskelet değil. Tek bir iş yapar ve onu bizim yerimize iyi yapar: sunucu durumunun yaşam döngüsünü yönetmek.
Kurulum: iki parça
Kütüphane npm paketidir (Bölüm 29): @tanstack/react-query. Uygulamada iki şeye ihtiyaç var. Birincisi bir sorgu istemcisi (QueryClient): bütün önbelleği ve ayarları tutan nesne. İkincisi bu istemciyi bütün bileşenlere ulaştıran QueryClientProvider. React'te bir bileşen ağacının üstünde sarmalayıcı koyup altındakilerin hepsine bir şey sağlamak, Bölüm 31'de gördüğün bileşen mantığının doğal uzantısıdır:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
export function Root() {
return (
<QueryClientProvider client={queryClient}>
<Leaderboard />
</QueryClientProvider>
)
}
new QueryClient() önbelleğin kendisini oluşturur ve uygulamanın ömrü boyunca tek bir tane tutulur (bu yüzden bileşenin dışında). QueryClientProvider, altındaki her bileşenin bu önbelleğe erişmesini sağlar. Provider'ı bir kez en üste koyarsın, sonra unutursun.2 Telefonda da aynı: belge, React Query'nin React Native ile kutudan çıktığı gibi çalışacak biçimde tasarlandığını söyler.8
Sorgu: sunucudan okumanın bildirimsel hali
Sorgu (query), belgenin tanımıyla, benzersiz bir anahtara bağlı, eşzamansız bir veri kaynağına bildirimsel (declarative) bir bağımlılıktır.3 "Bildirimsel" kelimesini Bölüm 21.5'ten ve Bölüm 31.2'den tanıyorsun: nasıl yapılacağını adım adım değil, ne istediğini söylüyorsun. Burada ne istiyorsun? "Bana şu anahtarın verisini ver; verinin nasıl getirileceğini de şu fonksiyon söylesin." Bir sorgu için iki şey zorunludur:
- Benzersiz bir anahtar (
queryKey): veriyi tanıyan etiket. - Bir fonksiyon (
queryFn): ya veriyle çözülen ya da hata fırlatan birPromisedöndürür (Bölüm 25.6).
Uygulamamızın leaderboard'u şöyle yazılır. Önce sunucuyla konuşan sade fonksiyon (Bölüm 35'te ayrıntısını göreceğimiz bir HTTP isteği, Bölüm 37'de yazacağımız Go uç noktasına gider):
// api.ts
export type LeaderboardEntry = { user_id: number; name: string; points: number }
const BASE_URL = 'http://localhost:8080'
export async function fetchLeaderboard(limit: number): Promise<LeaderboardEntry[]> {
const res = await fetch(`${BASE_URL}/leaderboard?limit=${limit}`)
if (!res.ok) throw new Error(`Sunucu ${res.status} döndürdü`)
return res.json()
}
Satır satır: fetch isteği atar (Bölüm 26.5); res.ok yanıt kodu 200–299 aralığındaysa doğrudur (kodların anlamı Bölüm 35.2'de); değilse hata fırlatırız, çünkü Query bir sorgu fonksiyonundan "hata fırlatma"yı başarısızlık olarak yorumlar.3 fetch 404 ya da 500 yanıtında kendiliğinden hata fırlatmaz, bu kontrolü bizim yapmamız gerekir. res.json() yanıtın gövdesini JSON olarak ayrıştırır (Bölüm 35.3). Şimdi bileşen:
import { useQuery } from '@tanstack/react-query'
import { fetchLeaderboard } from './api'
function Leaderboard() {
const { isPending, isError, error, data, isFetching } = useQuery({
queryKey: ['leaderboard', { limit: 10 }],
queryFn: () => fetchLeaderboard(10),
})
if (isPending) return <p>Yükleniyor…</p>
if (isError) return <p>Hata: {error.message}</p>
return (
<ol>
{data.map((row) => (
<li key={row.user_id}>{row.name}: {row.points}</li>
))}
{isFetching && <li>güncelleniyor…</li>}
</ol>
)
}
(Bu kod, bölümde yer alan diğer parçalarla birlikte TypeScript derleyicisinden hatasız geçti; strict modda, TanStack Query 5.107.1 ile denendi.) Naif sürümdeki iki useState ve bir useEffect gitti. Yerinde tek çağrı var ve ondan isPending, isError, data gibi hazır durum bilgileri geliyor. Belge, bir sorgunun aynı anda yalnızca şu üç durumdan birinde olabileceğini söyler: pending (henüz veri yok), error (hata oluştu), success (başarılı, veri var). Çoğu zaman önce isPending'e, sonra isError'a bakılır; ikisi de değilse veri vardır ve TypeScript data'nın tipini buna göre daraltır.3
İki ayrı soru: status ve fetchStatus
Belgenin dikkatle ayırdığı bir ayrım var, çünkü yeni başlayanları en çok o şaşırtır. status verinin durumunu söyler: elimizde veri var mı? fetchStatus ise sorgu fonksiyonunun durumunu söyler: şu an çalışıyor mu? (fetching, paused, idle.) İkisi bağımsızdır. Veri gelmiş (success) olabilir ve aynı anda arka planda yenisi çekiliyor (fetching) olabilir. Bunun ne işe yaradığını az sonra göreceğiz; ekranda eski veriyi gösterirken küçük bir "güncelleniyor" yazısı çıkarmak tam bu ikiliye dayanır. Kodumuzdaki isFetching yazısı onun kanıtı.3
Sorgu anahtarı: verinin adresi
Anahtar ilk bakışta süs gibi görünür, oysa bütün sistemin omurgasıdır. Belgeye göre TanStack Query önbelleği sorgu anahtarlarına göre yönetir; anahtar en üst düzeyde bir dizi (array) olmalıdır ve JSON.stringify ile seri hale getirilebildiği, verisine özgü olduğu sürece istediğin kadar basit ya da karmaşık olabilir.4 Düşün: anahtar, veritabanındaki bir satırın birincil anahtarına (Bölüm 38'de göreceksin), bir dosyanın yoluna (Bölüm 16.1), bir web sayfasının URL'sine benzer: veriyi tek anlamlı biçimde adresler.
İki kural var:
- Anahtar, sorgu fonksiyonunun bağımlı olduğu her değişkeni içermelidir. Belge bunu açıkça söyler: anahtarlar verinin eşsiz tarifidir, bu yüzden sorgu fonksiyonunda kullandığın, değişebilen her değişken anahtarda da olmalı. Böylece her değişken kombinasyonu ayrı önbellek girdisi olur ve değişken değiştiğinde sorgu otomatik olarak yeniden çekilir.4 Bizim
['leaderboard', { limit: 10 }]anahtarı ile['leaderboard', { limit: 50 }]ayrı kayıtlardır; yarış durumu sorunu da bu şekilde çözülür, çünkü her yanıt kendi anahtarının altına yazılır, yanlış ekrana değil. - Nesnelerdeki anahtar sırası önemsizdir, dizideki sıra önemlidir. Belge:
['todos', {status, page}]ile['todos', {page, status}]eşittir; ama['todos', status, page]ile['todos', page, status]farklıdır.4
Hiyerarşik anahtar yazma alışkanlığının bir ödülü de var: ['leaderboard', ...] diye başlayan bütün sorgular, "leaderboard" önekiyle toplu olarak hedeflenebilir. 33.2'nin sonlarında görecek, önce önbelleğin ne yaptığına bakalım.
Önbellek: hayat döngüsü, bayat ve taze
Query bir sorgunun sonucunu anahtarın altında önbellekte saklar. Belge bu hikâyeyi adım adım anlatır; biz uygulamamıza uyarlayalım. Varsayılan ayarlarda, yani staleTime 0, gcTime 5 dakika iken:5
- Ana ekrandaki
Leaderboardbileşeni ekrana gelir. Bu anahtarla daha önce sorgu yapılmadığı için önbellekte veri yok: ekranda "Yükleniyor…" ve ağ isteği çıkar. - Cevap gelince veri,
['leaderboard', {limit: 10}]anahtarının altına yazılır.staleTime0 olduğu için veri o anda bayat (stale) sayılır. - Başka bir yerde, mesela profil ekranında aynı anahtarı kullanan ikinci bir bileşen ekrana gelir. Önbellekte veri var, hemen ekrana verilir (yükleniyor yok). Ama veri bayat olduğu için arka planda yeni bir istek de çıkar. Gelince iki bileşen birden yenilenir.
- İki bileşen de ekrandan kalkar. Artık bu veriyi kullanan kimse yok; Query bir çöp toplama (garbage collection, Bölüm 21.4) zamanlayıcısı başlatır:
gcTime(varsayılan 5 dakika) sonra bu girdi önbellekten silinecek. - 5 dakika dolmadan kullanıcı geri dönerse veri hemen gösterilir, arka planda yenisi çekilir. Dolduysa girdi silinmiştir; yeniden "Yükleniyor…".
Belge buradaki iki kelimeyi şöyle kullanır: bir sorgu taze (fresh) ise önbellekten okunur ve yeniden çekilmez; bayat (stale) ise önbellekten hemen gösterilir ve uygun bir anda arka planda yeniden çekilir.6 Bu bir ayar meselesidir: staleTime, verinin ne kadar süre taze sayılacağını söyler. Varsayılan sıfırdır, yani veri gelir gelmez bayat sayılır; bu "agresif ama makul" varsayılan, güvenli tarafta kalmak içindir. Bizim leaderboard'umuz saniyede değişmez; 30 saniye taze saymak çok sayıda gereksiz isteği keser. Onu new QueryClient({ defaultOptions: { queries: { staleTime: 30_000 } } }) ile ayarlarız (30_000 JavaScript'te okunabilirlik için binlik ayırıcıdır, otuz bin milisaniye).
Belgedeki diğer varsayılanlar, agent'ın yazdığı bir kodu okurken lazım olacak:6
- Bayat sorgular şu durumlarda arka planda otomatik yeniden çekilir: sorgunun yeni bir örneği ekrana gelince, pencere yeniden odak kazanınca, ağ bağlantısı yeniden kurulunca.
- Hata alan sorgular, kullanıcıya hata göstermeden önce, üstel geri çekilmeyle (exponential backoff: bekleme süresi her denemede uzar) 3 kez sessizce yeniden denenir. Bölüm 33.1'deki "bir saniyelik kesinti" sorunu bu yüzden kendiliğinden çözülür.
- Sonuçlar "yapısal paylaşım" ile karşılaştırılır: veri gerçekten değişmediyse aynı nesne referansı korunur, böylece React gereksiz yere yeniden çizmez.
Bunu kitapta öylece yazmak yerine denedim. Kütüphanenin QueryClient ve QueryObserver sınıflarını (React'in altındaki çekirdek) Node'da çalıştırıp iki "bileşenin" aynı anda aynı anahtara abone olmasını, sonra invalidateQueries çağırmayı ve staleTime koymayı sınadım. Sorgu fonksiyonu her çağrıldığında bir sayaç artırıyordu. Çıktı:
hemen: pending fetching
1) iki abone, istek sayısı = 1 | durum = success | stale = true
invalidate sonrası fetchStatus = fetching | data hâlâ var = true
2) invalidate sonrası istek sayısı = 2
3) staleTime=30sn, ikinci abone hemen data = true | istek sayısı = 1
Okuması: (1) iki abone aynı anahtarı istedi, ağa tek istek çıktı; Query aynı veri için istekleri birleştirir (belgede "deduping"). Veri geldiği anda bayattı (stale = true, çünkü staleTime 0). (2) Geçersiz kılınca arka planda yeni istek başladı ve bu sırada eski veri hâlâ ekrandaydı: sayaç 2 oldu. (3) staleTime 30 saniyeyken ikinci abone veriyi anında önbellekten aldı ve yeni istek çıkmadı: sayaç 1'de kaldı.
Yeniden doğrulama: stale-while-revalidate
Az önceki hikâyede tekrar eden bir kalıp var: eski kopyayı hemen göster, arkada doğrula, değiştiyse güncelle. Buna yeniden doğrulama (revalidation) denir; "bayat iken göster, arkada yeniden doğrula" kalıbı da stale-while-revalidate adıyla bilinir ve Query'nin belgesi bu mantığa açıkça atıf yapar.3 Kullanıcı için sonuç şudur: ikinci kez açtığı ekran boş gelmez, tanıdık veri anında görünür, birkaç yüz milisaniye sonra gerekirse düzelir. Bölüm 8'deki bellek hiyerarşisinde de aynı his vardı: "yakındaki kopyayı hızla ver, gerekiyorsa uzaktakini sonra getir."
Değişiklik: sunucuya yazmak
Şimdiye kadar yalnızca okuduk. Kullanıcı bir soruyu cevaplayıp puan kazandığında sunucuda bir şey değişmeli. Belge ayrımı net yapar: veriyi oluşturan, güncelleyen, silen ya da sunucuda yan etki doğuran işler için sorgu değil değişiklik (mutation) kullanılır; bunun için useMutation vardır.37 Neden ayrı? Çünkü bir okuma "güvenli ve tekrarlanabilir"dir: iki kez çalıştırsan da durum değişmez, arka planda otomatik yenilenebilir. Bir yazma ise tekrarlanırsa zarar verir: cevabı iki kez gönderirsen iki kez puan alabilirsin. Bu ayrımın HTTP tarafındaki karşılığı Bölüm 35'te (GET ile POST farkı, "güvenli" ve "idempotent" kavramları). Query, sorguları otomatik tekrar eder; değişiklikleri yalnızca sen çağırdığında çalıştırır.
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { postAnswer } from './api'
function AnswerButton() {
const qc = useQueryClient()
const mutation = useMutation({
mutationFn: (choice: number) =>
postAnswer('token-kerem', { question_id: 1, choice }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['leaderboard'] }),
})
return (
<button disabled={mutation.isPending} onClick={() => mutation.mutate(2)}>
Cevapla
</button>
)
}
Satır satır: mutationFn, sunucuya yazan fonksiyondur (burada Bölüm 37'de yazacağımız POST /answers'a istek atar; postAnswer, api.ts'te fetch'in method: 'POST' ve JSON gövdeyle çağrıldığı hâldir). mutation.mutate(2) çağrıldığında çalışır ve parametresi (2, seçilen şık) mutationFn'e geçer. Değişiklik de bir durum makinesidir: idle (boşta), pending (çalışıyor), error, success.7 Düğmeyi mutation.isPending iken devre dışı bırakmak, kullanıcının çift tıklamasıyla aynı cevabın iki kez gitmesini engeller. Ama dikkat: bu yalnızca arayüzün nezaketi. Gerçek koruma (aynı soruya ikinci kez puan verilmemesi) sunucuda olmak zorunda; çünkü arayüze güvenilmez (Bölüm 34.2, güven sınırı).
Geçersiz kılma: "bu kopya artık eski, biliyorum"
Cevap başarıyla gitti. Leaderboard'daki puan artık yanlış. Beklemek yerine (staleTime 30 saniye diyelim), kullanıcının eyleminden kesin olarak bildiğimiz bir şey var: leaderboard eskidi. Belge bu durumu şöyle anlatır: sorguların bayatlamasını beklemek, kullanıcının yaptığı bir şey yüzünden verinin eskidiğini bildiğin durumlarda yetmez; bunun için invalidateQueries vardır.9 Bu çağrı iki şey yapar: ilgili sorguları bayat olarak işaretler (bu işaret staleTime ayarını da ezer) ve o sorgu şu an ekranda bir useQuery ile kullanılıyorsa arka planda yeniden çeker.9 Biz buna sorguyu geçersiz kılmak (query invalidation) diyeceğiz. Üstteki kodda onSuccess içindeki invalidateQueries({ queryKey: ['leaderboard'] }) tam bunu yapar. Belge, bu çağrıdan bir Promise döndürmenin, değişiklik "bitti" sayılmadan önce verinin güncellenmesini sağladığını da not eder: bu sürede isPending doğru kalır.10
İşte anahtar yapısının ödülü: ['leaderboard'] anahtarı önek olarak çalışır, yani ['leaderboard', {limit: 10}] ve ['leaderboard', {limit: 50}] ikisi birden geçersiz kılınır. Belge farklı seviyeler sunar: tek başına ['todos'] ile "todos ile başlayan hepsi"; exact: true ile yalnızca tam o anahtar; ya da bir predicate fonksiyonuyla elle seçim.9 Belge bu felsefeyi şöyle belirtir: başka kütüphaneler yerel sorguları yeni veriyle elle güncellemeye çalışan "normalize edilmiş önbellekler" tutar; TanStack Query ise elle bakımın yükünden kaçınıp hedefli geçersiz kılma, arka planda yeniden çekme ve nihayetinde atomik güncellemeyi öneriyor.9 Yani "puan 70'ti, 10 ekledim, ekranda 80 yazayım" diye hesaplamak yerine "eskidi, sunucudan yenisini iste" der. Sunucunun söylediği doğrudur; bizim hesabımız yanlış çıkabilir.
İyimser güncelleme: sunucu cevap vermeden ekranı güncellemek
Geçersiz kılma doğru ama biraz yavaş: kullanıcı düğmeye basar, ağ gidip gelir, ancak sonra puan değişir. Duolingo gibi bir uygulamada geri bildirim anında hissedilmeli. İyimser güncelleme (optimistic update) şu kumardır: "istek büyük ihtimalle başarılı olacak, o hâlde başarılı olmuş gibi ekranı hemen güncelleyeyim; olmazsa geri alırım." Belge iki yol sunar:11
- Arayüz üzerinden (basit): Önbelleğe dokunmazsın. Değişiklik
pendingiken,mutation.variablesiçindeki gönderilen değeri, listeye soluk bir geçici öğe olarak çizersin. İstek başarılı olup liste yeniden çekilince geçici öğe yerini gerçeğine bırakır; hata olursa kaybolur (ya da "yeniden dene" düğmesiyle gösterilir). - Önbellek üzerinden:
onMutateiçinde önbelleği doğrudan günceller ve geri almak için gereken bilgiyi döndürürsün;onError'da geri alırsın,onSettled'da (başarı ya da hata fark etmez) geçersiz kılıp sunucudaki gerçeğe dönersin.11
Mutation'ın yaşam döngüsü için belgedeki dört geri çağırma (callback) şunlardır: onMutate (değişiklik başlamadan hemen önce), onError (hata olunca), onSuccess (başarılı olunca), onSettled (sonuç ne olursa olsun, sonda).7
İyimser güncelleme yalnızca görüntüdür. Doğru cevap verince ekranda "+10" göstermek meşrudur; ama sunucu cevabı doğru bulmayabilir, ya da başka bir kurala göre puanı farklı hesaplayabilir. Puanın ne olduğuna sunucu karar verir (neden böyle olması gerektiğini Bölüm 34.2 anlatır). İyimser güncelleme sunucu "hayır" derse geri alınır; onSettled'daki geçersiz kılma, ekranı sunucudaki gerçek rakama döndürür. Agent'ın önerdiği bir iyimser güncelleme kodunda onError ile geri alma yoksa, bu bir hatadır: ekran sonsuza kadar yalan söyleyebilir.
Telefonda: pencere yok, uygulama durumu var
Query'nin "pencere yeniden odak kazanınca yenile" davranışı, tarayıcıda window olaylarına dayanır. Telefondaki bir uygulamada tarayıcı penceresi yoktur. Belgenin React Native sayfası bu boşluğu üç yamayla doldurmayı gösterir:8
- Çevrimiçi durumu:
onlineManager'a, ağın bağlı olup olmadığını bildiren bir kaynak verilir (örneğin Expo'nunexpo-networkmodülü). Böylece ağ geri gelince sorgular yenilenir. - Uygulama odağı: React Native'in
AppStatemodülü uygulamanın "aktif" olup olmadığını bildirir;focusManager.setFocused(status === 'active')ile Query'ye iletilir. Kullanıcı başka bir uygulamadan Duolingo'ya dönünce leaderboard yeniden çekilir. - Ekran odağı: Yığın içinde başka bir ekrana gidip geri dönüldüğünde de yenilemek için özel bir kanca yazılabilir; ya da odakta olmayan ekranın sorgularını
subscribedseçeneğiyle durdurabilirsin.
Bu yamalar Bölüm 32'deki "yerel modüller" fikrinin örneğidir: ortak mantık (Query) aynı kalır, platforma özgü sinyal (uygulama öne geldi mi?) platformun kendi API'sinden beslenir.
Uygulamamızdaki sunucu durumu parçalarını sıralayalım ve her biri için Query'nin karşılığını koyalım. Bu bir plan; Kerem'in prototipinde (vanilla HTML/CSS/JS) bu verilerin şu an nasıl tutulduğunu görmedik. Muhtemelen bir dizi ya da localStorage içinde duruyor ve sunucuya hiç gitmiyor; bu da Expo sürümünde değişecek.
| Ekranda gördüğün | Durum türü | Query karşılığı |
|---|---|---|
| Leaderboard | Sunucu | useQuery, anahtar ['leaderboard', {limit}], kısa staleTime |
| Bölüm ve soru listesi | Sunucu (nadiren değişir) | useQuery, uzun staleTime |
| Toplam puan, seri, can | Sunucu | useQuery + cevaptan sonra invalidateQueries |
| Soruya verilen cevap | Yazma | useMutation → POST /answers |
| Seçili şık, açık menü, karanlık mod | İstemci | useState; Query'nin işi değil |
Son satır önemli: Query bütün durumu yönetmez. Yalnızca sunucu durumunu. Bir agent, "hepsini Query'ye koyalım" ya da "seçili şıkkı da Redux'a taşıyalım" derse, bu bölümdeki ayrımla sorabilirsin: bu veri sunucuda mı yaşıyor, cihazda mı?
33.3 TanStack Router ve Start: kısa bakış
TanStack ailesinde, Query'nin yanında bilmen gereken iki isim daha var. Bu kitapta ayrıntıya girmeyeceğiz; hangi sorunun hangi araçla çözüldüğünü bilmek yeterli.
Router: sayfalar arası geçişin tipli hali
Bölüm 31.5'te yönlendirme (routing), URL'ye göre hangi sayfanın (bileşen ağacının) gösterileceğine karar vermekti. TanStack Router (TanStack Router), React (ve Solid) için bir yönlendiricidir. Belgesine göre öne çıkan özellikleri: yüzde yüz çıkarımlı (inferred) TypeScript desteği, tipli gezinme, iç içe yönlendirme, yükleyiciler (loaders) ile veri yükleme ve bu yükleyiciler için önbellek, dosya tabanlı yönlendirme, ve URL'deki sorgu parametrelerini (?limit=10&tab=weekly) tipli, doğrulanmış bir durum olarak yönetmek.12 Belge ayrıca TanStack Router'ın "istemci tarafı veri önbellekleriyle (TanStack Query, SWR vb.) birlikte çalışmak üzere tasarlandığını" söyler: yönlendirici ekran açılmadan önce bir sorguyu başlatabilir (önyükleme, prefetching), böylece sayfa geldiğinde veri çoktan yoldadır.12
Belgenin ilginç bir iddiası: URL'deki arama parametreleri "uygulamandaki en güçlü durum yöneticisidir: küresel, serileştirilebilir, yer imine eklenebilir, paylaşılabilir." Haklı: kullanıcı "ilk 50, haftalık" leaderboard filtresini bir bağlantı olarak başkasına gönderebilir. Bu fikir web'in (Bölüm 20.2'deki URL anatomisi: sorgu kısmı) kendi mekanizmasından yararlanır.12
Start: yönlendirici üstüne tam yığın çerçeve
TanStack Start (TanStack Start), belgenin tanımıyla, TanStack Router üzerine kurulu tam yığın (full-stack) bir React çerçevesidir; sunucuda sayfanın tamamını üretme (SSR: server-side rendering), akış (streaming), sunucu fonksiyonları ve hem istemci hem sunucu için derleme sağlar.13 Yani yalnızca "tarayıcıda çalışan arayüz" değil, aynı projede çalışan bir sunucu tarafı da getirir. Belge ikisi arasındaki ayrımı basitçe koyar: Start, yönlendirme için %100 Router'a dayanır; yukarıdaki ek özelliklerin hiçbirine ihtiyacın yoksa yalnızca Router da yeterlidir.13
Bizim yığın için ne anlama geliyor?
Aşağıdaki, belgelerden çıkarılmış bizim değerlendirmemizdir, belgenin sözü değildir:
- Query merkezde. Ekran Expo ile yapılacaksa (Bölüm 32) ve arka uç Go ise (Bölüm 36–37), sunucudan veri çekmenin bütün sorunları Query'nin alanı. Router ya da Start olmadan da kullanılır.
- Mobil yönlendirme Expo Router'dır (Bölüm 32.3), TanStack Router değil. TanStack Router web için; mobil uygulamada Expo'nun dosya tabanlı yönlendiricisi kullanılır.
- Start'ın "sunucu" tarafı bizde Go olacak. Start'ın sunucu fonksiyonları ve API rotaları, arka ucu da aynı JavaScript/TypeScript projesinde yazmayı sağlar. Biz arka ucu ayrı bir Go programı olarak kuracağımız için Start'ın o bölümüne ihtiyacımız olmaz. Bir web sürümü için Start'ı değerlendirmek mümkün, ama bu bir mimari karar (Bölüm 44).
Pratik sonuç: agent kodda @tanstack/react-query import'u görürsen, bu bölümün konusudur; @tanstack/react-router ya da @tanstack/react-start görürsen bir web yönlendirme/çerçeve kararı verilmiştir ve Emre ile konuşmaya değer.
Bölüm 31'de React'in bileşen, durum ve yeniden çizme üçlüsünü öğrendin; Bölüm 32'de aynı fikri telefona taşıdın. Bu bölümde o üçlüye üçüncü bir veri kaynağı ekledik: sunucu. Bileşenin yerel durumu (useState) hâlâ istemci durumu için; sunucu durumu için artık useQuery. İkisi bileşenin içinde yan yana yaşar: useState seçili şıkkı tutar, useQuery leaderboard'u. Kural basit: asıl kopya nerede? Cevap "burada" ise useState, "sunucuda" ise Query.
Bu bölümde neler kuruldu
Arayüzde iki tür durum var: istemci durumu (sahibi sensin, anında, hep doğru) ve sunucu durumu (başkasının verisinin ödünç kopyası, eşzamansız, her an bayatlayabilir). useEffect ile elle veri çekmek yarış durumu, tekrarlanan istek, önbelleksizlik ve bayatlık yönetimsizliği gibi sorunlar bırakır. TanStack Query bu işi sorgu (useQuery: anahtar + fonksiyon) ve değişiklik (useMutation) ile yönetir. Sorgu anahtarı verinin adresidir ve fonksiyonun bağımlı olduğu her değişkeni içerir. Veri taze (staleTime içinde, önbellekten) ya da bayat (önbellekten hemen gösterilir, arka planda yeniden doğrulanır) olabilir; kullanılmayan veri gcTime sonra silinir. Yazma sonrası ilgili sorgular geçersiz kılınır; iyimser güncelleme ekranı önden günceller, ama puanı sunucu belirler. Router sayfa geçişini ve URL durumunu, Start ise Router üstüne tam yığın çerçeveyi sağlar; bizim yığında Query merkezdedir.
Terimler sözlükte, Bölüm 33 başlığı altında.
TanStack Query belgeleri, Overview (Motivation bölümü) ve hemen ardından Important Defaults. İkisi birlikte yirmi dakikayı geçmez ve bu bölümün omurgasıdır. Derinleşmek için sırayla Queries, Mutations ve Optimistic Updates sayfaları.
Takıldığın her yeri yapay zekâ öğretmenine sor. Bir öneri: ondan, uygulamamızdaki beş ekranı (giriş, bölüm listesi, soru, sonuç, leaderboard) sana versin; sen her birinin hangi verisinin istemci, hangisinin sunucu durumu olduğunu yaz, sonra hangisine hangi queryKey'in uyacağını tartışın.
Bölüm sonu soruları
Kitaba bakmadan cevapla. Kâğıt kalem serbest.
useEffect ile elle veri çekmenin çözümsüz bıraktığı sorun hangisidir?useQuery) queryKey yanındaki ikinci zorunlu parçası nedir?queryFn bir HTTP isteği atıyor. limit değişince yeni veri çekilsin istiyorsun. Ne gerekir?staleTime varsayılan değerindeyken (0) veri geldiği anda ne olur?staleTime sıfırdır; veri hemen bayattır ama gösterilmeye devam eder. Silinme gcTime ile (kullanan kalmayınca, varsayılan 5 dk) olur. Bkz. 33.2.invalidateQueries, eşleşen sorguları bayat olarak işaretler ve ekranda kullanılıyorlarsa arka planda yeniden çeker. Bkz. 33.2.onError) ve onSettled ile gerçek veriye dönülmelidir. Puanı sunucu belirler. Bkz. 33.2.AppState uygulamanın aktif olup olmadığını bildirir ve focusManager aracılığıyla Query'ye iletilir. Bkz. 33.2.invalidateQueries bu farkı nasıl kullanır? Tapu kaydı benzetmesini kullan.Örnek cevabı göster
Taze veri, belirlenen süre (staleTime) içinde güvenilir sayılan kopyadır; önbellekten okunur, yeniden çekilmez. Bayat veri ise güvenilirliği sorgulanan kopyadır; hemen gösterilir ama arka planda yeniden doğrulanır. Tapu benzetmesi: elindeki fotokopi bir süre geçerli sayılır; süre dolunca ya da önemli bir işlem öncesi güncel kaydı çıkarttırırsın. invalidateQueries, "bu kopyanın eskidiğini kesin biliyorum" demektir: kopyayı süresi dolmadan bayat işaretler (staleTime ayarını ezer) ve ekranda kullanılıyorsa hemen yenisini ister.
useState + useEffect yazdı. Bu bölümden hangi üç sorunu işaret eder ve ne önerirsin?Örnek cevabı göster
Örneğin: (1) yarış durumu: hızlı filtre değişiminde yanıtlar ters sırada gelip yanlış veriyi gösterebilir; (2) önbellek yok: ekrana her dönüşte baştan "Yükleniyor"; (3) cevap gönderildikten sonra leaderboard'ı yenilemek elle yazılmalı. Öneri: useQuery kullanmak; anahtara limit gibi değişkenleri koymak, cevabı useMutation ile göndermek ve onSuccess'te invalidateQueries({ queryKey: ['leaderboard'] }) çağırmak.
Kaynaklar
- TanStack Query belgeleri, Overview: "missing data-fetching library", sunucu durumunun dört özelliği, çözülmesi gereken sorunlar listesi,
QueryClientProviderörneği. - TanStack Query belgeleri, Quick Start:
QueryClientveQueryClientProviderkullanımı. - TanStack Query belgeleri, Queries: sorgu tanımı,
statusvefetchStatus,isPending/isError/isSuccess, "stale-while-revalidate". - TanStack Query belgeleri, Query Keys: anahtarın dizi olması, deterministik hash, "değişkeni anahtara koy" kuralı.
- TanStack Query belgeleri, Caching Examples:
gcTimevestaleTimeile önbellek hikâyesi. - TanStack Query belgeleri, Important Defaults: varsayılan bayatlık, yeniden çekme anları,
gcTime5 dakika, 3 deneme ve üstel geri çekilme, yapısal paylaşım. - TanStack Query belgeleri, Mutations:
useMutation, durumlar,onMutate/onError/onSuccess/onSettled. - TanStack Query belgeleri, React Native:
onlineManager,focusManager,AppState. - TanStack Query belgeleri, Query Invalidation:
invalidateQueries, önek eşleşmesi,exact,predicate, "targeted invalidation" felsefesi. - TanStack Query belgeleri, Invalidation from Mutations:
onSuccessiçindeinvalidateQueriesve Promise döndürmek. - TanStack Query belgeleri, Optimistic Updates: arayüz üzerinden ve önbellek üzerinden iki yol, geri alma.
- TanStack Router belgeleri, Overview: tipli gezinme, arama parametreleri, yükleyiciler ve önbellek, istemci veri önbellekleriyle çalışma.
- TanStack Start belgeleri, Overview: tam yığın React çerçevesi, Router ile ilişkisi.
- Doğrulama notu:
QueryClient/QueryObserverile Node'da yapılan deney (tek istek, geçersiz kılma,staleTime) vestrictTypeScript denetimi, bu kitabın yazımı sırasında TanStack Query 5.107.1 ile yapıldı; kendi gözlemimizdir, belge alıntısı değildir.