Blok 6 · Ders 42
Effect ve veri çekme
useEffect'in ne zaman çalıştığını anlamak, API'den veri çekmek, yükleniyor ve hata durumları, sonsuz döngü tuzağı.
Bu dersin sonunda
- Bileşenin çizimini yan etkilerden ayırıp
useEffectile çizimden sonra çalışan kod yazabilirsin - Bağımlılık dizisinin üç hâlinde (dizi yok,
[],[id]) effect'in ne zaman çalışacağını önceden söyleyebilirsin - Zamanlayıcı ve olay dinleyicisi kuran effect'lere temizlik fonksiyonu yazıp StrictMode'un çift çalıştırmasıyla eksik temizliği yakalayabilirsin
- Bir API'den veri çekip yükleniyor, hata ve veri durumlarını gösterebilir, hızlı seçimlerdeki yarış durumunu
AbortControllerile önleyebilirsin Maximum update depth exceeded, async effect uyarısı ve eksik bağımlılık uyarısını okuyup düzeltebilirsin- Veri çekme mantığını
useVeriözel hook'una taşıyabilir, effect gerekmeyen yerde effect yazmaktan kaçınabilirsin
Bu sayfada
- Yeni proje: sampiyon-rehberi
- Yan etki: çizimin dışına taşan iş
- useEffect: çizimden sonra çalışan kod
- Bağımlılık dizisi: effect ne zaman yeniden çalışır?
- Temizlik fonksiyonu: saati durdurmak
- StrictMode effect'i neden iki kez çalıştırıyor?
- Veri çekmek: yükleniyor, hata, veri
- Seçime bağlı veri: [surum, id]
- Yarış durumu ve iptal
- Sonsuz döngü: Maximum update depth exceeded
- Özel hook: useVeri
- Ne zaman effect gerekmez?
- Uygulamanın son hâli
- Görevler
- Kendini sına
- Kayıt noktası
Ders 41'deki bileşenler yalnız kullanıcıya tepki veriyordu: tıklamaya, yazmaya, form gönderimine. Gerçek bir uygulama ise ekrana gelir gelmez de iş yapar: sunucudan veri ister, sekme başlığını değiştirir, saat kurar. Bu derste React'in bu işler için verdiği aracı, useEffect'i öğreniyorsun. Dersin sonunda Data Dragon'dan şampiyon listesini çeken, tıkladığın şampiyonun ayrıntısını getiren, hızlı tıklamalara dayanıklı bir şampiyon rehberi yapmış olacaksın.
Yeni proje: sampiyon-rehberi
Ders 41'deki gibi soru sormayan sihirbazla yeni bir Vite projesi aç:
PS C:\kod> npm create vite@latest sampiyon-rehberi -- --template react-ts --no-interactive > npx> create-vite sampiyon-rehberi --template react-ts --no-interactive │◇ Scaffolding project in C:\kod\sampiyon-rehberi...│└ Done. Now run: cd sampiyon-rehberi npm install npm run dev PS C:\kod> code sampiyon-rehberiAçılan VS Code penceresinde terminali aç ve paketleri kur:
PS C:\kod\sampiyon-rehberi> npm install added 27 packages, and audited 28 packages in 8s 9 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesBu ders create-vite 9.2.1, Vite 8.3.4 ve React 19.3.0 ile denendi; sende daha yeni sürümler çıkabilir. Sonra npm run dev ile sunucuyu başlat ve terminalin yazdığı adresi (http://localhost:5173) tarayıcıda aç. Şablonu Ders 41'deki gibi temizle:
src/App.cssdosyasını vesrc/assetsklasörünü sil.src/main.tsx'e dokunma; içindeki<StrictMode>bu derste önemli bir rol oynayacak.src/index.css'in içini tamamen sil ve aşağıdaki stili koy. CSS bu dersin konusu değil, bu yüzden kopyalayabilirsin.
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #10131a; color: #e8e6e3;} body { margin: 0; padding: 24px 16px;} main { max-width: 880px; margin: 0 auto;} h1 { margin: 0 0 4px;} button,input { font: inherit; padding: 6px 12px; border-radius: 6px; color: inherit;} button { border: 1px solid #c8aa6e; background: #1e2330; cursor: pointer;} input { border: 1px solid #3c4458; background: #0b0e14; width: 100%; box-sizing: border-box; margin-bottom: 12px;} .sutunlar { display: grid; grid-template-columns: 260px 1fr; gap: 16px; margin-top: 16px;} .panel { background: #171b25; border: 1px solid #2a3142; border-radius: 10px; padding: 16px;} .panel h2 { margin-top: 0;} .liste { list-style: none; margin: 0; padding: 0; max-height: 420px; overflow-y: auto;} .liste button { width: 100%; margin-bottom: 6px; text-align: left; border-color: #2a3142;} .liste button.secili { border-color: #c8aa6e; background: #2a2416;} .soluk { color: #8a93a6;} .etiket { display: inline-block; margin-right: 6px; padding: 2px 8px; border-radius: 999px; background: #24304a; font-size: 13px;} .hata { background: #3a1d1d; color: #ffb4a8; padding: 8px 12px; border-radius: 6px;} @media (max-width: 640px) { .sutunlar { grid-template-columns: 1fr; }}Yan etki: çizimin dışına taşan iş
Ders 10'da iki tür fonksiyon görmüştün. Saf fonksiyon yalnız hesaplar ve sonucu döndürür. Yan etkisi olan fonksiyon ise dünyada iz bırakır: ekrana yazar, dışarıdaki bir şeyi değiştirir, internete istek gönderir. Ders 41'de bileşenin saf olması gerektiğini söylemiştik; React onu ne zaman ve kaç kez çağıracağına kendisi karar verir.
Ama bir arayüzün işlerinin çoğu yan etkidir. Şampiyon rehberinde şunlar lazım olacak:
- Sayfa açılınca Data Dragon'dan şampiyon listesini istemek.
- Seçilen şampiyonun adını tarayıcı sekmesinin başlığına yazmak.
- Saniye sayan bir saat kurmak.
Bunların hiçbiri JSX döndürmek değil. Peki nereye yazılacaklar? Aklına gelen ilk yer büyük ihtimalle bileşenin gövdesi. Data Dragon'daki sürüm sayısını göstermek için src/App.tsx'e şunu yazdığını düşün:
import { useState } from "react" function App() { const [surumler, setSurumler] = useState<string[]>([]) console.log("istek gönderiliyor") fetch("https://ddragon.leagueoflegends.com/api/versions.json") .then((yanit) => yanit.json()) .then((veri) => setSurumler(veri)) return ( <main> <h1>Şampiyon Rehberi</h1> <p>Data Dragon'da {surumler.length} sürüm var.</p> </main> )} export default AppEkranda Data Dragon'da 501 sürüm var. yazar (sayı sende farklı olabilir); ilk bakışta çalışıyor. Ama Console durmadan dolar:
istek gönderiliyoristek gönderiliyoristek gönderiliyoristek gönderiliyoristek gönderiliyorBiz denediğimizde 2 saniyede yüzlerce satır düştü (bir denemede 3.500'ü geçti) ve Network sekmesinde aynı sayıda versions.json isteği birikti. Bunu denersen birkaç saniye bak ve kodu hemen sil; boş yere Riot'un sunucusunu yoruyorsun. Chrome art arda gelen aynı satırları tek satırda toplar ve yanına kaç kez geldiğini yazan bir sayı koyar; o sayının hızla arttığını görürsün.
Ne oldu? Döngüyü adım adım izle:
- React
App'i çağırır. Gövdedekifetchbir istek gönderir. - Yanıt gelince
setSurumler(veri)çalışır.veriher seferinde yepyeni bir dizidir, React eskisiyle aynı kutu olmadığını görür ve yeniden çizer (Ders 41). - Yeniden çizim
App'i yeniden çağırır, gövdedekifetchyeniden çalışır. Başa dön.
Bize iki şey lazım: yan etkiyi çizimin dışına, çizimden sonraya taşıyan bir yer; ve o işin yalnız gerektiğinde yeniden çalışmasını sağlayan bir kural. React'te bu yerin adı effect.
useEffect: çizimden sonra çalışan kod
En küçük yan etkiyle başla: sekme başlığı. Tarayıcı sekmesindeki yazı document.title'da durur (Ders 22); onu değiştirmek React'in dışındaki bir şeyi değiştirmektir. src/App.tsx'i şöyle yaz:
import { useEffect, useState } from "react" function App() { const [kill, setKill] = useState(0) console.log("çizim, kill:", kill) useEffect(() => { document.title = `${kill} kill · Şampiyon Rehberi` console.log("effect, kill:", kill) }) return ( <main> <h1>Kill: {kill}</h1> <button onClick={() => setKill((onceki) => onceki + 1)}>Kill al</button> </main> )} export default AppSekmenin başlığı 0 kill · Şampiyon Rehberi olur; Kill al'a basınca 1 kill · Şampiyon Rehberi. Console'a bak:
çizim, kill: 0çizim, kill: 0effect, kill: 0effect, kill: 0çizim, kill: 1çizim, kill: 1effect, kill: 1useEffect(fonksiyon) React'e "bu fonksiyonu şimdi çalıştırma, çizimden sonra çalıştır" der. Sıraya dikkat et: önce çizim satırları, sonra effect satırı. React önce bileşeni çağırıp JSX'i hesaplar, sayfayı günceller, en son effect'leri çalıştırır. Effect'in içinde kill'in o çizimdeki değerini görürsün, tıpkı olay işleyicilerinin gördüğü anlık görüntü gibi.
çizim satırının iki kez çıkmasını Ders 41'den biliyorsun: StrictMode. Sayfa açılırken effect'in de iki kez çalışması aynı yerden geliyor ama tıklamadan sonra yalnız bir kez çalışıyor. Bunun neden böyle olduğunu birazdan, temizlik fonksiyonuyla birlikte göreceksin.
useEffect de bir hook: useState gibi yalnız bileşenin en üst düzeyinde çağrılır; if'in ya da döngünün içine yazılmaz.
Bağımlılık dizisi: effect ne zaman yeniden çalışır?
İlk effect'in her çizimden sonra çalıştı. Sekme başlığı için bu zararsız, ama bir istek her tıklamada yeniden gitmemeli. useEffect'e ikinci bir değer verebilirsin: bir dizi. React effect'i yalnız bu dizideki değerlerden biri değişince yeniden çalıştırır. Üç hâlini yan yana denemek için App.tsx'i şöyle değiştir:
import { useEffect, useState } from "react" function App() { const [kill, setKill] = useState(0) const [altin, setAltin] = useState(500) useEffect(() => { console.log("A) dizisiz: her çizimden sonra") }) useEffect(() => { console.log("B) boş dizi: yalnız ilk çizimden sonra") }, []) useEffect(() => { console.log("C) [kill]: kill değişince, kill =", kill) }, [kill]) return ( <main> <h1>Kill: {kill} · Altın: {altin}</h1> <button onClick={() => setKill((onceki) => onceki + 1)}>Kill al</button> <button onClick={() => setAltin((onceki) => onceki + 100)}>Altın topla</button> </main> )} export default AppSayfayı yenile, önce Altın topla'ya, sonra Kill al'a bas:
A) dizisiz: her çizimden sonraB) boş dizi: yalnız ilk çizimden sonraC) [kill]: kill değişince, kill = 0A) dizisiz: her çizimden sonraB) boş dizi: yalnız ilk çizimden sonraC) [kill]: kill değişince, kill = 0A) dizisiz: her çizimden sonraA) dizisiz: her çizimden sonraC) [kill]: kill değişince, kill = 1İlk altı satır sayfa açılışı; StrictMode yüzünden üç effect de iki kez çalıştı. Sonraki tek satır Altın topla: yalnız A çalıştı. Son iki satır Kill al: A ve C çalıştı, B ise bir daha hiç çalışmadı.
| Yazılış | Ne zaman çalışır? |
|---|---|
useEffect(fn) | Her çizimden sonra. |
useEffect(fn, []) | Yalnız bileşen ekrana ilk geldiğinde. |
useEffect(fn, [kill]) | İlk gelişte ve kill bir önceki çizimdekinden farklıysa. |
useEffect(fn, [surum, id]) | İlk gelişte ve ikisinden biri değişmişse. |
Kuralın bir istisnası var: setKill gibi set… fonksiyonları. React onların bileşenin ömrü boyunca hiç değişmediğini garanti eder, bu yüzden effect'te kullansan da diziye yazman gerekmez.
"Değişti mi?" sorusunu React, ısınmadaki dördüncü sorunun cevabıyla aynı yoldan sorar. Sayı ve metin değerine göre karşılaştırılır: 500 ile 500 aynıdır. Obje ve dizi kutusuna göre: içi aynı olsa bile her çizimde yeniden kurulan bir obje "değişti" sayılır. Bu ayrıntı, sonsuz döngü bölümünde başına iş açacak.
Veri çekmeyi effect'e taşımak
Şimdi bölümün başındaki bozuk kodu düzeltebilirsin. İsteği bir effect'e koy ve ona boş bir bağımlılık dizisi ver: "yalnız ekrana ilk gelişte":
import { useEffect, useState } from "react" function App() { const [surumler, setSurumler] = useState<string[]>([]) useEffect(() => { console.log("istek gönderiliyor") fetch("https://ddragon.leagueoflegends.com/api/versions.json") .then((yanit) => yanit.json()) .then((veri) => setSurumler(veri)) }, []) return ( <main> <h1>Şampiyon Rehberi</h1> <p>Data Dragon'da {surumler.length} sürüm var.</p> </main> )} export default Appistek gönderiliyoristek gönderiliyorEkranda yine Data Dragon'da 501 sürüm var. yazar (sayı sende farklı olabilir), ama artık iki istek gidiyor ve sonra her şey duruyor. setSurumler yeni bir çizim başlatır; effect'in dizisi boş olduğu için React onu bir daha çalıştırmaz. İkinci istek StrictMode'dan geliyor; ne işe yaradığını iki bölüm sonra göreceksin.
Temizlik fonksiyonu: saati durdurmak
Bazı effect'ler bir kez çalışıp biten işler değil, süren işler başlatır: her saniye çalan bir zamanlayıcı, pencereyi dinleyen bir olay dinleyicisi, yolda bir istek. Bileşen ekrandan kalkınca bu işler kendiliğinden durmaz. Önce yeni bir aracı React olmadan tanı: setInterval.
setTimeout'u Ders 15'ten biliyorsun: verilen süre dolunca fonksiyonu bir kez çağırır. setInterval aynı süreyle tekrar tekrar çağırır ve bir kimlik döndürür. O kimliği clearInterval'a verince zamanlayıcı durur. C:\kod\ogren klasöründe ders42.js dosyasını aç:
let saniye = 0 const kimlik = setInterval(() => { saniye = saniye + 1 console.log("Maç süresi:", saniye) if (saniye === 3) { clearInterval(kimlik) console.log("Saat durdu") }}, 1000) console.log("Saat kuruldu")Saat kurulduMaç süresi: 1Maç süresi: 2Maç süresi: 3Saat durduSaat kuruldu en önce yazıldı: setInterval beklemeyi Node'a bırakıp hemen bitti (Ders 15'teki olay döngüsü). clearInterval(kimlik) satırını silersen program hiç bitmez, sayı sonsuza dek artar; Ctrl+C ile durdurursun. Tarayıcıda Ctrl+C yok. Orada durdurulması unutulan zamanlayıcı sayfa açık kaldıkça çalışır.
Temizliksiz saat
sampiyon-rehberi projesine dön. src içinde MacSaati.tsx adında yeni bir dosya aç ve App.tsx'i saati gösterip gizleyen bir düğmeyle değiştir:
import { useEffect, useState } from "react" function MacSaati() { const [saniye, setSaniye] = useState(0) useEffect(() => { console.log("saat kuruldu") setInterval(() => { console.log("tik") setSaniye((onceki) => onceki + 1) }, 1000) }, []) return <p>Maç süresi: {saniye} sn</p>} export default MacSaatisetSaniye'ye güncelleme fonksiyonu verdik: zamanlayıcının içindeki kod her saniye aynı ilk çizimin anlık görüntüsünü görür, saniye orada hep 0'dır.
import { useState } from "react"import MacSaati from "./MacSaati.tsx" function App() { const [saatAcik, setSaatAcik] = useState(true) return ( <main> <h1>Maç saati</h1> <button onClick={() => setSaatAcik((onceki) => !onceki)}> {saatAcik ? "Saati gizle" : "Saati göster"} </button> {saatAcik && <MacSaati />} </main> )} export default AppSayfayı yenile ve üç saniye bekle. Ekranda Maç süresi: 6 sn yazar: saat iki kat hızlı. Sonra Saati gizle'ye bas, iki saniye bekle, Saati göster'e bas:
saat kuruldusaat kuruldutiktiktiktiktiktiktiktiktiktiksaat kuruldusaat kuruldutiktiktiktikİlk iki satır iki zamanlayıcı kurulduğunu söylüyor. Saat gizliyken de tik'ler devam etti: bileşen ekrandan kalktı, ama zamanlayıcıları yerinde kaldı. Saati yeniden gösterince iki zamanlayıcı daha kuruldu; artık her saniye dört tik düşüyor. Gizleyip göstermeye devam ettikçe sayı artar ve hiçbiri durmaz.
Temizlik fonksiyonu
Çözüm, effect'ten bir fonksiyon döndürmek. React bu fonksiyonu, effect'in kurduğu işi sökmesi gerektiğinde çağırır. MacSaati.tsx'teki effect'i şöyle değiştir:
useEffect(() => { console.log("saat kuruldu") const kimlik = setInterval(() => { console.log("tik") setSaniye((onceki) => onceki + 1) }, 1000) return () => { clearInterval(kimlik) console.log("saat durdu") } }, [])Aynı deneyi tekrarla: üç saniyede Maç süresi: 3 sn, gizleyince tik'ler kesilir, gösterince saat 0'dan ve doğru hızda başlar.
saat kuruldusaat durdusaat kuruldutiktiktiksaat durdusaat kuruldusaat durdusaat kuruldutiktikHer kurulumun bir sökümü olmalı. En sık eşleşmeler:
| Effect'te kurduğun | Temizlikte yaptığın |
|---|---|
const kimlik = setInterval(...) | clearInterval(kimlik) |
const kimlik = setTimeout(...) | clearTimeout(kimlik) |
window.addEventListener("resize", olcu) | window.removeEventListener("resize", olcu) |
fetch(adres, { signal: kumanda.signal }) | kumanda.abort() (birazdan) |
removeEventListener'a eklediğin aynı fonksiyonu vermelisin. Bu yüzden dinleyiciyi effect'in içinde adı olan bir fonksiyon olarak tanımlarsın; iki ayrı ok fonksiyonu, içleri aynı olsa da iki ayrı kutudur.
Ne zaman çıkar? Zamanlayıcı ya da olay dinleyicisi kuran bir effect'ten temizlik fonksiyonu döndürmeyince. Hata mesajı yoktur. Belirtiler: geliştirmede her şey iki kat hızlı (StrictMode iki kez kurar), bileşeni gizleyince Console'da tik'ler sürer, göstere göstere hızlanır.
Nasıl düzelir? Kurduğun şeyin kimliğini bir değişkende tut ve effect'ten onu söken bir fonksiyon döndür: return () => clearInterval(kimlik). Sonra StrictMode'un ilk açılışta kuruldu → durdu → kuruldu sırasını yazdığını Console'da gör.
StrictMode effect'i neden iki kez çalıştırıyor?
Az önceki Console'da açılışın ilk üç satırı dikkatini çekmiş olmalı: saat kuruldu, saat durdu, saat kuruldu. Bileşen henüz ekrandan kalkmadığı hâlde temizlik çalıştı. Bunu yapan src/main.tsx'teki <StrictMode>. Geliştirme sırasında React yeni ekrana gelen her bileşeni bir kez sökmüş gibi yapar ve yeniden kurar: kur → temizle → kur.
Amacı tam olarak az önce yaşadığın hatayı yakalamak. Temizliği eksik bir effect bu denemede hemen ele verir kendini: saat iki kat hızlanır, dinleyici iki kez tetiklenir. Gerçek kullanıcı da sayfadan çıkıp geri döndüğünde aynı şey olacaktı; StrictMode bunu sen yazarken gösteriyor. Temizlik doğruysa kullanıcı, effect'in bir kez mi yoksa kur → temizle → kur sırasıyla mı çalıştığını ayırt edemez.
Yayında böyle olmadığını kendin gör. Geliştirme sunucusunu Ctrl+C ile durdur ve projeyi yayın için derle:
PS C:\kod\sampiyon-rehberi> npm run build > sampiyon-rehberi@0.0.0 build> tsc -b && vite build vite v8.3.4 building client environment for production...transforming...✓ 17 modules transformed.rendering chunks...computing gzip size...dist/index.html 0.46 kB │ gzip: 0.29 kBdist/assets/index-DgbbAgQS.css 1.14 kB │ gzip: 0.55 kBdist/assets/index-DTdgLChw.js 219.98 kB │ gzip: 68.80 kB ✓ built in 171msSonra npm run preview yaz. Bu komut dist klasöründeki derlenmiş sayfayı sunar; Vite önizlemeyi varsayılan olarak 4173 portunda açar, terminaldeki adrese tıkla. Aynı deneyi orada yap:
saat kuruldutiktiktiksaat durdusaat kuruldutiktikHer kurulum bir kez. Önizlemeyi de Ctrl+C ile kapat ve npm run dev'e dön.
Veri çekmek: yükleniyor, hata, veri
Saat denemesi bitti, rehbere geçiyoruz. MacSaati.tsx'i silme, örnek olarak dursun. Veri çekme kodunu bileşenlerden ayrı bir dosyada toplayalım. src/api.ts dosyasını aç:
export type SampiyonOzeti = { id: string name: string title: string tags: string[]} export type SampiyonDetayi = { id: string name: string title: string lore: string tags: string[] passive: { name: string } spells: { name: string }[]} const KOK = "https://ddragon.leagueoflegends.com" export async function jsonGetir(adres: string, sinyal?: AbortSignal): Promise<unknown> { const yanit = await fetch(adres, { signal: sinyal }) if (!yanit.ok) { throw new Error("İstek başarısız: " + yanit.status) } return await yanit.json()} export async function listeGetir() { const surumler = (await jsonGetir(`${KOK}/api/versions.json`)) as string[] const surum = surumler[0] const adres = `${KOK}/cdn/${surum}/data/tr_TR/champion.json` const veri = (await jsonGetir(adres)) as { data: Record<string, SampiyonOzeti> } return { surum, sampiyonlar: Object.values(veri.data) }} export async function detayGetir(surum: string, id: string, sinyal?: AbortSignal) { const adres = `${KOK}/cdn/${surum}/data/tr_TR/champion/${id}.json` const veri = (await jsonGetir(adres, sinyal)) as { data: Record<string, SampiyonDetayi> } return veri.data[id]}jsonGetirDers 24'teki fonksiyonun tipli hâli.sinyal?: AbortSignalisteğe bağlı bir parametre: verilmezseundefinedolur. Ne işe yarayacağını yarış durumu bölümünde göreceksin.- Dönüş tipi
Promise<unknown>: dışarıdan gelen verinin ne olduğunu bilmiyoruz (Ders 39). Record<string, SampiyonOzeti>: anahtarları metin, değerleriSampiyonOzetiolan obje.champion.json'dakidatatam böyle:{ "Aatrox": {…}, "Ahri": {…} }.detayGetir,champion/Ahri.jsongibi tek bir şampiyonun ayrıntı dosyasını ister. İçinde hikâye (lore), pasif ve dört yetenek var; dosyanın yapısını tarayıcıda açıp kendin incele.
Şimdi App.tsx'i baştan yaz. Ders 24'teki yükleniyor, hata ve veri durumlarını bu kez üç state'le tutuyoruz:
import { useEffect, useState } from "react"import { listeGetir, type SampiyonOzeti } from "./api.ts" function App() { const [sampiyonlar, setSampiyonlar] = useState<SampiyonOzeti[]>([]) const [surum, setSurum] = useState("") const [yukleniyor, setYukleniyor] = useState(true) const [hata, setHata] = useState<string | null>(null) useEffect(() => { async function yukle() { try { const sonuc = await listeGetir() setSurum(sonuc.surum) setSampiyonlar(sonuc.sampiyonlar) setYukleniyor(false) } catch (sorun) { console.error(sorun) setHata("Şampiyonlar yüklenemedi. İnternet bağlantını denetleyip sayfayı yenile.") setYukleniyor(false) } } yukle() }, []) if (yukleniyor) { return <main><p className="soluk">Şampiyonlar yükleniyor…</p></main> } if (hata !== null) { return <main><p className="hata">{hata}</p></main> } return ( <main> <h1>Şampiyon Rehberi</h1> <p className="soluk">Yama {surum} · {sampiyonlar.length} şampiyon</p> <section className="panel"> <ul className="liste"> {sampiyonlar.map((s) => ( <li key={s.id}>{s.name} <span className="soluk">{s.title}</span></li> ))} </ul> </section> </main> )} export default AppSatır satır:
yukleniyortrueile başlıyor: bileşen ilk çizildiğinde veri henüz yok, ekranda doğrudan "yükleniyor" yazmalı.- Effect'in içinde
async function yukle()tanımlayıp hemen çağırıyoruz. Neden effect'in kendisiniasyncyapmadığımızı aşağıdaki hata kartı anlatıyor. - Hata iki yere gidiyor:
console.errorayrıntıyı geliştiriciye,setHataanlaşılır cümleyi kullanıcıya (Ders 24'teki kural).catch'teki değişkenin adısorun, çünkühataadı state'e ait. - Erken
return'ler (Ders 40) her durum için ayrı bir görünüm çiziyor.hata !== nulldenetiminden sonra TypeScripthata'nın metin olduğunu bilir: ısınmadaki daraltma.
Sayfada önce Şampiyonlar yükleniyor…, ardından Yama 16.20.1 · 173 şampiyon ve uzun bir liste görünür. Üç durumu kendin dene:
- Yükleniyor: Ders 24'teki gibi Network sekmesinde Disable cache'i işaretle, hız menüsünden 3G'yi seç ve sayfayı yenile. Yükleniyor yazısı birkaç saniye ekranda kalır. Denemeden sonra No throttling'e dön.
- Hata:
api.ts'tekiversions.json'u geçici olarakversionss.jsonyap. Data Dragon403döndürür, ekranda kırmızı mesaj çıkar. Console'da aynı hatayı iki kez görürsün; StrictMode iki istek gönderdi ve ikisi de başarısız oldu. HerErrorsatırının üstünde Chrome'un isteğin kendisi için yazdığı kırmızı bir403 (Forbidden)satırı da durur. Sonra adresi düzelt.
Error: İstek başarısız: 403 ...Error: İstek başarısız: 403 ...useEffect must not return anything besides a function, which is used for clean-up.Ne zaman çıkar? Effect'in kendisini async yazınca. Kısa göründüğü için akla ilk gelen yazılış budur:
useEffect(async () => { try { const sonuc = await listeGetir() setSurum(sonuc.surum) setSampiyonlar(sonuc.sampiyonlar) setYukleniyor(false) } catch (sorun) { console.error(sorun) setHata("Şampiyonlar yüklenemedi. İnternet bağlantını denetleyip sayfayı yenile.") setYukleniyor(false) } }, [])TypeScript kaydetmeden önce altını çizer; npm run build ile denetlersen:
PS C:\kod\sampiyon-rehberi> npm run build > sampiyon-rehberi@0.0.0 build> tsc -b && vite build src/App.tsx:10:15 - error TS2345: Argument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'. Type 'Promise<void>' is not assignable to type 'void | Destructor'. 10 useEffect(async () => { ~~~~~~~~~~~~~ Found 1 error.Vite'ın geliştirme sunucusu tipleri denetlemediği için sayfa yine açılır, ama bembeyaz kalır. Console:
useEffect must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately: useEffect(() => { async function fetchData() { // You can await here const response = await MyAPI.getData(someId); // ... } fetchData();}, [someId]); // Or [] if effect doesn't need props or state Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetchingUncaught TypeError: destroy is not a function ...An error occurred in the <App> component. Consider adding an error boundary to your tree to customize error handling behavior.Visit https://react.dev/link/error-boundaries to learn more about error boundaries.Uyarı da hata da Console'da ikişer kez görünür, çünkü StrictMode effect'i iki kez kurdu. Biz yer kazanmak için birer kez gösterdik.
Bir async fonksiyon her zaman bir Promise döndürür. React effect'in döndürdüğü şeyi temizlik fonksiyonu sanar. StrictMode'un deneme turunda o "temizliği" çağırmaya kalkar, elinde fonksiyon değil Promise vardır: destroy is not a function ("yok etme fonksiyonu bir fonksiyon değil"). TypeScript'in mesajı da aynı şeyi söylüyor: effect ya hiçbir şey (void) ya bir temizlik fonksiyonu (Destructor) döndürmeli. Mesajın ortasındaki örnek, tam olarak bizim yazdığımız kalıp. npm run lint da bu yazılışı yakalar: Effect callbacks are synchronous to prevent race conditions. ("Effect fonksiyonları yarış durumlarını önlemek için senkrondur"; yarış durumunu birazdan göreceksin).
Nasıl düzelir? Effect'i düz bir fonksiyon olarak bırak; içinde bir async fonksiyon tanımla ve hemen çağır.
Seçime bağlı veri: [surum, id]
Listeden bir şampiyona tıklayınca sağda ayrıntısı görünsün. Ayrıntıyı ayrı bir bileşen çekecek: hangi şampiyonu göstereceğini props ile alır, id her değiştiğinde yeni ayrıntıyı ister. src/SampiyonDetay.tsx:
import { useEffect, useState } from "react"import { detayGetir, type SampiyonDetayi } from "./api.ts" type SampiyonDetayProps = { surum: string id: string} function SampiyonDetay({ surum, id }: SampiyonDetayProps) { const [detay, setDetay] = useState<SampiyonDetayi | null>(null) const [yukleniyor, setYukleniyor] = useState(true) const [hata, setHata] = useState<string | null>(null) useEffect(() => { async function yukle() { setYukleniyor(true) setHata(null) try { const yeni = await detayGetir(surum, id) setDetay(yeni) setYukleniyor(false) } catch (sorun) { console.error(sorun) setHata("Ayrıntılar yüklenemedi.") setYukleniyor(false) } } yukle() }, [surum, id]) if (yukleniyor) { return <section className="panel"><p className="soluk">Ayrıntılar yükleniyor…</p></section> } if (hata !== null || detay === null) { return <section className="panel"><p className="hata">{hata}</p></section> } return ( <section className="panel"> <h2>{detay.name}</h2> <p className="soluk">{detay.title}</p> <p> {detay.tags.map((etiket) => ( <span key={etiket} className="etiket">{etiket}</span> ))} </p> <p><b>Pasif:</b> {detay.passive.name}</p> <ol> {detay.spells.map((yetenek) => ( <li key={yetenek.name}>{yetenek.name}</li> ))} </ol> <p>{detay.lore}</p> </section> )} export default SampiyonDetayİki fark var. yukle her çalışmada önce setYukleniyor(true) ve setHata(null) diyor: yeni bir şampiyon seçildiğinde eski ayrıntı ve eski hata ekranda kalmasın. Bağımlılık dizisi de [surum, id], çünkü effect ikisini de kullanıyor. Kullanıcı başka şampiyona tıklayınca id değişir ve React effect'i yeniden çalıştırır.
App.tsx'e seçimi ekle. Listedeki her satır bir düğme oluyor; tıklanan şampiyonun kimliği secili state'ine giriyor:
import { useEffect, useState } from "react"import { listeGetir, type SampiyonOzeti } from "./api.ts"import SampiyonDetay from "./SampiyonDetay.tsx" function App() { const [sampiyonlar, setSampiyonlar] = useState<SampiyonOzeti[]>([]) const [surum, setSurum] = useState("") const [yukleniyor, setYukleniyor] = useState(true) const [hata, setHata] = useState<string | null>(null) const [secili, setSecili] = useState<string | null>(null) useEffect(() => { async function yukle() { try { const sonuc = await listeGetir() setSurum(sonuc.surum) setSampiyonlar(sonuc.sampiyonlar) setYukleniyor(false) } catch (sorun) { console.error(sorun) setHata("Şampiyonlar yüklenemedi. İnternet bağlantını denetleyip sayfayı yenile.") setYukleniyor(false) } } yukle() }, []) if (yukleniyor) { return <main><p className="soluk">Şampiyonlar yükleniyor…</p></main> } if (hata !== null) { return <main><p className="hata">{hata}</p></main> } return ( <main> <h1>Şampiyon Rehberi</h1> <p className="soluk">Yama {surum} · {sampiyonlar.length} şampiyon</p> <div className="sutunlar"> <section className="panel"> <ul className="liste"> {sampiyonlar.map((s) => ( <li key={s.id}> <button className={s.id === secili ? "secili" : ""} onClick={() => setSecili(s.id)} > {s.name} </button> </li> ))} </ul> </section> {secili === null ? <section className="panel"><p className="soluk">Soldan bir şampiyon seç.</p></section> : <SampiyonDetay surum={surum} id={secili} />} </div> </main> )} export default AppAhri'ye tıkla: sağda hikâyesi, pasifi ve dört yeteneği görünür. Garen'e tıkla: ayrıntı Garen'e döner.
React Hook useEffect has missing dependencies: 'id', and 'surum'Ne zaman çıkar? Effect'in içinde kullandığın bir prop'u ya da state'i bağımlılık dizisine yazmayınca. SampiyonDetay'deki diziyi [] yaparsan ilk tıkladığın şampiyon ekranda kalır: Garen'den sonra Jinx'e tıklarsın, listede Jinx seçili görünür, ayrıntıda hâlâ Garen yazar. Hata mesajı yok; effect bir kez çalıştı ve React'e "id değişince yeniden çalıştır" demedin.
yukle() }, [])Bunu Ders 40'ta tanıdığın kod denetleyici yakalar. Vite şablonundaki Oxlint, React'in hook kurallarını da bilir:
PS C:\kod\sampiyon-rehberi> npm run lint > sampiyon-rehberi@0.0.0 lint> oxlint ! react-hooks(exhaustive-deps): React Hook useEffect has missing dependencies: 'id', and 'surum' ,-[src/SampiyonDetay.tsx:30:8] 18 | try { 19 | const yeni = await detayGetir(surum, id) : ^^^^^ ^^ 20 | setDetay(yeni) 21 | setYukleniyor(false) 22 | } catch (sorun) { 23 | console.error(sorun) 24 | setHata("Ayrıntılar yüklenemedi.") 25 | setYukleniyor(false) 26 | } 27 | } 28 | 29 | yukle() 30 | }, []) : ^^ 31 | `---- help: Either include it or remove the dependency array. Found 1 warning and 0 errors.Finished in 13ms on 6 files with 116 rules using 12 threads."useEffect'in eksik bağımlılıkları var: id ve surum. Ya ekle ya da diziyi tamamen kaldır." Mesaj kullanıldıkları yeri ^^^^^ ^^ ile, boş diziyi de ^^ ile gösteriyor. Çerçeve çizgileri sende farklı karakterlerle görünebilir. Kuralın adı exhaustive-deps, "eksiksiz bağımlılıklar" demek.
Nasıl düzelir? Effect'in kullandığı her değeri diziye yaz: [surum, id]. İkinci yol olan "diziyi kaldırmak" effect'i her çizimde çalıştırır; veri çekmede bu da yanlış. Uyarıyı susturmak için diziden değer çıkarmak hiçbir zaman çözüm değildir. VS Code'un uzantılar bölümünde Oxlint'in eklentisi de var (adı Oxc, kimliği oxc.oxc-vscode); kurarsan bu uyarılar yazarken sarı çizgiyle görünür. Kurmasan da arada bir npm run lint çalıştır.
Yarış durumu ve iptal
Şu ana kadar her şey düzgün çalışıyor gibi. Ama internetin bir huyu var: önce gönderilen isteğin yanıtı her zaman önce gelmez. Bunu kendi gözünle görmek için api.ts'e Ahri'nin isteğini yapay olarak yavaşlatan geçici satırlar ekle. KOK satırının altına bir bekle fonksiyonu (Ders 15'teki), detayGetir'in başına da bir bekleme:
function bekle(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms) })}export async function detayGetir(surum: string, id: string, sinyal?: AbortSignal) { if (id === "Ahri") { await bekle(2000) }SampiyonDetay.tsx'te de yanıtın ne zaman geldiğini görmek için setDetay(yeni)'nin üstüne bir satır ekle:
const yeni = await detayGetir(surum, id) console.log("yanıt geldi:", yeni.name) setDetay(yeni)Şimdi Ahri'ye tıkla ve hemen ardından, iki saniye dolmadan Garen'e tıkla. Önce Garen'in ayrıntısı gelir. Bir iki saniye sonra ise ekran kendiliğinden Ahri'ye döner, oysa listede seçili olan Garen:

yanıt geldi: Garenyanıt geldi: Ahriyanıt geldi: AhriAhri iki kez geldi, çünkü ayrıntı bileşeni Ahri ile ekrana geldi ve StrictMode iki istek gönderdi. Asıl sorun sıralama: Garen'in effect'i Ahri'ninkinden sonra başladı, ama Ahri'nin yanıtı sonra geldi ve setDetay ile Garen'in üstüne yazdı. Hata mesajı yok, kullanıcı yanlış şampiyonun yeteneklerini okuyor.
AbortController: elinde tuttuğun kumanda
Ders 30'da fetch'in signal ayarına bir kumanda vermiştin: AbortSignal.timeout(2000) süre dolunca kendiliğinden basılan bir kumandaydı. Bu kez düğmeye sen basmak istiyorsun, "kullanıcı başka şampiyon seçti" anında. Bunun aracı AbortController. Önce React olmadan dene; C:\kod\ogren klasöründe ders42.mjs dosyasını aç (en üstte await kullanabilmek için .mjs, Ders 15'teki gibi):
const kumanda = new AbortController()console.log("İptal edildi mi?", kumanda.signal.aborted) kumanda.abort()console.log("İptal edildi mi?", kumanda.signal.aborted) try { await fetch("https://ddragon.leagueoflegends.com/api/versions.json", { signal: kumanda.signal }) console.log("Yanıt geldi")} catch (hata) { console.log(hata.name + ": " + hata.message)}İptal edildi mi? falseİptal edildi mi? trueAbortError: This operation was abortednew AbortController()bir kumanda kurar.kumanda.signalona bağlı sinyal:fetch'e verdiğin şey bu.kumanda.abort()düğmeye basar.signal.abortedartıktrue.- Düğmesi basılmış bir sinyalle
fetchhiç yola çıkmaz; yolda olan bir istek de düğmeye basıldığı an kesilir. İkisinde defetchadıAbortErrorolan bir hata fırlatır. Node mesajıThis operation was aborted, Chrome'unkisignal is aborted without reason; ad aynı.
Effect'e kumanda takmak
Plan şu: her effect kendi kumandasını kursun, isteğe sinyalini versin, temizlik fonksiyonunda düğmeye bassın. Kullanıcı Garen'e tıklayınca React, Ahri'nin effect'ini temizler; Ahri'nin isteği kesilir. SampiyonDetay.tsx'teki effect'i şöyle değiştir:
useEffect(() => { const kumanda = new AbortController() async function yukle() { setYukleniyor(true) setHata(null) try { const yeni = await detayGetir(surum, id, kumanda.signal) console.log("yanıt geldi:", yeni.name) setDetay(yeni) setYukleniyor(false) } catch (sorun) { if (kumanda.signal.aborted) { return } console.error(sorun) setHata("Ayrıntılar yüklenemedi.") setYukleniyor(false) } } yukle() return () => { kumanda.abort() } }, [surum, id])Ahri → Garen denemesini tekrarla. Ekranda Garen kalır, Console'da yalnız yanıt geldi: Garen yazar. Network sekmesine bak: Ahri.json hiç yok, yalnız Garen.json var. Garen'e tıkladığında Ahri'nin işi daha bekle'deydi. Temizlik kumandaya bastı; bekleme bitince fetch düğmesi basılmış bir sinyalle karşılaştı ve yola hiç çıkmadı. Yolda olan bir isteğin kesildiğini, bekleme satırlarını sildikten sonra dersin sonunda Network'te göreceksin.
Üç parça birlikte çalışıyor:
- Kumanda başına bir effect.
kumandaeffect'in içinde kuruluyor; heridiçin yeni bir kumanda, yeni bir istek. - Temizlikte düğmeye bas.
iddeğişince React önce eski effect'in temizliğini çağırır, sonra yenisini kurar (Şekil 42.3). Eski istek kesilir. - İptali hata sayma. Kesilen istek
catch'e düşer. Bu kullanıcıya gösterilecek bir sorun değil;kumanda.signal.abortedtrue'ysa hiçbir şey yapmadan çıkıyoruz. Bu denetimi unutmak bir sonraki hata kartının konusu.
finally kullanmadık. İptal edilen istek de finally'ye girer ve oradaki setYukleniyor(false) yeni isteğin "yükleniyor" yazısını erkenden kapatırdı. Bu yüzden setYukleniyor(false) iki dalda ayrı ayrı yazılı.
AbortError: signal is aborted without reasonNe zaman çıkar? Effect'e kumandayı ekleyip catch'teki if (kumanda.signal.aborted) denetimini unutunca. Garen'e tek bir tıklama yeter: ekranda kırmızı bir hata çıkar, oysa Garen'in ayrıntısı geldi.

AbortError: signal is aborted without reason ...yanıt geldi: GarenStrictMode'un deneme turu ilk effect'i temizledi ve isteği kesti. Kesilen istek catch'e düştü, catch de bunu gerçek bir hata sanıp setHata çağırdı. Bu sırada ikinci effect'in isteği başarıyla geldi, ama başarılı dal hata'yı temizlemediği için ekranda hata kaldı. Yayında StrictMode olmasa da aynı şey hızlı tıklamada olur.
Nasıl düzelir? catch'in ilk işi if (kumanda.signal.aborted) { return } olsun. İptal bir hata değil, senin verdiğin bir karar.
Deneme bitti: api.ts'teki bekle fonksiyonunu ve if (id === "Ahri") bloğunu, SampiyonDetay.tsx'teki console.log("yanıt geldi:"…) satırını sil.
Sonsuz döngü: Maximum update depth exceeded
Dersin başında gövdeye yazılan fetch'in sonsuz döngüye girdiğini gördün. Effect de aynı döngüye girebilir. Kaç kez çizildiğini sayan bir bileşen düşün; src içinde geçici bir Sayac.tsx aç ve main.tsx'teki import App from './App.tsx' satırını geçici olarak import App from './Sayac.tsx' yap:
import { useEffect, useState } from "react" function Sayac() { const [cizimSayisi, setCizimSayisi] = useState(0) useEffect(() => { setCizimSayisi(cizimSayisi + 1) }) return <p>Bu bileşen {cizimSayisi} kez çizildi.</p>} export default SayacMaximum update depth exceeded.Ne zaman çıkar? Bir effect state'i değiştiriyor ve o değişiklik effect'in yeniden çalışmasına yol açıyorsa: ya effect'in bağımlılık dizisi yoktur ya da dizideki bir değer her çizimde değişir.
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."En fazla güncelleme derinliği aşıldı. Bir bileşen useEffect içinde setState çağırıyorsa ve useEffect'in ya bağımlılık dizisi yoksa ya da bağımlılıklarından biri her çizimde değişiyorsa bu olabilir." Mesaj sebebi kendisi söylüyor. Bu bir uyarı, programı durdurmaz: biz denediğimizde 2,5 saniyede Console'a bu satırdan binden fazla düştü, ekrandaki sayı 59.598'e çıktı ve bilgisayarın fanı hızlandı. Sekmeyi kapat ya da kodu hemen düzelt.
Zinciri Şekil 42.1'deki kesik okla izle: effect çalışır → setCizimSayisi yeni bir çizim ister → dizi olmadığı için effect yeniden çalışır → yine setCizimSayisi… React'in belgesi bunu "bir uzatma kablosunu kendi prizine takmak" diye anlatıyor.
Nasıl düzelir? Üç soruyu sırayla sor:
- Bu state'e gerçekten gerek var mı? Çizim sayısı gibi ekranda kullanılmayan bir sayıysa state değil; hesaplanabiliyorsa türetilmiş değer yap.
- Bağımlılık dizisi eksik mi? Effect yalnız belli bir değer değişince çalışmalıysa onu diziye yaz.
- Dizide her çizimde yeniden kurulan bir obje ya da dizi mi var? Bir sonraki karta bak.
Döngünün daha sinsi bir hâli var: effect'in içindeki iş asenkronsa React onu yakalayamaz, çünkü her setState ayrı bir anda gelir. Ekranda bir sorun görünmez, ama arka planda istekler durmadan gider. Görmek için Sayac.tsx'in içini şununla değiştir:
import { useEffect, useState } from "react" function SurumSayisi() { const [surumler, setSurumler] = useState<string[]>([]) const ayar = { adres: "https://ddragon.leagueoflegends.com/api/versions.json" } useEffect(() => { console.log("istek gönderiliyor") fetch(ayar.adres) .then((yanit) => yanit.json()) .then((veri) => setSurumler(veri)) }, [ayar]) return <p>{surumler.length} sürüm</p>} export default SurumSayisiReact hook useEffect depends on `ayar`, which changes every renderNe zaman çıkar? Bileşenin gövdesinde kurulan bir objeyi ya da diziyi bağımlılık dizisine yazınca. Yukarıdaki bileşende ekranda doğru sürüm sayısı yazar ve React hiçbir uyarı vermez, ama Console'da istek gönderiliyor durmadan artar ve Network sekmesi isteklerle dolar. Denersen birkaç saniye bakıp sekmeyi kapat.
ayar her çizimde yeniden kurulan yeni bir obje; içi aynı olsa da React onu "değişti" sayar (referans). İstek gelir, state değişir, çizim olur, ayar yine yeni, effect yine çalışır. Oxlint bunu görür:
PS C:\kod\sampiyon-rehberi> npm run lint > sampiyon-rehberi@0.0.0 lint> oxlint ! react-hooks(exhaustive-deps): React hook useEffect depends on `ayar`, which changes every render ,-[src/Sayac.tsx:5:11] 4 | const [surumler, setSurumler] = useState<string[]>([]) 5 | const ayar = { adres: "https://ddragon.leagueoflegends.com/api/versions.json" } : ^^|^ : `-- `ayar` is declared here 6 | `---- ,-[src/Sayac.tsx:12:9] 11 | .then((veri) => setSurumler(veri)) 12 | }, [ayar]) : ^^|^ : `-- it will always cause this hook to re-evaluate 13 | `---- help: Try memoizing this variable with `useRef` or `useCallback`. Found 1 warning and 0 errors.Finished in 16ms on 7 files with 116 rules using 12 threads."useEffect, her çizimde değişen ayar'a bağlı; bu, hook'un her seferinde yeniden çalışmasına yol açacak." help satırındaki useRef ve useCallback başka iki hook; bu kursta onlara gerek kalmadan çözeceğiz.
Nasıl düzelir? Objeyi bileşenin dışına taşı (hiç değişmiyorsa sabit olur) ya da effect'in içinde kur. En iyisi, diziye objenin kendisini değil, içindeki metni koymak: [adres] gibi. Metin değerine göre karşılaştırılır ve değişmedikçe effect yeniden çalışmaz.
Denemeler bitti: Sayac.tsx'i sil ve main.tsx'teki satırı yeniden ./App.tsx yap.
Özel hook: useVeri
App ve SampiyonDetay'e yan yana bak: ikisinde de üç state (veri, yükleniyor, hata), içinde async fonksiyon olan bir effect ve try/catch var. İptal ise yalnız SampiyonDetay'de; App'in listesine eklemeyi unuttuk bile. Tekrar eden kod, tekrar eden eksik demek. Bir de eşya listesi, bir de sihirdar büyüleri sayfası yapsan aynı yirmi satırı iki kez daha yazacaksın. Ders 3'teki kendini tekrar etme kuralı burada da geçerli: tekrar eden kodu bir fonksiyona taşı.
Sorun şu ki bu kod useState ve useEffect çağırıyor ve hook'lar yalnız bileşenlerin içinde çağrılabilir. React'in cevabı: adı use ile başlayan ve içinde hook çağıran sıradan bir fonksiyon yaz. Buna özel hook denir. src/useVeri.ts dosyasını aç (JSX yok, uzantı .ts):
import { useEffect, useState } from "react"import { jsonGetir } from "./api.ts" export function useVeri<T>(adres: string | null) { const [veri, setVeri] = useState<T | null>(null) const [yukleniyor, setYukleniyor] = useState(adres !== null) const [hata, setHata] = useState<string | null>(null) useEffect(() => { if (adres === null) { return } const kumanda = new AbortController() async function getir(istenen: string) { setYukleniyor(true) setHata(null) try { const yeni = await jsonGetir(istenen, kumanda.signal) setVeri(yeni as T) setYukleniyor(false) } catch (sorun) { if (kumanda.signal.aborted) { return } console.error(sorun) setHata("Veri yüklenemedi.") setYukleniyor(false) } } getir(adres) return () => { kumanda.abort() } }, [adres]) return { veri, yukleniyor, hata }}İçi SampiyonDetay'deki effect'in neredeyse aynısı. Yeni olan üç şey var:
<T>: Fonksiyonun bir tip parametresi.useState<TakimBirimi[]>([])yazarken açılı parantezleuseState'e bir tip veriyordun; şimdi aynı yeteneği kendi fonksiyonuna kazandırıyorsun.useVeri<string[]>(adres)diye çağırınca fonksiyonun içindeki herTstring[]olur veveri'nin tipistring[] | nullçıkar.adres: string | null: Adres henüz bilinmiyorsa (null) effect hiçbir şey yapmadan çıkar. Birazdan sürüm gelmeden şampiyon listesini istememek için bunu kullanacağız.getir(istenen: string): Adresi parametre olarak alıyor. Effect'in başındakiif (adres === null)denetimi effect'in içindeadres'i metne daraltır, ama bu daraltma içeride tanımlanan bir fonksiyona taşınmaz; adresi doğrudan kullanmaya kalkarsan TypeScriptArgument of type 'string | null' is not assignable to parameter of type 'string'.der. Değeri parametreyle vermek en temiz yol.
api.ts'teki listeGetir ve detayGetir artık işini hook'a bırakıyor. Dosyanın son hâli; iki fonksiyonun yerinde artık adres üreten küçük fonksiyonlar ve yanıt tipleri var:
export type SampiyonOzeti = { id: string name: string title: string tags: string[]} export type SampiyonDetayi = { id: string name: string title: string lore: string tags: string[] passive: { name: string } spells: { name: string }[]} export type ListeYaniti = { data: Record<string, SampiyonOzeti> }export type DetayYaniti = { data: Record<string, SampiyonDetayi> } const KOK = "https://ddragon.leagueoflegends.com" export const SURUM_ADRESI = `${KOK}/api/versions.json` export function listeAdresi(surum: string) { return `${KOK}/cdn/${surum}/data/tr_TR/champion.json`} export function detayAdresi(surum: string, id: string) { return `${KOK}/cdn/${surum}/data/tr_TR/champion/${id}.json`} export async function jsonGetir(adres: string, sinyal?: AbortSignal): Promise<unknown> { const yanit = await fetch(adres, { signal: sinyal }) if (!yanit.ok) { throw new Error("İstek başarısız: " + yanit.status) } return await yanit.json()}SampiyonDetay.tsx kısalıyor. Effect, üç state ve iptal, hepsi tek satırda:
import { detayAdresi, type DetayYaniti } from "./api.ts"import { useVeri } from "./useVeri.ts" type SampiyonDetayProps = { surum: string id: string} function SampiyonDetay({ surum, id }: SampiyonDetayProps) { const { veri, yukleniyor, hata } = useVeri<DetayYaniti>(detayAdresi(surum, id)) if (yukleniyor) { return <section className="panel"><p className="soluk">Ayrıntılar yükleniyor…</p></section> } if (hata !== null) { return <section className="panel"><p className="hata">{hata}</p></section> } if (veri === null) { return null } const detay = Object.values(veri.data)[0] return ( <section className="panel"> <h2>{detay.name}</h2> <p className="soluk">{detay.title}</p> <p> {detay.tags.map((etiket) => ( <span key={etiket} className="etiket">{etiket}</span> ))} </p> <p><b>Pasif:</b> {detay.passive.name}</p> <ol> {detay.spells.map((yetenek) => ( <li key={yetenek.name}>{yetenek.name}</li> ))} </ol> <p>{detay.lore}</p> </section> )} export default SampiyonDetaydetayAdresi(surum, id) her çizimde aynı metni üretir; metin değerine göre karşılaştırıldığı için hook'un effect'i yalnız id ya da surum değişince yeniden çalışır. Ayrıntı dosyasındaki data tek anahtarlı bir obje ({ "Ahri": {…} }); Object.values(veri.data)[0] o tek şampiyonu verir. Hook'u kullanan bileşen artık ne istediğini söylüyor, nasıl getirildiğini değil.
App de kısalıyor. Liste için iki hook'u zincirliyor: önce sürümler, sürüm gelince şampiyon listesi. api.ts'ten listeGetir gittiği için üç dosyayı da değiştirene kadar Vite tarayıcıda bir hata gösterebilir; bu normal.
import { useState } from "react"import { SURUM_ADRESI, listeAdresi, type ListeYaniti } from "./api.ts"import { useVeri } from "./useVeri.ts"import SampiyonDetay from "./SampiyonDetay.tsx" function App() { const [secili, setSecili] = useState<string | null>(null) const surumler = useVeri<string[]>(SURUM_ADRESI) const surum = surumler.veri === null ? null : surumler.veri[0] const liste = useVeri<ListeYaniti>(surum === null ? null : listeAdresi(surum)) const hata = surumler.hata ?? liste.hata if (hata !== null) { return <main><p className="hata">{hata} İnternet bağlantını denetleyip sayfayı yenile.</p></main> } if (surum === null || liste.veri === null) { return <main><p className="soluk">Şampiyonlar yükleniyor…</p></main> } const sampiyonlar = Object.values(liste.veri.data) return ( <main> <h1>Şampiyon Rehberi</h1> <p className="soluk">Yama {surum} · {sampiyonlar.length} şampiyon</p> <div className="sutunlar"> <section className="panel"> <ul className="liste"> {sampiyonlar.map((s) => ( <li key={s.id}> <button className={s.id === secili ? "secili" : ""} onClick={() => setSecili(s.id)} > {s.name} </button> </li> ))} </ul> </section> {secili === null ? <section className="panel"><p className="soluk">Soldan bir şampiyon seç.</p></section> : <SampiyonDetay surum={surum} id={secili} />} </div> </main> )} export default Appsurumler.hata ?? liste.hata: iki hook'tan hangisi hata verdiyse onu al (Ders 12'deki??).- Erken
return'ler bütün hook çağrılarından sonra geliyor. Hook'lar her çizimde aynı sırayla çağrılmalı (Ders 41); birreturnhook'lardan önce gelseydi bazı çizimlerde hook'lar atlanırdı. - Sürüm gelene kadar ikinci hook'un adresi
null; o arada hiçbir şey istemez.surumler.veridoluncaAppyeniden çizilir,surummetin olur, ikinci hook'un adresi değişir ve effect'i çalışır. Zinciri sen kurmadın: her hook kendi adresine tepki veriyor.
Ne zaman effect gerekmez?
Effect'i öğrenince her şeyi effect'le yazmak cazip gelir. React'in belgesinde bunun için ayrı bir sayfa var: "You Might Not Need an Effect" (Bir effect'e ihtiyacın olmayabilir). Ana fikri tek soruda: Bu kod, bileşen ekranda göründüğü için mi çalışmalı, yoksa kullanıcı bir şey yaptığı için mi? Görüntülendiği için çalışıyorsa effect; bir eylemden doğuyorsa olay işleyici; elindeki veriden hesaplanabiliyorsa ikisi de değil, çizimde hesap.
Türetilmiş değeri effect'le kopyalamak
Listeye bir arama kutusu ekleyeceğiz. Önce Ders 24'teki Türkçe harf tuzağını hatırla: "Irelia".toLocaleLowerCase("tr") sonucu ırelia olur ve irelia yazan kullanıcı Irelia'yı bulamaz. Oradaki aramaBicimi fonksiyonunu tipli hâliyle src/arama.ts dosyasına koy:
export function aramaBicimi(metin: string) { return metin.toLocaleLowerCase("tr").replaceAll("ı", "i")}Aranan metne de şampiyonun adına da aynı fonksiyonu uygulayacaksın; ikisi aynı biçime iner. Ders 41'de görünen birimleri filter ile her çizimde hesaplamıştın. Effect'i yeni öğrenen biri ise şunu yazabilir:
const [gorunenler, setGorunenler] = useState<SampiyonOzeti[]>([]) const sampiyonlar = liste.veri === null ? [] : Object.values(liste.veri.data) useEffect(() => { const aranan = aramaBicimi(arama.trim()) setGorunenler(sampiyonlar.filter((s) => aramaBicimi(s.name).includes(aranan))) }, [sampiyonlar, arama])Bu kod üç kez yanlış. Gereksiz: aynı bilgi iki yerde tutuluyor. Yavaş: her harfte önce eski listeyle bir çizim, effect'ten sonra yenisiyle ikinci bir çizim. Üstelik bozuk: sampiyonlar her çizimde Object.values ile kurulan yeni bir dizi; biz denediğimizde sayfa açılır açılmaz Maximum update depth exceeded uyarıları başladı. Oxlint iki uyarı verir:
! react(set-state-in-effect): Calling setState synchronously within an effect can trigger cascading renders help: Effects should synchronize React with external systems. Calling setState synchronously inside an effect starts another render and is usually unnecessary. Derive the value during render, initialize state directly, or update it from the event that caused the change. Use an effect only when synchronizing with an external system. ! react-hooks(exhaustive-deps): React hook useEffect depends on `sampiyonlar`, which changes every render help: Try memoizing this variable with `useRef` or `useCallback`.Birincisinin help satırı bu bölümün özeti: "Effect'ler React'i dış sistemlerle eşitlemek içindir. Effect içinde doğrudan setState çağırmak bir çizim daha başlatır ve çoğu zaman gereksizdir. Değeri çizim sırasında türet, state'e doğrudan ilk değer ver ya da değişikliğe yol açan olayın içinde güncelle." Kod çerçevelerini atlayıp her uyarının yalnız başlık ve yardım satırını gösterdik.
Doğrusu Ders 41'deki gibi: süzülmüş liste bir türetilmiş değer.
const sampiyonlar = Object.values(liste.veri.data) const aranan = aramaBicimi(arama.trim()) const gorunenler = sampiyonlar.filter((s) => aramaBicimi(s.name).includes(aranan) )Bir eyleme bağlı işi effect'e koymak
Şampiyon seçilince arama kutusu boşalsın istiyoruz. "secili değişince kutuyu temizle" diye düşünürsen eline ilk bu gelir:
useEffect(() => { setArama("") }, [secili])Oxlint yine react(set-state-in-effect) der. Sorun ince ama gerçek: bu iş "bileşen ekranda göründüğü için" değil, "kullanıcı bir şampiyona tıkladığı için" oluyor. Effect'e koyarsan seçim başka bir sebeple değiştiğinde de (ileride bir "son baktığın şampiyonu hatırla" özelliği eklediğinde, sayfa açılırken) kullanıcının yazdığı arama sessizce silinir. Ekstra bir çizim de cabası. İşi, olayı başlatan yere koy:
function sec(id: string) { setSecili(id) setArama("") }Aynı soru sunucuya kayıt gönderen işler için daha da önemli. "Satın al" düğmesine basınca gidecek bir POST isteğini effect'e koyarsan StrictMode onu geliştirmede iki kez gönderir ve satın alma iki kez olur. React'in belgesi bu örneği verir: satın almak bir çizimin değil bir tıklamanın sonucudur, olay işleyicide yapılır.
| İş | Nereye? | Neden |
|---|---|---|
| Süzülmüş liste, toplam, sayaç yazısı | Çizim sırasında hesap | Elindeki state ve props'tan çıkıyor. |
Seçince kutuyu temizlemek, satın almak (POST) | Olay işleyici | Kullanıcının bir eylemi yüzünden oluyor. |
| Sekme başlığı, zamanlayıcı, pencere dinleyicisi | Effect + temizlik | Ekrandaki duruma göre dış sistemi eşitliyor. |
Ekrana gelince ya da id değişince veri çekmek | Effect + iptal | Görünen şey o veriye ihtiyaç duyuyor. |
Uygulamanın son hâli
api.ts, useVeri.ts ve SampiyonDetay.tsx yukarıdaki son hâlleriyle kalıyor, arama.ts'i de az önce yazdın. App.tsx'in son hâlinde üç yenilik var: arama kutusu türetilmiş değerle süzüyor, seçim bir olay işleyicide kutuyu da temizliyor ve tek effect sekme başlığını eşitliyor.
import { useEffect, useState } from "react"import { SURUM_ADRESI, listeAdresi, type ListeYaniti } from "./api.ts"import { useVeri } from "./useVeri.ts"import { aramaBicimi } from "./arama.ts"import SampiyonDetay from "./SampiyonDetay.tsx" function App() { const [secili, setSecili] = useState<string | null>(null) const [arama, setArama] = useState("") const surumler = useVeri<string[]>(SURUM_ADRESI) const surum = surumler.veri === null ? null : surumler.veri[0] const liste = useVeri<ListeYaniti>(surum === null ? null : listeAdresi(surum)) const seciliAd = secili === null || liste.veri === null ? null : liste.veri.data[secili].name useEffect(() => { document.title = seciliAd === null ? "Şampiyon Rehberi" : `${seciliAd} · Şampiyon Rehberi` }, [seciliAd]) const hata = surumler.hata ?? liste.hata if (hata !== null) { return <main><p className="hata">{hata} İnternet bağlantını denetleyip sayfayı yenile.</p></main> } if (surum === null || liste.veri === null) { return <main><p className="soluk">Şampiyonlar yükleniyor…</p></main> } const sampiyonlar = Object.values(liste.veri.data) const aranan = aramaBicimi(arama.trim()) const gorunenler = sampiyonlar.filter((s) => aramaBicimi(s.name).includes(aranan) ) function sec(id: string) { setSecili(id) setArama("") } return ( <main> <h1>Şampiyon Rehberi</h1> <p className="soluk">Yama {surum} · {sampiyonlar.length} şampiyon</p> <div className="sutunlar"> <section className="panel"> <input placeholder="Şampiyon ara" value={arama} onChange={(olay) => setArama(olay.target.value)} /> {gorunenler.length === 0 && <p className="soluk">Bu adla şampiyon yok.</p>} <ul className="liste"> {gorunenler.map((s) => ( <li key={s.id}> <button className={s.id === secili ? "secili" : ""} onClick={() => sec(s.id)} > {s.name} </button> </li> ))} </ul> </section> {secili === null ? <section className="panel"><p className="soluk">Soldan bir şampiyon seç.</p></section> : <SampiyonDetay surum={surum} id={secili} />} </div> </main> )} export default App- Sekme başlığına
secili'yi değilseciliAd'ı yazıyoruz.seciliData Dragon'daki kimlik ve her zaman adla aynı değil: Wukong'un kimliğiMonkeyKing, Kai'Sa'nınkiKaisa.seciliAdbir türetilmiş değer; metin olduğu için effect yalnız seçilen ad değişince yeniden çalışır. - Sekme başlığının effect'i de erken
return'lerden önce, öteki hook'ların yanında duruyor. Onuif'lerin altına koysaydın, liste yüklenirken hiç çağrılmayacak, yüklenince çağrılacaktı; hook'ların sırası çizimden çizime değişirdi. gorunenlervesecise erkenreturn'lerden sonra: biri sıradan bir hesap, öteki sıradan bir fonksiyon; ikisi de hook değil.
Arama kutusuna ah yazıp Ahri'ye tıklayınca kutu boşalır, sekme başlığı Ahri · Şampiyon Rehberi olur:

Sınırlarda dene
- Kutuya
xyzyaz:Bu adla şampiyon yok.görünür, ayrıntı paneli olduğu gibi kalır. - Büyük harfle
AHRIya daAHRİ, küçük harfleireliaya dajarvan ivyaz: dördü de bulunur, çünküaramaBicimiiki tarafı da aynı biçime indirir. YalnıztoLocaleLowerCase("tr")kullansaydınirelia,ivernveAHRIhiçbir şey bulmazdı; biz önce öyle yazıp denedik. - Kutuya
wukongyazıp Wukong'u seç: sekme başlığındaMonkeyKingdeğilWukongyazar. - Aynı şampiyona iki kez tıkla:
iddeğişmediği için yeni istek gitmez. Network sekmesinde kendin gör. - Disable cache işaretliyken Wi-Fi'ı kapatıp sayfayı yenile: kırmızı hata mesajı. Bağlanıp yenile: liste gelir.
- Network sekmesinde sayfayı yenile ve ilk şampiyonu seç. İlk
versions.jsonile o şampiyonun ilk isteği kırmızı görünür, Status sütununda(canceled)yazar (arayüz değişmiş olabilir). Bunlar StrictMode'un deneme turunda temizliğin kestiği, yolda olan istekler. - Hızı 3G'ye düşürüp arka arkaya hızlıca beş şampiyona tıkla: ekranda en son tıkladığın kalır, ötekilerin yarıda kalan istekleri
(canceled)olur.
Görevler
Görevleri sampiyon-rehberi projesinde, her birini src içinde ayrı bir dosyada yap: Gorev1.tsx, Gorev2.tsx… Ders 41'deki gibi denemek için src/main.tsx'teki import App from './App.tsx' satırını geçici olarak import App from './Gorev1.tsx' yap; bütün görevler bitince geri al. Her görevde Console açık olsun ve StrictMode'u kaldırma: temizlik hatalarını o yakalayacak. Önce kendin dene, takılırsan ipuçlarını sırayla aç.
Q W E R tuşları
Klavyede Q, W, E ya da R'ye basınca ekranda Son yetenek: E yazsın, altında Kullanılan yetenek sayısı: 3 gibi bir sayaç olsun.
- Başka tuşlar (A, X, rakamlar) sayılmasın.
- Küçük harf de büyük harf de (Shift+R) sayılsın.
- Bir tuşa bir kez basınca sayaç 1 artsın, 2 değil.
Beklenen: sırayla q, w, x, e, Shift+R, a, r basınca Son yetenek: R ve Kullanılan yetenek sayısı: 5.
İpucu 1
Tuşları Ders 22'de keydown olayıyla dinlemiştin. Tuş basışları sayfanın bütününe gelir: window.addEventListener("keydown", fonksiyon). Dinleyiciyi bir effect'te kur; bileşen ekrana bir kez geldiğinde kurulması yeter, yani dizi [].
İpucu 2
Fonksiyonun parametresinin tipi KeyboardEvent; basılan tuşun yazısı olay.key'de. olay.key.toUpperCase() ile büyük harfe çevir ve ["Q", "W", "E", "R"].includes(tus) ile denetle.
İpucu 3
Sayaç ikişer artıyorsa StrictMode iki dinleyici kurdu ve sen birini sökmedin. Dinleyiciyi effect'in içinde adı olan bir fonksiyon olarak tanımla ve temizlikte window.removeEventListener("keydown", aynıFonksiyon) çağır.
Çözümü göster
import { useEffect, useState } from "react" function YetenekTuslari() { const [sonTus, setSonTus] = useState("-") const [sayi, setSayi] = useState(0) useEffect(() => { function tusaBasildi(olay: KeyboardEvent) { const tus = olay.key.toUpperCase() if (!["Q", "W", "E", "R"].includes(tus)) { return } setSonTus(tus) setSayi((onceki) => onceki + 1) } window.addEventListener("keydown", tusaBasildi) return () => { window.removeEventListener("keydown", tusaBasildi) } }, []) return ( <main> <h1>Son yetenek: {sonTus}</h1> <p>Kullanılan yetenek sayısı: {sayi}</p> </main> )} export default YetenekTuslariDenedik: yukarıdaki yedi tuşla Son yetenek: R ve 5; x ile a sayılmadı. Temizlik satırlarını silip aynı yedi tuşa bastığımızda sayaç 10 oldu: StrictMode'un deneme turu ikinci bir dinleyici bırakmıştı. Sayaç güncelleme fonksiyonuyla artıyor, çünkü dinleyici ilk çizimin anlık görüntüsünü görür; setSayi(sayi + 1) hep 1 yapardı.
Baron sayacı
Sayfa açılınca Baron'a 5 saniye yazsın ve her saniye bir azalsın. 0'a gelince Baron doğdu! yazsın ve sayaç dursun: eksiye inmesin, arkada çalışan zamanlayıcı kalmasın. Bir Yeniden başlat düğmesi sayacı 5'e döndürsün.
Sınırlar: sayaç ortadayken Yeniden başlat'a basınca sayaç hızlanmamalı (saniyede bir azalmaya devam etmeli). Baron doğdu! yazdıktan sonra beklesen de yazı değişmemeli.
İpucu 1
MacSaati'ndeki gibi setInterval ve temizlik. Fark şu: zamanlayıcı 0'da durmalı. Effect'in kalan'ı görmesi gerekiyor; dizisi [kalan] olsun.
İpucu 2
Effect'in başında if (kalan === 0) { return }: sıfırdaysan zamanlayıcı hiç kurulmaz. Her saniye kalan değişince React eski zamanlayıcıyı temizler, yenisini kurar.
Çözümü göster
import { useEffect, useState } from "react" function BaronSayaci() { const [kalan, setKalan] = useState(5) useEffect(() => { if (kalan === 0) { return } const kimlik = setInterval(() => { setKalan((onceki) => onceki - 1) }, 1000) return () => { clearInterval(kimlik) } }, [kalan]) return ( <main> <h1>{kalan === 0 ? "Baron doğdu!" : `Baron'a ${kalan} saniye`}</h1> <button onClick={() => setKalan(5)}>Yeniden başlat</button> </main> )} export default BaronSayaciDenedik: 5, 4, 3, 2, 1, sonra Baron doğdu!; yarım saniyede bir ölçtük, her sayı yaklaşık bir saniye kaldı. Sıfırdan sonra bir buçuk saniye daha bekledik, yazı değişmedi. Sayaç 3'teyken Yeniden başlat: bir saniye sonra Baron'a 4 saniye, bir saniye daha sonra 3; hızlanma yok, çünkü her yeni değerde eski zamanlayıcı temizleniyor. Bu yazılışta setInterval aslında hep bir kez çalışıp temizleniyor; setTimeout ve clearTimeout ile de aynı sonucu alırsın.
Eşya arama
Data Dragon'un eşya listesini çek ve bir arama kutusuyla göster. Eşyalar https://ddragon.leagueoflegends.com/cdn/<sürüm>/data/tr_TR/item.json adresinde; yapısı şampiyon listesine benzer: data içinde, eşya kimliğiyle anahtarlanmış objeler. Her eşyada name, gold.total (fiyat), gold.purchasable (dükkânda satılıyor mu) ve maps (hangi haritalarda var; Sihirdar Vadisi'nin kimliği "11") alanları var.
- Yüklenirken
Eşyalar yükleniyor…, hata olursaEşyalar yüklenemedi.yazsın. - Kutu boşken
Aramak için bir eşya adı yaz.yazsın, liste görünmesin. - Yalnız Sihirdar Vadisi'nde satın alınabilen eşyalar aransın; büyük-küçük harf ve
ı/ifarkı sonucu değiştirmesin (ioniayazan Ionia Çizmeleri'ni bulsun). - Sonuçlar ucuzdan pahalıya sıralansın, en fazla 10 tanesi
Uzun Kılıç · 350 altınbiçiminde görünsün; üstte4 eşya bulundu.gibi toplam sayı. - Uyan yoksa
Bu adla eşya yok.yazsın.
Beklenen (16.20.1 yamasında; sende sayılar ve fiyatlar farklı olabilir): kılıç ya da KILIÇ için 4 eşya bulundu. ve Uzun Kılıç 350, Hextech Silahkılıç 2500, Hextech Silahkılıç 3000, Ebedi Kılıç 3500. ionia ya da IONIA için 2 eşya: Ionia Çizmeleri ve Kızıl Ionia Çizmeleri, ikisi de 900. a için 201 eşya bulunur ama yalnız 10'u listelenir.
İpucu 1
Veriyi useVeri ile çek; dersteki App gibi iki hook zincirle: önce SURUM_ADRESI, sürüm gelince eşya adresi. Eşya için kendi tipini yaz: { name: string; gold: { total: number; purchasable: boolean }; maps: Record<string, boolean> }.
İpucu 2
Süzme ve sıralama bir türetilmiş değer; effect ya da state gerekmez. Adları dersteki gibi arama.ts'teki aramaBicimi ile karşılaştır. Aynı adlı iki eşya olabilir (iki Hextech Silahkılıç var), bu yüzden key olarak adı değil kimliği kullan: Object.entries(veri.data).map(([id, esya]) => ({ id, ...esya })).
İpucu 3
Sıralamak için Ders 41'deki tablodaki toSorted: .toSorted((a, b) => a.gold.total - b.gold.total). İlk 10 için .slice(0, 10). Toplam sayıyı slice'tan önceki diziden al.
Çözümü göster
import { useState } from "react"import { SURUM_ADRESI } from "./api.ts"import { useVeri } from "./useVeri.ts"import { aramaBicimi } from "./arama.ts" type Esya = { name: string gold: { total: number; purchasable: boolean } maps: Record<string, boolean>} type EsyaYaniti = { data: Record<string, Esya> } function esyaAdresi(surum: string) { return `https://ddragon.leagueoflegends.com/cdn/${surum}/data/tr_TR/item.json`} function EsyaArama() { const [arama, setArama] = useState("") const surumler = useVeri<string[]>(SURUM_ADRESI) const surum = surumler.veri === null ? null : surumler.veri[0] const esyalar = useVeri<EsyaYaniti>(surum === null ? null : esyaAdresi(surum)) const hata = surumler.hata ?? esyalar.hata if (hata !== null) { return <main><p className="hata">Eşyalar yüklenemedi.</p></main> } if (esyalar.veri === null) { return <main><p className="soluk">Eşyalar yükleniyor…</p></main> } const aranan = aramaBicimi(arama.trim()) const uyanlar = Object.entries(esyalar.veri.data) .map(([id, esya]) => ({ id, ...esya })) .filter((esya) => esya.gold.purchasable && esya.maps["11"]) .filter((esya) => aramaBicimi(esya.name).includes(aranan)) .toSorted((a, b) => a.gold.total - b.gold.total) return ( <main> <h1>Eşya ara</h1> <input placeholder="Eşya adı" value={arama} onChange={(olay) => setArama(olay.target.value)} /> {aranan === "" && <p className="soluk">Aramak için bir eşya adı yaz.</p>} {aranan !== "" && uyanlar.length === 0 && <p className="soluk">Bu adla eşya yok.</p>} {aranan !== "" && uyanlar.length > 0 && ( <> <p className="soluk">{uyanlar.length} eşya bulundu.</p> <ul> {uyanlar.slice(0, 10).map((esya) => ( <li key={esya.id}>{esya.name} · {esya.gold.total} altın</li> ))} </ul> </> )} </main> )} export default EsyaAramaDenedik: kılıç ve KILIÇ aynı dört eşyayı verdi; ionia ve IONIA iki çizmeyi (yalnız toLocaleLowerCase("tr") ile ikisi de boş dönüyordu); xyz → Bu adla eşya yok.; a → 201 eşya bulundu. ve yalnız 10 satır; başında ve sonunda boşluk olan hydra → üç Hydra. Sınır: esyaAdresi'ndeki tr_TR'yi tr_tr yapınca Data Dragon 403 döndü ve Eşyalar yüklenemedi. yazdı. Network'ü yavaşlatınca Eşyalar yükleniyor… göründü. Ekranda hiç effect yok; bütün effect işini useVeri yapıyor.
Hata avı: yama paneli
Bu bileşen yamanın sürümünü, Aatrox'un unvanını seçilen dilde ve sayfanın kaç saniyedir açık olduğunu gösteriyor. İçinde üç hata var. Gözle aramadan önce çalıştır, belirtiyi gör, Console'u ve npm run lint'i oku; bir hatayı düzeltip yeniden dene.
import { useEffect, useState } from "react"import { SURUM_ADRESI, jsonGetir, type ListeYaniti } from "./api.ts" function YamaPaneli() { const [surum, setSurum] = useState("") const [dil, setDil] = useState("tr_TR") const [unvan, setUnvan] = useState("") const [saniye, setSaniye] = useState(0) useEffect(async () => { const surumler = (await jsonGetir(SURUM_ADRESI)) as string[] setSurum(surumler[0]) }, []) useEffect(() => { if (surum === "") { return } const adres = `https://ddragon.leagueoflegends.com/cdn/${surum}/data/${dil}/champion.json` jsonGetir(adres).then((veri) => { setUnvan((veri as ListeYaniti).data.Aatrox.title) }) }, [surum]) useEffect(() => { setInterval(() => { setSaniye((onceki) => onceki + 1) }, 1000) }, []) return ( <main> <h1>Yama {surum}</h1> <p>Aatrox: {unvan}</p> <button onClick={() => setDil(dil === "tr_TR" ? "en_US" : "tr_TR")}> Dil: {dil} </button> <p className="soluk">Sayfa {saniye} saniyedir açık.</p> </main> )} export default YamaPaneliHepsi düzelince: Yama 16.20.1 (sende daha yeni olabilir), Aatrox: Darkin Kılıcı; Dil düğmesine basınca Aatrox: the Darkin Blade ve düğmede Dil: en_US; saniye her saniye bir artar.
İpucu 1
İlk belirti bembeyaz bir sayfa. Console'daki ilk uyarı neyi yazdığını tahmin ettiğini söylüyor: It looks like you wrote useEffect(async () => ...).
İpucu 2
Sayfa açılınca Dil'e bas: düğme en_US der ama unvan Türkçe kalır. İkinci effect dil'i kullanıyor; React'e "dil değişince yeniden çalış" dedin mi? npm run lint aynı şeyi söylüyor.
İpucu 3
Saniyeyi izle: iki iki artıyorsa StrictMode'un kurduğu ikinci zamanlayıcı hiç durdurulmuyor. Bir de şunu düşün: dil düğmesine hızlı hızlı basarsan iki istek yarışabilir mi?
Çözümü göster
import { useEffect, useState } from "react"import { SURUM_ADRESI, jsonGetir, type ListeYaniti } from "./api.ts" function YamaPaneli() { const [surum, setSurum] = useState("") const [dil, setDil] = useState("tr_TR") const [unvan, setUnvan] = useState("") const [saniye, setSaniye] = useState(0) useEffect(() => { async function surumGetir() { const surumler = (await jsonGetir(SURUM_ADRESI)) as string[] setSurum(surumler[0]) } surumGetir() }, []) useEffect(() => { if (surum === "") { return } const kumanda = new AbortController() const adres = `https://ddragon.leagueoflegends.com/cdn/${surum}/data/${dil}/champion.json` jsonGetir(adres, kumanda.signal) .then((veri) => { setUnvan((veri as ListeYaniti).data.Aatrox.title) }) .catch((sorun) => { if (!kumanda.signal.aborted) { console.error(sorun) } }) return () => { kumanda.abort() } }, [surum, dil]) useEffect(() => { const kimlik = setInterval(() => { setSaniye((onceki) => onceki + 1) }, 1000) return () => { clearInterval(kimlik) } }, []) return ( <main> <h1>Yama {surum}</h1> <p>Aatrox: {unvan}</p> <button onClick={() => setDil(dil === "tr_TR" ? "en_US" : "tr_TR")}> Dil: {dil} </button> <p className="soluk">Sayfa {saniye} saniyedir açık.</p> </main> )} export default YamaPaneliSırasıyla: useEffect(async …) effect'ten Promise döndürüyordu (useEffect must not return anything besides a function… ve Uncaught TypeError: destroy is not a function); async fonksiyonu içeri aldık. Bu düzelince sayfa açıldı, ama dil düğmesi unvanı değiştirmedi ve saniye iki iki arttı; npm run lint React Hook useEffect has a missing dependency: 'dil' dedi, dil'i diziye ekledik. Zamanlayıcıya temizlik ekledik. Dil değişince eski istek hâlâ yoldaysa yarışmasın diye ikinci effect'e de kumanda taktık; iptal edilen isteği catch'te yok sayıyoruz. Denedik: Darkin Kılıcı, Dil'den sonra the Darkin Blade, iki saniyede saniye 1'den 3'e.
Gecikmeli arama: useGecikmeli
Arama kutusuna her harf yazıldığında sunucuya istek göndermek israftır: jinx yazan biri dört istek gönderir, üçü boşa gider. Çözüm, kullanıcı yazmayı bırakana kadar beklemek. src/useGecikmeli.ts dosyasında şu özel hook'u yaz:
export function useGecikmeli<T>(deger: T, ms: number): TVerilen değeri ms milisaniye boyunca değişmeden kalırsa geri versin; o süre dolmadan değer yeniden değişirse beklemeyi baştan başlatsın. Sonra Gorev5.tsx'te bu birim listesi için bir arama kutusu yap:
const birimler = [ "Ahri", "Akali", "Annie", "Aurelion Sol", "Garen", "Gwen", "Jinx", "Kai'Sa", "Lux", "Teemo", "Vi", "Yasuo", "Yone", "Zed"]- Kutuya yazılan metin
useGecikmeli(arama, 400)'den geçsin, liste gecikmeli değere göre süzülsün. - Yazılan ile gecikmeli değer farklıyken
Aranıyor…yazsın. - Gerçek bir uygulamada gecikmeli değer değişince sunucuya istek gönderirdin. İsteğin yerine Console'a
arama yapıldı: jinxyazdır.
Beklenen: jinx'i hızlıca (harf arası 0,4 saniyeden kısa) yazınca Console'da açılıştaki boş aramalardan sonra yalnız bir satır: arama yapıldı: jinx. Harfleri yavaş yazarsan her harf için bir satır.
İpucu 1
Hook'un içinde bir state (gecikmeli, ilk değeri deger) ve bir effect. Effect setTimeout ile ms sonra setGecikmeli(deger) çağırsın.
İpucu 2
Beklemeyi baştan başlatan şey temizlik: deger değişince React önce eski effect'in temizliğini çağırır. Temizlikte clearTimeout(kimlik) yaparsan eski bekleme hiç sonuçlanmaz. Bağımlılık dizisi: [deger, ms].
İpucu 3
console.log'u ayrı bir effect'e koy, dizisi [aranan] olsun: yalnız gecikmeli değer değişince çalışır. Görünen liste ise bir türetilmiş değer.
Çözümü göster
import { useEffect, useState } from "react" export function useGecikmeli<T>(deger: T, ms: number) { const [gecikmeli, setGecikmeli] = useState(deger) useEffect(() => { const kimlik = setTimeout(() => { setGecikmeli(deger) }, ms) return () => { clearTimeout(kimlik) } }, [deger, ms]) return gecikmeli}import { useEffect, useState } from "react"import { useGecikmeli } from "./useGecikmeli.ts"import { aramaBicimi } from "./arama.ts" const birimler = [ "Ahri", "Akali", "Annie", "Aurelion Sol", "Garen", "Gwen", "Jinx", "Kai'Sa", "Lux", "Teemo", "Vi", "Yasuo", "Yone", "Zed"] function GecikmeliArama() { const [arama, setArama] = useState("") const aranan = useGecikmeli(arama, 400) useEffect(() => { console.log("arama yapıldı:", aranan) }, [aranan]) const kelime = aramaBicimi(aranan.trim()) const sonuclar = birimler.filter((birim) => aramaBicimi(birim).includes(kelime) ) return ( <main> <h1>Birim ara</h1> <input placeholder="Birim adı" value={arama} onChange={(olay) => setArama(olay.target.value)} /> {arama !== aranan && <p className="soluk">Aranıyor…</p>} <ul> {sonuclar.map((birim) => ( <li key={birim}>{birim}</li> ))} </ul> </main> )} export default GecikmeliAramaarama yapıldı:arama yapıldı:arama yapıldı: jinxDenedik: jinx'i harf arası 60 milisaniyeyle yazdık. Hemen ardından ekranda Aranıyor… ve hâlâ 14 birim vardı; yarım saniye sonra yalnız Jinx kaldı ve Console'a tek satır düştü. İlk iki boş satır açılışta StrictMode'dan. Sınır: ya'yı harf arası 0,6 saniyeyle yazınca arama yapıldı: y ve arama yapıldı: ya ayrı ayrı geldi; bekleme 400 milisaniyeden uzun olduğu için her harf kendi aramasını yaptı. ms'i 0 yaparsan bekleme kalmaz, ama yine bir sonraki ana ertelenir. Hook T tip parametresi sayesinde yalnız metinle değil, sayı ya da objeyle de çalışır.
Kendini sına
Soru 1
Bu bileşen yayındaki sitede (StrictMode yok) ilk açılışta Console'a ne yazar? Sonra düğmeye bir kez basılınca ne eklenir?
function Sayac() { const [sayi, setSayi] = useState(0) console.log("çizim", sayi) useEffect(() => { console.log("A") }, []) useEffect(() => { console.log("B", sayi) }, [sayi]) return <button onClick={() => setSayi(sayi + 1)}>{sayi}</button>}Cevabı göster
Açılışta çizim 0, A, B 0: önce çizim, sonra effect'ler yazıldıkları sırayla. Tıklamada çizim 1 ve B 1; A'nın dizisi boş olduğu için bir daha çalışmaz. StrictMode'u kapatarak denedik, birebir bu. Geliştirmede StrictMode açıkken açılış satırları ikişer olurdu.
Soru 2
Hangileri sonsuz döngüye girer?
// auseEffect(() => { setSayi(sayi + 1)}) // buseEffect(() => { setSayi(sayi + 1)}, []) // cuseEffect(() => { setListe([...liste])}, [liste]) // duseEffect(() => { document.title = ad}, [ad])Cevabı göster
a ve c. a'nın dizisi yok: her çizimden sonra çalışır ve her seferinde yeni bir çizim ister. c her seferinde yeni bir dizi kurup state'e koyar; dizi yeni olduğu için liste "değişti" sayılır ve effect yeniden çalışır. b bir kez çalışır (geliştirmede StrictMode yüzünden iki kez). d state değiştirmiyor, yalnız dış dünyayı eşitliyor; ad değişmedikçe yeniden çalışmaz.
Soru 3
Temizlik fonksiyonu hangi anlarda çalışır?
Cevabı göster
Üç anda: bağımlılıklardan biri değişince, effect yeniden kurulmadan önce (eski işi sökmek için); bileşen ekrandan kalkınca; geliştirmede StrictMode'un ekrana gelişteki deneme turunda. Bu yüzden temizlik, kurulumun tam tersini yapmalı: kurduğunu durdur, eklediğini kaldır, başlattığını iptal et.
Soru 4
Bunlardan hangileri için effect yazarsın? Diğerleri nereye gider?
a) sepetteki eşyaların toplam fiyatı · b) seçilen şampiyonun adını sekme başlığına yazmak · c) Satın al'a basınca sunucuya POST göndermek · d) pencere genişliğini dinleyip ekranda göstermek
Cevabı göster
Effect: b ve d; ikisi de ekrandaki duruma göre dış dünyayı (sekme başlığı, pencere olayları) eşitliyor; d'de temizlikte removeEventListener şart. a bir türetilmiş değer: çizim sırasında reduce ile hesaplanır. c bir kullanıcı eyleminin sonucu: olay işleyicide yapılır; effect'e koysan StrictMode geliştirmede iki kez satın alırdı.
Soru 5
Mesajı oku: sebep ne, belirti ne olur, nasıl düzelir?
! react-hooks(exhaustive-deps): React Hook useEffect has a missing dependency: 'sampiyonId' help: Either include it or remove the dependency array.Cevabı göster
Effect sampiyonId'yi kullanıyor ama bağımlılık dizisinde yok. Belirti: sampiyonId değişse de effect yeniden çalışmaz, ekranda ilk seçilen şampiyonun verisi kalır. Düzeltme: sampiyonId'yi diziye ekle. "Diziyi kaldır" seçeneği effect'i her çizimde çalıştırır; veri çekmede bu da yanlış.
Soru 6
Ahri'ye, hemen ardından Garen'e tıkladın. Ahri'nin yanıtı 2 saniye, Garen'inki yarım saniye sürüyor. 3 saniye sonra ekranda kimin ayrıntısı yazar: temizlik yokken ve kumanda.abort() varken?
Cevabı göster
Temizlik yokken Ahri: Garen'in yanıtı yarım saniyede gelip ekrana yazılır, Ahri'ninki 2. saniyede gelip üstüne yazar. Seçili olan Garen'dir ama ekranda Ahri durur; yarış durumu. kumanda.abort() varken Garen: Garen'e tıklayınca React Ahri'nin effect'ini temizler, kumanda basılır, Ahri'nin isteği AbortError ile kesilir ve catch'te yok sayılır.
Soru 7
App ve SampiyonDetay aynı adresle useVeri çağırsa, iki bileşen aynı veriyi ve tek bir isteği mi paylaşır?
Cevabı göster
Hayır. Özel hook bir fonksiyondur; her çağrı kendi useState'lerini ve kendi effect'ini kurar. İki bileşen, iki ayrı state, iki ayrı istek. Özel hook mantığı paylaştırır, veriyi değil. Aynı veriyi paylaşmak istiyorsan onu ortak ebeveynde tek kez çekip props ile verirsin.
Kayıt noktası
Commit'ten önce src/main.tsx'teki satırın yeniden import App from './App.tsx' olduğundan emin ol; tarayıcıda şampiyon rehberi açılmalı. Deneme dosyalarını (Sayac.tsx) sildiğini ve api.ts'te Ahri'yi yavaşlatan satırların kalmadığını da denetle. Son olarak bir kez npm run lint çalıştır: Found 0 warnings and 0 errors. görmelisin.
Sıradaki derste React'in üstüne kurulmuş bir çatıya, Next.js'e geçiyorsun: dosyalardan sayfa yapmak, sunucu ve istemci bileşenleri ve bu derste effect'le çektiğin veriyi sunucuda hazırlamanın yolu.