İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

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 useEffect ile ç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 AbortController ile ö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
  1. Yeni proje: sampiyon-rehberi
  2. Yan etki: çizimin dışına taşan iş
  3. useEffect: çizimden sonra çalışan kod
  4. Bağımlılık dizisi: effect ne zaman yeniden çalışır?
  5. Temizlik fonksiyonu: saati durdurmak
  6. StrictMode effect'i neden iki kez çalıştırıyor?
  7. Veri çekmek: yükleniyor, hata, veri
  8. Seçime bağlı veri: [surum, id]
  9. Yarış durumu ve iptal
  10. Sonsuz döngü: Maximum update depth exceeded
  11. Özel hook: useVeri
  12. Ne zaman effect gerekmez?
  13. Uygulamanın son hâli
  14. Görevler
  15. Kendini sına
  16. 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ç:

Terminal — PowerShell
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-rehberi

Açılan VS Code penceresinde terminali aç ve paketleri kur:

Terminal — PowerShell
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 vulnerabilities

Bu 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.css dosyasını ve src/assets klasö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.
src/index.css
: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:

src/App.tsx✗ Yanlış
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 App

Ekranda Data Dragon'da 501 sürüm var. yazar (sayı sende farklı olabilir); ilk bakışta çalışıyor. Ama Console durmadan dolar:

Console (F12) · birkaç saniye içinde
istek gönderiliyoristek gönderiliyoristek gönderiliyoristek gönderiliyoristek gönderiliyor

Biz 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:

  1. React App'i çağırır. Gövdedeki fetch bir istek gönderir.
  2. Yanıt gelince setSurumler(veri) çalışır. veri her seferinde yepyeni bir dizidir, React eskisiyle aynı kutu olmadığını görür ve yeniden çizer (Ders 41).
  3. Yeniden çizim App'i yeniden çağırır, gövdedeki fetch yeniden ç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:

src/App.tsx
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 App

Sekmenin başlığı 0 kill · Şampiyon Rehberi olur; Kill al'a basınca 1 kill · Şampiyon Rehberi. Console'a bak:

Console (F12) · sayfa açılınca, sonra bir tıklama
çizim, kill: 0çizim, kill: 0effect, kill: 0effect, kill: 0çizim, kill: 1çizim, kill: 1effect, kill: 1

useEffect(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.

1. Çizim Bileşen çağrılır, JSX hesaplanır. Saf olmalı: dışarıya dokunmaz. 2. Ekran React sayfada yalnız değişen yeri günceller. 3. Effect Dış dünyayla eşitle: document.title setInterval fetch effect state değiştirirse yeni bir çizim başlar
Şekil 42.1 Bir çizimin üç adımı. Effect en sonda çalışır; içinde state değiştirirsen yeni bir çizim başlar.

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:

src/App.tsx
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 App

Sayfayı yenile, önce Altın topla'ya, sonra Kill al'a bas:

Console (F12) · açılış, Altın topla, Kill al
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ı.

zaman ilk çizim Altın topla Kill al Altın topla kill 0 altın 500 kill 0 altın 600 kill 1 altın 600 kill 1 altın 700 dizi yok her çizimden sonra [ ] yalnız ilk çizimde [kill] kill değişince
Şekil 42.2 Aynı dört olayda üç effect. Dolu daire "çalıştı", boş daire "React atladı" demek. StrictMode'un açılıştaki ek turu şekilde yok.
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":

src/App.tsx
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 App
Console (F12) · sayfa açılınca
istek gönderiliyoristek gönderiliyor

Ekranda 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ç:

ders42.js
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")
Çıktı
Saat kurulduMaç süresi: 1Maç süresi: 2Maç süresi: 3Saat durdu

Saat 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:

src/MacSaati.tsx — sampiyon-rehberi — Visual Studio Code
src/MacSaati.tsx
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 MacSaati
Ln 1, Col 1Spaces: 4UTF-8TSX

setSaniye'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.

src/App.tsx
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 App

Sayfayı 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:

Console (F12)
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:

src/MacSaati.tsx✓ Doğru
    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.

Console (F12)
saat kuruldusaat durdusaat kuruldutiktiktiksaat durdusaat kuruldusaat durdusaat kuruldutiktik

Her kurulumun bir sökümü olmalı. En sık eşleşmeler:

Effect'te kurduğunTemizlikte 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.

Sık hataSaat iki kat hızlı ve gizleyince durmuyor

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 StrictMode çalışmaz kur temizle kur temizle Geliştirmede StrictMode açık kur temizle kur temizle kur temizle React'in denemesi ekrana geldi bağımlılık değişti ekrandan kalktı kur: effect'in gövdesi temizle: return ettiğin fonksiyon
Şekil 42.3 Effect'in yaşam döngüsü. Yayında kur ve temizle sırayla gelir; geliştirmede StrictMode ekrana gelişte bir deneme turu ekler.

Yayında böyle olmadığını kendin gör. Geliştirme sunucusunu Ctrl+C ile durdur ve projeyi yayın için derle:

Terminal — PowerShell
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 171ms

Sonra 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:

Console (F12) · npm run preview ile
saat kuruldutiktiktiksaat durdusaat kuruldutiktik

Her 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ç:

src/api.ts
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]}
  • jsonGetir Ders 24'teki fonksiyonun tipli hâli. sinyal?: AbortSignal isteğe bağlı bir parametre: verilmezse undefined olur. 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ğerleri SampiyonOzeti olan obje. champion.json'daki data tam böyle: { "Aatrox": {…}, "Ahri": {…} }.
  • detayGetir, champion/Ahri.json gibi 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:

