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 · Neye dayanıyor

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.

  1. 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.
  2. Kat 9Web ve tarayıcı: HTML, CSS, JavaScript (ve telefonda React Native)Sunucuya isteği fetch ile bu kattaki kod atar. Bu bölüm: gelen verinin bu katta nasıl yönetildiği.
  3. Kat 8Ağ ve internetİstek ve yanıt buradan geçer (HTTP). Bölüm 35'te ayrıntısı.
  4. 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.

Bir not: "TanStack" tek bir şey değil

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 durumuSunucu durumu
Asıl kopya nerede?Cihazda, uygulamanın belleğindeSunucuda (veritabanında)
Okumak ne kadar sürer?AnındaAğ gecikmesi kadar; eşzamansız
Başkası değiştirebilir mi?HayırEvet, haberin olmadan
Yanlış/eski olabilir mi?Hayır, tanım gereği o doğruEvet, her an
Örnek (uygulamamız)Seçili şık, açık menü, karanlık modLeaderboard, 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:

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 ile bağlantı: önbellek fikri, yeni bir sorunla

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:

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:

  1. 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.
  2. 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

  1. Ana ekrandaki Leaderboard bileşeni ekrana gelir. Bu anahtarla daha önce sorgu yapılmadığı için önbellekte veri yok: ekranda "Yükleniyor…" ve ağ isteği çıkar.
  2. Cevap gelince veri, ['leaderboard', {limit: 10}] anahtarının altına yazılır. staleTime 0 olduğu için veri o anda bayat (stale) sayılır.
  3. 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.
  4. İ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. 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

Kendi gözünle gör: aynı anahtar, tek istek

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

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üncellemenin sınırı: puanı kim hesaplar?

İ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

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ızda

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üğünDurum türüQuery karşılığı
LeaderboardSunucuuseQuery, anahtar ['leaderboard', {limit}], kısa staleTime
Bölüm ve soru listesiSunucu (nadiren değişir)useQuery, uzun staleTime
Toplam puan, seri, canSunucuuseQuery + cevaptan sonra invalidateQueries
Soruya verilen cevapYazmauseMutation → POST /answers
Seçili şık, açık menü, karanlık modİstemciuseState; 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:

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 ve 32 ile bağlantı

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.

Birincil okuma

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.

1. Aşağıdakilerden hangisi sunucu durumudur?
Sunucu durumunun asıl kopyası uzaktadır, başkaları onu sen bilmeden değiştirebilir; leaderboard tam böyledir. Menü ve tema yalnızca cihazdadır (istemci durumu). Bkz. 33.1.
2. useEffect ile elle veri çekmenin çözümsüz bıraktığı sorun hangisidir?
Elle yazılan kodda iki bileşen aynı veriyi isterse iki ayrı istek çıkar; önbellek, birleştirme ve bayatlık yönetimi yoktur. Veri yazmak ve JSON ayrıştırmak sorun değildir. Bkz. 33.1.
3. Bir sorgunun (useQuery) queryKey yanındaki ikinci zorunlu parçası nedir?
Bir sorgu en az benzersiz bir anahtar ve veriyi çözen ya da hata fırlatan bir Promise döndüren fonksiyon ister. Bkz. 33.2.
4. queryFn bir HTTP isteği atıyor. limit değişince yeni veri çekilsin istiyorsun. Ne gerekir?
Anahtar, fonksiyonun bağımlı olduğu her değişkeni içermelidir; değişken değişince anahtar değişir, yeni girdi oluşur ve gerekirse yeniden çekilir. Bkz. 33.2.
5. staleTime varsayılan değerindeyken (0) veri geldiği anda ne olur?
Varsayılan 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.
6. Kullanıcı cevap gönderdi. Leaderboard sorgularını bayat işaretleyip yeniden çeken çağrı hangisidir?
invalidateQueries, eşleşen sorguları bayat olarak işaretler ve ekranda kullanılıyorlarsa arka planda yeniden çeker. Bkz. 33.2.
7. İyimser güncellemede sunucu isteği reddederse doğru davranış hangisidir?
İyimser güncelleme bir tahmindir; sunucu reddederse geri alınmalı (onError) ve onSettled ile gerçek veriye dönülmelidir. Puanı sunucu belirler. Bkz. 33.2.
8. Expo uygulamasında tarayıcıdaki pencere odağı olayının yerine ne kullanılır?
Telefonda tarayıcı penceresi yoktur; AppState uygulamanın aktif olup olmadığını bildirir ve focusManager aracılığıyla Query'ye iletilir. Bkz. 33.2.
9. Bizim yığında TanStack parçalarının rolleri nasıl dağılır?
Sunucu durumu Query'nin alanıdır. Start tam yığın çerçevedir ama arka ucumuz Go olacak; Router yönlendirme içindir ve mobilde Expo Router kullanılır. Bkz. 33.3.
10. Kendi cümlelerinle: "taze" ile "bayat" veri arasındaki fark nedir ve 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.

11. Bir agent, leaderboard için 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

  1. TanStack Query belgeleri, Overview: "missing data-fetching library", sunucu durumunun dört özelliği, çözülmesi gereken sorunlar listesi, QueryClientProvider örneği.
  2. TanStack Query belgeleri, Quick Start: QueryClient ve QueryClientProvider kullanımı.
  3. TanStack Query belgeleri, Queries: sorgu tanımı, status ve fetchStatus, isPending/isError/isSuccess, "stale-while-revalidate".
  4. TanStack Query belgeleri, Query Keys: anahtarın dizi olması, deterministik hash, "değişkeni anahtara koy" kuralı.
  5. TanStack Query belgeleri, Caching Examples: gcTime ve staleTime ile önbellek hikâyesi.
  6. TanStack Query belgeleri, Important Defaults: varsayılan bayatlık, yeniden çekme anları, gcTime 5 dakika, 3 deneme ve üstel geri çekilme, yapısal paylaşım.
  7. TanStack Query belgeleri, Mutations: useMutation, durumlar, onMutate/onError/onSuccess/onSettled.
  8. TanStack Query belgeleri, React Native: onlineManager, focusManager, AppState.
  9. TanStack Query belgeleri, Query Invalidation: invalidateQueries, önek eşleşmesi, exact, predicate, "targeted invalidation" felsefesi.
  10. TanStack Query belgeleri, Invalidation from Mutations: onSuccess içinde invalidateQueries ve Promise döndürmek.
  11. TanStack Query belgeleri, Optimistic Updates: arayüz üzerinden ve önbellek üzerinden iki yol, geri alma.
  12. TanStack Router belgeleri, Overview: tipli gezinme, arama parametreleri, yükleyiciler ve önbellek, istemci veri önbellekleriyle çalışma.
  13. TanStack Start belgeleri, Overview: tam yığın React çerçevesi, Router ile ilişkisi.
  14. Doğrulama notu: QueryClient/QueryObserver ile Node'da yapılan deney (tek istek, geçersiz kılma, staleTime) ve strict TypeScript 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.