src/App.tsx
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 App

Satır satır:

  • yukleniyor true ile 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 kendisini async yapmadığımızı aşağıdaki hata kartı anlatıyor.
  • Hata iki yere gidiyor: console.error ayrıntıyı geliştiriciye, setHata anlaşılır cümleyi kullanıcıya (Ders 24'teki kural). catch'teki değişkenin adı sorun, çünkü hata adı state'e ait.
  • Erken return'ler (Ders 40) her durum için ayrı bir görünüm çiziyor. hata !== null denetiminden sonra TypeScript hata'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'teki versions.json'u geçici olarak versionss.json yap. Data Dragon 403 dö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. Her Error satırının üstünde Chrome'un isteğin kendisi için yazdığı kırmızı bir 403 (Forbidden) satırı da durur. Sonra adresi düzelt.
Console (F12) · adres bozukken
Error: İstek başarısız: 403    ...Error: İstek başarısız: 403    ...
Sık hatauseEffect 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:

src/App.tsx✗ Yanlış
    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:

Terminal — PowerShell
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:

Console (F12)
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:

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:

src/App.tsx
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 App

Ahri'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.

Sık hataReact 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.

src/SampiyonDetay.tsx✗ Yanlış
        yukle()    }, [])

Bunu Ders 40'ta tanıdığın kod denetleyici yakalar. Vite şablonundaki Oxlint, React'in hook kurallarını da bilir:

Terminal — PowerShell
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:

src/api.ts
function bekle(ms: number) {    return new Promise((resolve) => {        setTimeout(resolve, ms)    })}
src/api.ts
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:

src/SampiyonDetay.tsx
                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:

localhost:5173
Şampiyon Rehberi sayfası. Solda liste; Garen düğmesi seçili olarak altın renkli çerçeveyle vurgulu. Sağdaki ayrıntı panelinde ise Ahri, Dokuz Kuyruklu Tilki başlığı, Mage ve Assassin etiketleri, Öz Hırsızı pasifi, dört yetenek ve Ahri'nin hikâyesi var.
Listede Garen seçili, ama ayrıntı panelinde Ahri yazıyor: geç gelen eski yanıt yenisini ezdi.
Console (F12) · Ahri'ye, hemen ardından Garen'e tıklayınca
yanıt geldi: Garenyanıt geldi: Ahriyanıt geldi: Ahri

Ahri 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.

Temizlik yok eski yanıt yenisini ezer Ahri isteği yanıt Garen isteği Ekran yükleniyor Garen Ahri Temizlikte kumanda.abort() eski istek kesilir Ahri isteği ✗ iptal Garen isteği Ekran yükleniyor Garen 0 0,3 0,8 2,3 sn
Şekil 42.4 Yarış durumu. Üstte temizlik yok: geç gelen Ahri yanıtı ekrandaki Garen'i ezer. Altta temizlikte kumanda basılır: Ahri isteği Garen'e tıklandığı an kesilir.

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):

ders42.mjs
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)}
Çıktı
İptal edildi mi? falseİptal edildi mi? trueAbortError: This operation was aborted
  • new AbortController() bir kumanda kurar. kumanda.signal ona bağlı sinyal: fetch'e verdiğin şey bu.
  • kumanda.abort() düğmeye basar. signal.aborted artık true.
  • Düğmesi basılmış bir sinyalle fetch hiç yola çıkmaz; yolda olan bir istek de düğmeye basıldığı an kesilir. İkisinde de fetch adı AbortError olan bir hata fırlatır. Node mesajı This operation was aborted, Chrome'unki signal 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:

src/SampiyonDetay.tsx
    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. kumanda effect'in içinde kuruluyor; her id için yeni bir kumanda, yeni bir istek.
  • Temizlikte düğmeye bas. id değ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.aborted true'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ı.

Sık hataAbortError: signal is aborted without reason

Ne 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.

localhost:5173
Şampiyon Rehberi sayfası. Solda listede Garen seçili. Sağdaki panelde kırmızı zeminli tek satır: Ayrıntılar yüklenemedi.
Garen seçili ve ayrıntısı yüklendi, ama panelde yalnız Ayrıntılar yüklenemedi yazıyor.
Console (F12)
AbortError: signal is aborted without reason    ...yanıt geldi: Garen

StrictMode'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:

src/Sayac.tsx✗ Yanlış
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 Sayac
Sık hataMaximum 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.

Console (F12)
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:

  1. 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.
  2. Bağımlılık dizisi eksik mi? Effect yalnız belli bir değer değişince çalışmalıysa onu diziye yaz.
  3. 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:

src/Sayac.tsx✗ Yanlış
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 SurumSayisi
Sık hataReact hook useEffect depends on `ayar`, which changes every render

Ne 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:

Terminal — PowerShell
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):

src/useVeri.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ı parantezle useState'e bir tip veriyordun; şimdi aynı yeteneği kendi fonksiyonuna kazandırıyorsun. useVeri<string[]>(adres) diye çağırınca fonksiyonun içindeki her T string[] olur ve veri'nin tipi string[] | 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şındaki if (adres === null) denetimi effect'in içinde adres'i metne daraltır, ama bu daraltma içeride tanımlanan bir fonksiyona taşınmaz; adresi doğrudan kullanmaya kalkarsan TypeScript Argument 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:

src/api.ts
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:

src/SampiyonDetay.tsx
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 SampiyonDetay

detayAdresi(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.

src/App.tsx
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 App
  • surumler.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); bir return hook'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.veri dolunca App yeniden çizilir, surum metin 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:

src/arama.ts
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:

App.tsx: süzülmüş listeyi state'te tutmak✗ Yanlış
    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:

npm run lint · iki uyarının başlık ve yardım satırları
  ! 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.

App.tsx: çizim sırasında hesaplamak✓ Doğru
    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:

App.tsx: seçim değişince effect'le temizlemek✗ Yanlış
    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:

App.tsx: olay işleyicide✓ Doğru
    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 hesapElindeki state ve props'tan çıkıyor.
Seçince kutuyu temizlemek, satın almak (POST)Olay işleyiciKullanıcının bir eylemi yüzünden oluyor.
Sekme başlığı, zamanlayıcı, pencere dinleyicisiEffect + temizlikEkrandaki duruma göre dış sistemi eşitliyor.
Ekrana gelince ya da id değişince veri çekmekEffect + iptalGö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.

src/App.tsx
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ğil seciliAd'ı yazıyoruz. secili Data Dragon'daki kimlik ve her zaman adla aynı değil: Wukong'un kimliği MonkeyKing, Kai'Sa'nınki Kaisa. seciliAd bir 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. Onu if'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.
  • gorunenler ve sec ise erken return'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:

localhost:5173
Şampiyon Rehberi, Yama 16.20.1, 173 şampiyon. Solda Şampiyon ara kutusu ve altında Aatrox, Ahri, Akali, Akshan, Alistar, Ambessa, Amumu, Anivia, Annie, Aphelios düğmeleri; Ahri seçili. Sağda Ahri, Dokuz Kuyruklu Tilki, Mage ve Assassin etiketleri, Pasif: Öz Hırsızı, numaralı dört yetenek (Aldatan Küre, Tilki Ateşi, Ayartan Öpücük, Ruh Saldırısı) ve Ahri'nin hikâyesi.
Bitmiş şampiyon rehberi: arama kutusu, şampiyon listesi ve seçilen Ahri'nin ayrıntısı.

Sınırlarda dene

  • Kutuya xyz yaz: Bu adla şampiyon yok. görünür, ayrıntı paneli olduğu gibi kalır.
  • Büyük harfle AHRI ya da AHRİ, küçük harfle irelia ya da jarvan iv yaz: dördü de bulunur, çünkü aramaBicimi iki tarafı da aynı biçime indirir. Yalnız toLocaleLowerCase("tr") kullansaydın irelia, ivern ve AHRI hiçbir şey bulmazdı; biz önce öyle yazıp denedik.
  • Kutuya wukong yazıp Wukong'u seç: sekme başlığında MonkeyKing değil Wukong yazar.
  • Aynı şampiyona iki kez tıkla: id değ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.json ile 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ç.

Görev 1

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
src/Gorev1.tsx
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 YetenekTuslari

Denedik: 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ı.

Görev 2

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
src/Gorev2.tsx
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 BaronSayaci

Denedik: 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.

Görev 3

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 olursa Eş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 ı/i farkı sonucu değiştirmesin (ionia yazan Ionia Çizmeleri'ni bulsun).
  • Sonuçlar ucuzdan pahalıya sıralansın, en fazla 10 tanesi Uzun Kılıç · 350 altın biçiminde görünsün; üstte 4 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
src/Gorev3.tsx
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 EsyaArama

Denedik: 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.

Görev 4

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.

src/Gorev4.tsx✗ Yanlış
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 YamaPaneli

Hepsi 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
src/Gorev4.tsx
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 YamaPaneli

Sı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.

Görev 5

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:

TypeScript
export function useGecikmeli<T>(deger: T, ms: number): T

Verilen 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:

TypeScript
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ı: jinx yazdı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
src/useGecikmeli.ts
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}
src/Gorev5.tsx
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 GecikmeliArama
Console (F12) · açılış, sonra jinx hızlıca
arama yapıldı:arama yapıldı:arama yapıldı: jinx

Denedik: 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?

TSX
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?

TSX
// 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?

Metin
! 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.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.