Blok 6 · Ders 41
State ve olaylar
useState ile değişen veriyi tutmak, tıklama ve yazma olaylarına tepki vermek, kontrollü girdiler ve state'i yukarı taşımak.
Bu dersin sonunda
- Düz bir değişkenin neden ekranı güncellemediğini açıklayıp değişen veriyi
useStateile tutabilirsin - Yeni değer eskisinden hesaplanıyorsa güncelleme fonksiyonu yazıp eski değer tuzağından kaçınabilirsin
onClick,onChangeveonSubmitile tıklamaya, yazmaya ve form gönderimine tepki veren kontrollü girdiler kurabilirsin- Dizi ve obje state'ini
...,mapvefilterile kopyalayarak ekleyebilir, değiştirebilir ve silebilirsin - İki bileşenin paylaştığı state'i ortak ebeveyne taşıyıp toplam maliyet gibi değerleri state yapmadan türetebilirsin
Too many re-renders, güncellenmeyen ekran veonChange'sizvalueuyarısını tanıyıp düzeltebilirsin
Bu sayfada
- Yeni proje: tft-kurucu
- Düz değişken neden yetmez?
- useState: React'in hatırladığı değer
- Güncelleme fonksiyonu ve eski değer tuzağı
- Olaylara tepki vermek
- Dizi state'i: takıma birim eklemek
- Kontrollü girdi: arama kutusu
- State'i ortak ebeveyne taşımak
- Türetilmiş değerler
- Uygulamanın tamamı
- Sık hatalar
- Görevler
- Kendini sına
- Kayıt noktası
Ders 40'taki bileşenler veriyi props ile alıp çiziyordu, ama sayfa açıldıktan sonra hiçbir şey değişmiyordu. Bu derste arayüze hafıza ve tepki ekliyorsun: tıklamaya, yazmaya ve form gönderimine cevap veren bileşenler. Dersin sonunda bir TFT takım kurucu yapmış olacaksın: dükkândan birim ekleyecek, yıldızını artıracak, çıkaracak; toplam maliyeti ve uyarıları anında göreceksin.
Yeni proje: tft-kurucu
Bu derste yeni bir Vite projesi açıyorsun. Ders 40'ta Vite sana soruları tek tek sormuştu; bu kez sorusuz kuruyoruz. --template react-ts "React + TypeScript şablonunu kullan", --no-interactive "soru sorma, gerisi varsayılan olsun" demek. Varsayılanlar Ders 40'ta seçtiklerinle aynı: Oxlint, paketleri kurma. Terminalde:
PS C:\kod> npm create vite@latest tft-kurucu -- --template react-ts --no-interactive > npx> create-vite tft-kurucu --template react-ts --no-interactive │◇ Scaffolding project in C:\kod\tft-kurucu...│└ Done. Now run: cd tft-kurucu npm install npm run dev PS C:\kod> code tft-kurucunpm create-vite paketini indirmek için izin isterse Ders 40'taki gibi y yaz. code tft-kurucu projeyi yeni bir VS Code penceresinde açar. Orada terminali aç, paketleri kur ve geliştirme sunucusunu başlat:
PS C:\kod\tft-kurucu> npm install added 27 packages, and audited 28 packages in 5s 9 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesPS C:\kod\tft-kurucu> npm run dev > tft-kurucu@0.0.0 dev> vite VITE v8.3.4 ready in 810 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show helpTarayıcıda http://localhost:5173 adresini aç. Şablonun Count is 0 sayacı görünecek; birkaç kez tıkla, sayı artar. Silmeden önce src/App.tsx'i aç ve şu iki satırı bul:
const [count, setCount] = useState(0) onClick={() => setCount((count) => count + 1)}Sayacın bütün işi bu iki satırda. Şimdilik yabancı görünüyorlar; dersin ortasına geldiğinde ikisini de parça parça okuyabileceksin. Şimdi şablonu temizle:
src/App.tsx'in içini tamamen sil; ilk kodunu birazdan oraya yazacaksın. O zamana kadar sayfa boş kalır, Console'daki kırmızı satırı dert etme.src/App.cssdosyasını vesrc/assetsklasörünü sil; artık kullanılmıyorlar.src/main.tsx'e dokunma.src/index.css'in içini tamamen sil ve aşağıdaki stili koy. CSS bu dersin konusu değil; Ders 20–21'de öğrendiklerinle okuyabilirsin. Bu yüzden bu bloğa kopyala düğmesi koyduk.
: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;} button,input { font: inherit; padding: 6px 12px; border-radius: 6px; color: inherit;} button { border: 1px solid #c8aa6e; background: #1e2330; cursor: pointer;} button:disabled { opacity: 0.4; cursor: default;} input { border: 1px solid #3c4458; background: #0b0e14;} input[type="number"] { width: 5em;} form input { width: 100%; box-sizing: border-box;} .ayarlar,.ayarlar label { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-bottom: 16px;} .ayarlar label { gap: 8px; margin: 0;} .sutunlar { display: grid; grid-template-columns: 1fr 1fr; gap: 16px;} .panel { background: #171b25; border: 1px solid #2a3142; border-radius: 10px; padding: 16px;} .panel h2 { margin-top: 0;} .liste { list-style: none; padding: 0;} .liste li { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #2a3142;} .liste li span:first-child { flex: 1;} .soluk { color: #8a93a6;} .uyari { background: #3a1d1d; color: #ffb4a8; padding: 8px 12px; border-radius: 6px;} @media (max-width: 640px) { .sutunlar { grid-template-columns: 1fr; }}Düz değişken neden yetmez?
Ders 22'de altın sayacı yapmıştın: bir değişken, bir düğme, tıklayınca değişkeni artırıp textContent ile ekrana yazan bir dinleyici. React'te ekranı elle değiştirmiyoruz; JSX'e {altin} yazıyoruz ve React onu çiziyor. O hâlde aklına ilk gelen yol şu olur. src/App.tsx'e yaz:
function App() { let altin = 0 function altinEkle() { altin = altin + 1 console.log("altin:", altin) } return ( <main> <h1>Altın: {altin}</h1> <button onClick={altinEkle}>Altın topla</button> </main> )} export default ApponClick={altinEkle} düğmeye "tıklanınca altinEkle'yi çalıştır" der; Ders 22'deki addEventListener("click", altinEkle)'in React'teki yazılışı. Birazdan ayrıntısına gireceğiz. Kaydet, tarayıcıda F12 ile Console'u aç ve düğmeye üç kez bas:

altin: 1altin: 2altin: 3Değişken gerçekten 3 oldu, Console bunu söylüyor. Ama ekranda hâlâ Altın: 0 var. İki ayrı sebep var:
- React'in haberi olmadı. React bir bileşeni yalnız bir sebep olunca yeniden çizer. Bir değişkene yeni değer atamak React için sebep değildir; React senin değişkenlerini izlemez.
- Haberi olsaydı bile işe yaramazdı. Bileşen bir fonksiyondur ve React her çizimde onu baştan çağırır. Her çağrıda
let altin = 0satırı yeniden çalışır, kutu yeniden 0'la açılır. Ders 3'teki kapsam kuralı: fonksiyonun içindeki değişkenler her çağrıda yeniden doğar, çağrı bitince kaybolur.
Bize iki şey gerekiyor: değeri çizimler arasında saklayan ve değişince React'e haber veren bir yer. React'in buna verdiği ad state. Ders 23'te "uygulamanın o anki verisi" diye tanımladığın durumun ta kendisi; fark, bu kez onu React'in tutması.
useState: React'in hatırladığı değer
src/App.tsx'i şöyle değiştir:
import { useState } from "react" function App() { const [altin, setAltin] = useState(0) function altinEkle() { setAltin(altin + 1) } return ( <main> <h1>Altın: {altin}</h1> <button onClick={altinEkle}>Altın topla</button> </main> )} export default App
Üç satır değişti:
import { useState } from "react": React paketindenuseStatefonksiyonunu al (Ders 14'teki adlı içe aktarma).const [altin, setAltin] = useState(0): React'ten bir hafıza kutusu iste, ilk değeri0olsun.useStateiki elemanlı bir dizi döndürür: kutunun şu anki değeri ve onu değiştiren fonksiyon. Köşeli parantez Ders 12'deki dizi parçalama; iki elemanı iki ada açıyor. Adları sen seçersin, gelenekxvesetX.setAltin(altin + 1): "altını şu değer yap ve yeniden çiz" demek.
setAltin(1) çağrıldığında React yeni değeri kendi hafızasına yazar ve App'i yeniden çağırır. Bu ikinci çağrıda useState(0) satırı yine çalışır ama artık 0 değil, React'in sakladığı 1'i döndürür. Parantezdeki 0 yalnız ilk çizimde kullanılır. JSX'teki {altin} 1 olur; React yeni JSX'i eskisiyle karşılaştırır ve sayfada yalnız değişen yazıyı günceller.
İkinci sebebi kendi gözünle gör. Bu bileşende hem state hem düz değişken var:
import { useState } from "react" function App() { const [altin, setAltin] = useState(0) let duz = 0 function altinEkle() { setAltin(altin + 1) duz = duz + 1 console.log("duz:", duz) } return ( <main> <h1>Altın: {altin}</h1> <p>Düz değişken: {duz}</p> <button onClick={altinEkle}>Altın topla</button> </main> )} export default AppÜç tıklamadan sonra ekranda Altın: 3 ve Düz değişken: 0 yazar. Console'a bak:
duz: 1duz: 1duz: 1Ekran üç kez yeniden çizildi, ama duz hep 1'e çıkıp her yeni çizimde 0'dan başladı. Her çizim fonksiyonun yeni bir çağrısıdır; o çağrının düz değişkenleri bir sonrakine geçmez.
App fonksiyonunun yeni bir çağrısıdır. Düz değişken her çağrıda yeniden 0'la açılır; useState ise React'in sakladığı son değeri verir.TypeScript useState(0)'dan kutunun tipini number olarak çıkarır, tıpkı ısınmadaki let altin = 20 gibi. setAltin("10") yazarsan VS Code satırın altını kırmızıyla çizer. Bunun gerçek mesajını kontrollü girdi bölümünde göreceksin.
Güncelleme fonksiyonu ve eski değer tuzağı
Bir düğme daha ekleyelim: iki tur birden geçince iki altın gelsin. Elinde zaten tek tur geçen bir fonksiyon var; onu iki kez çağırmak mantıklı görünüyor:
import { useState } from "react" function App() { const [altin, setAltin] = useState(0) function turGec() { setAltin(altin + 1) } function ikiTurGec() { turGec() turGec() } return ( <main> <h1>Altın: {altin}</h1> <button onClick={turGec}>Tur geç</button> <button onClick={ikiTurGec}>İki tur geç</button> </main> )} export default AppSayfa açıkken İki tur geç'e bir kez bas. Ekranda Altın: 2 değil, Altın: 1 yazar. Hata mesajı yok; program sessizce yanlış hesaplıyor.
Sebep şu: altin bu çizim boyunca sabit bir değerdir, kutunun kendisi değil. Tıklama anında bu çizimin altin'i 0'dı. İlk turGec() "altını 0 + 1 yap" dedi; ikinci turGec() da aynı çizimin altin'ini okudu ve yine "0 + 1 yap" dedi. React iki kez "1 yap" emri aldı. Bunu bir deneyle de görebilirsin. turGec'i şöyle değiştir ve Tur geç'e üç kez bas:
function turGec() { setAltin(altin + 1) console.log("altin:", altin) }altin: 0altin: 1altin: 2Ekran 3 gösterirken Console hep bir eksiğini yazdı. setAltin değeri hemen değiştirmez; React'e "bir sonraki çizimde bu değer olsun" diye not bırakır. O tıklamanın içinde altin eski çizimdeki değer olarak kalır.
Çözüm, setAltin'e değer yerine bir fonksiyon vermek. turGec'teki console.log satırını sil ve fonksiyonu şöyle değiştir:
function turGec() { setAltin((onceki) => onceki + 1) }Şimdi İki tur geç her basışta 2 ekler. React bu fonksiyonları sıraya koyar ve sırayla çalıştırır: birincisi 0'ı alıp 1 döndürür, ikincisi o 1'i alıp 2 döndürür. Kurulumda şablonun App.tsx'inde gördüğün setCount((count) => count + 1) satırı da bir güncelleme fonksiyonuydu: count adlı parametre önceki değeri alıyor.
Kural: yeni değer eskisinden hesaplanıyorsa (+ 1, - 30, listeye ekle) güncelleme fonksiyonu güvenlidir. Sabit bir değer koyuyorsan (setAltin(0)) gerek yok. Bir olayda yalnız bir kez setX çağırıyorsan setAltin(altin + 1) da doğru çalışır; tuzak, aynı olayda ikinci kez çağırdığında kurulur.
Olaylara tepki vermek
Ders 22'de olayları addEventListener ile dinliyordun. React'te dinleyiciyi JSX'in içine, öğenin bir prop'u olarak yazarsın:
| Ders 22 (DOM) | React |
|---|---|
dugme.addEventListener("click", altinEkle) | <button onClick={altinEkle}> |
kutu.addEventListener("input", ...) | <input onChange={...}> |
form.addEventListener("submit", ...) | <form onSubmit={...}> |
olay.target.value, olay.preventDefault() | aynısı |
Üç fark var. Adlar camelCase yazılır: onclick değil onClick. Değer tırnak içinde bir metin değil, süslü parantez içinde bir fonksiyondur. Bir de onChange: React'te o, Ders 22'deki input olayı gibi her tuş vuruşunda çalışır; kutudan çıkmayı beklemez.
Fonksiyonu ver, çağırma
İki düğme: biri 1, biri 5 altın eklesin. Miktarı parametre olarak alan bir fonksiyon yazarsın. Yeni değer eskisinden hesaplandığı için güncelleme fonksiyonunu baştan kullanıyoruz:
import { useState } from "react" function App() { const [altin, setAltin] = useState(0) function altinEkle(miktar: number) { setAltin((onceki) => onceki + miktar) } return ( <main> <h1>Altın: {altin}</h1> <button onClick={() => altinEkle(1)}>+1 altın</button> <button onClick={() => altinEkle(5)}>+5 altın</button> </main> )} export default ApponClick={() => altinEkle(5)} satırında React'e verdiğin şey, içinde altinEkle(5) yazan yeni bir ok fonksiyonu. React onu tıklama anında çağırır, o da altinEkle(5)'i çağırır. Parametre gerekmediğinde onClick={altinEkle} yeterli; gerektiğinde araya bir ok fonksiyonu koyarsın. Akla gelen kısa yol onClick={altinEkle(5)} ise sayfayı bomboş bırakır:
Too many re-renders. React limits the number of renders to prevent an infinite loop.Ne zaman çıkar? Olay işleyiciye fonksiyonu değil, fonksiyonun çağrısını verince ve o fonksiyon state değiştiriyorsa.
<h1>Altın: {altin}</h1> <button onClick={altinEkle(1)}>+1 altın</button> <button onClick={altinEkle(5)}>+5 altın</button>Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. at renderWithHooksAgain (react-dom_client.js?v=33ce73f9:4444:53) at renderWithHooks (react-dom_client.js?v=33ce73f9:4402:62) ...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.Sayfa boş kalır. Süslü parantezin içi çizim sırasında hesaplanır: React App'i çizerken altinEkle(1)'i hemen çalıştırır. O da setAltin çağırır, bu yeni bir çizim ister; yeni çizim yine altinEkle(1)'i çalıştırır... Bir sonsuz döngü. React 25 turdan sonra döngüyü kesip bu hatayı fırlatır. Ders 22'deki addEventListener("click", altinEkle()) hatasıyla aynı yanlış; orada sessizce bir kez çalışıyordu, burada state yüzünden döngüye giriyor.
TypeScript bunu sen kaydetmeden önce yakalar: VS Code onClick'in altını kırmızıyla çizer. npm run build ile denetlersen:
PS C:\kod\tft-kurucu> npm run build > tft-kurucu@0.0.0 build> tsc -b && vite build src/App.tsx:13:21 - error TS2322: Type 'void' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'. 13 <button onClick={altinEkle(1)}>+1 altın</button> ~~~~~~~...Found 2 errors.altinEkle(1) bir şey döndürmüyor (void), onClick ise bir fare olayı işleyicisi (MouseEventHandler) bekliyor. Mesaj, "fonksiyon bekliyordum, fonksiyonun sonucunu aldım" diyor.
Nasıl düzelir? Parametre yoksa parantezi sil: onClick={altinEkle}. Parametre varsa araya ok fonksiyonu koy: onClick={() => altinEkle(1)}.
onChange ve onSubmit'i birazdan dükkânın arama kutusunda kullanacaksın. Önce dükkânın kendisine, yani bir dizi state'e geçelim.
Dizi state'i: takıma birim eklemek
Sayaç denemeleri bitti; asıl uygulamaya geçiyoruz. src klasöründe birimler.ts adında yeni bir dosya aç. Birim listesi Ders 22'deki dükkânla aynı:
export type Birim = { ad: string maliyet: number} export type TakimBirimi = { id: number ad: string maliyet: number yildiz: number} export const birimler: Birim[] = [ { ad: "Garen", maliyet: 1 }, { ad: "Teemo", maliyet: 1 }, { ad: "Annie", maliyet: 2 }, { ad: "Yasuo", maliyet: 2 }, { ad: "Lux", maliyet: 3 }, { ad: "Jinx", maliyet: 3 }, { ad: "Ahri", maliyet: 4 }, { ad: "Aurelion Sol", maliyet: 5 }]Birim dükkândaki bir kart; TakimBirimi takıma girmiş bir birim. Takımdakilerin bir kimliği (id) ve yıldızı var, çünkü aynı birimden iki tane alabilirsin ve ikisinin yıldızı farklı olabilir.
Şimdi App.tsx'i baştan yaz. İlk denemede herkesin aklına gelen yolu izliyoruz: diziye push ile eklemek.
import { useState } from "react"import { birimler, type Birim, type TakimBirimi } from "./birimler.ts" function App() { const [takim, setTakim] = useState<TakimBirimi[]>([]) function ekle(birim: Birim) { takim.push({ ...birim, id: Date.now(), yildiz: 1 }) setTakim(takim) console.log("takimda", takim.length, "birim var") } return ( <main> <h1>TFT takım kurucu</h1> <div className="sutunlar"> <section className="panel"> <h2>Dükkân</h2> <ul className="liste"> {birimler.map((birim) => ( <li key={birim.ad}> <span>{birim.ad}</span> <span className="soluk">{birim.maliyet} altın</span> <button onClick={() => ekle(birim)}>Ekle</button> </li> ))} </ul> </section> <section className="panel"> <h2>Takım ({takim.length})</h2> <ul className="liste"> {takim.map((birim) => ( <li key={birim.id}> <span>{birim.ad}</span> </li> ))} </ul> </section> </div> </main> )} export default AppYeni olan birkaç parça:
type Birimiçe aktarırken başınatypeyazdık: bu adlar yalnız tip, çalışan koda girmez. Vite şablonunun TypeScript ayarı bunu ister.useState<TakimBirimi[]>([]): başlangıç değeri boş dizi. TypeScript boş diziye bakıp içine ne gireceğini çıkaramaz; açılı parantezle sen söylersin: "bu kutudaTakimBirimidizisi duracak". Ders 39'da gördüğünArray<string>yazılışındaki açılı parantezin aynısı.{ ...birim, id: Date.now(), yildiz: 1 }: dükkândaki birimin alanlarını kopyala, üstüne kimlik ve yıldız ekle. Kimlik için Ders 23'tekiDate.now()'u kullandık.
Üç farklı birimin Ekle düğmesine bas. Console:
takimda 1 birim vartakimda 2 birim vartakimda 3 birim varDizi büyüyor, ama sağdaki başlık hâlâ Takım (0) ve liste boş. Hata mesajı yok.
setTakim(takim) React'e aynı diziyi verdi. React yeni değeri eskisiyle karşılaştırırken içine bakmaz, ikisinin aynı kutu olup olmadığına bakar (Ders 12'deki referans). Aynı kutuysa "değişen bir şey yok" der ve çizmez. push diziyi yerinde değiştirdiği için yeni kutu hiç oluşmadı. Bunu React olmadan da görebilirsin; C:\kod\ogren klasöründe ders41.js dosyasını aç:
const takim = [{ ad: "Lux", yildiz: 1 }] const ayniDizi = takimayniDizi.push({ ad: "Jinx", yildiz: 1 })console.log(ayniDizi === takim, takim.length) const yeniDizi = [...takim, { ad: "Ahri", yildiz: 1 }]console.log(yeniDizi === takim, takim.length, yeniDizi.length)true 2false 2 3push'tan sonra iki ad hâlâ aynı diziyi gösteriyor (true). Yaymayla kurulan dizi ise yepyeni bir kutu (false), üstelik eski dizi olduğu gibi duruyor.
push aynı kutuyu değiştirir, React çizmez. Yayma yeni kutu kurar, React yeniden çizer.Kural: state'teki diziyi ya da objeyi asla yerinde değiştirme; değişikliği içeren yeni bir kopya kur ve onu ver. Ders 11'de buna değişmezlik demiştik ve "ileride React bu alışkanlığı isteyecek" diye eklemiştik. O gün bugün.
Eklemek, çıkarmak, değiştirmek
App.tsx'teki ekle fonksiyonunu yaymayla yeniden yaz, altına çıkarma ve yıldız fonksiyonlarını ekle. Takım listesinde de her birimin yanına iki düğme koy. Dosyanın tamamı:
import { useState } from "react"import { birimler, type Birim, type TakimBirimi } from "./birimler.ts" function App() { const [takim, setTakim] = useState<TakimBirimi[]>([]) function ekle(birim: Birim) { const yeni = { ...birim, id: Date.now(), yildiz: 1 } setTakim([...takim, yeni]) } function cikar(id: number) { setTakim(takim.filter((birim) => birim.id !== id)) } function yildizArtir(id: number) { setTakim(takim.map((birim) => birim.id === id ? { ...birim, yildiz: birim.yildiz + 1 } : birim )) } return ( <main> <h1>TFT takım kurucu</h1> <div className="sutunlar"> <section className="panel"> <h2>Dükkân</h2> <ul className="liste"> {birimler.map((birim) => ( <li key={birim.ad}> <span>{birim.ad}</span> <span className="soluk">{birim.maliyet} altın</span> <button onClick={() => ekle(birim)}>Ekle</button> </li> ))} </ul> </section> <section className="panel"> <h2>Takım ({takim.length})</h2> <ul className="liste"> {takim.map((birim) => ( <li key={birim.id}> <span>{birim.ad} {"★".repeat(birim.yildiz)}</span> <button onClick={() => yildizArtir(birim.id)} disabled={birim.yildiz === 3} > Yıldız </button> <button onClick={() => cikar(birim.id)}>Çıkar</button> </li> ))} </ul> </section> </div> </main> )} export default AppÜç işlemin üçü de yeni dizi kuruyor:
- Eklemek:
[...takim, yeni]eski birimleri yeni bir diziye döker, sonuna yenisini koyar. - Çıkarmak:
filter(Ders 11) kimliği tutmayanlardan yeni bir dizi kurar; tutan dışarıda kalır. - Değiştirmek:
mapher birimi olduğu gibi geri verir, yalnız kimliği tutanın yerine{ ...birim, yildiz: birim.yildiz + 1 }koyar. Yani hem dizi hem değişen obje yeni; Ders 23'teki düzenleme kalıbının aynısı.
"★".repeat(birim.yildiz) yıldız işaretini yıldız sayısı kadar yan yana yazar (Ders 32). Yıldız 3 olunca düğme disabled ile soluklaşır; TFT'de 3 yıldızın üstü yok. Lux, Jinx ve Ahri'yi ekleyip Lux'un yıldızını bir kez artırınca:

map ile değiştirmenin bir obje ayrıntısı var: değişmeyen birimler aynı obje olarak yeni diziye geçer, yalnız değişen birim yepyeni bir obje olur. ders41.js'te dene:
const takim = [ { id: 1, ad: "Lux", yildiz: 1 }, { id: 2, ad: "Jinx", yildiz: 1 }] const yeniTakim = takim.map((birim) => birim.id === 2 ? { ...birim, yildiz: birim.yildiz + 1 } : birim)console.log(yeniTakim)console.log(takim[1].yildiz)console.log(yeniTakim[0] === takim[0], yeniTakim[1] === takim[1])[ { id: 1, ad: 'Lux', yildiz: 1 }, { id: 2, ad: 'Jinx', yildiz: 2 } ]1true falseEski takımdaki Jinx hâlâ 1 yıldızlı; eski state bozulmadı. Bu tablo hangi işlemi nasıl yazacağını özetliyor:
| İşlem | Yerinde değiştiren (state'te yasak) | Kopya kuran (doğru) |
|---|---|---|
| Sona ekle | takim.push(yeni) | [...takim, yeni] |
| Çıkar | takim.splice(i, 1) | takim.filter((b) => b.id !== id) |
| Bir elemanı değiştir | takim[i].yildiz = 2 | map içinde { ...b, yildiz: 2 } |
| Sırala | takim.sort(...) | takim.toSorted(...) |
| Objenin bir alanı | oyuncu.altin = 50 | { ...oyuncu, altin: 50 } |
push sonrası ekran güncellenmiyorNe zaman çıkar? State'teki diziyi push, splice, sort ya da takim[0].yildiz = 2 gibi yerinde değiştiren bir işlemle değiştirip aynı diziyi setTakim(takim) ile verince. Hata mesajı yok; Console'a yazdırırsan dizi değişmiş görünür, ekran değişmez.
Daha sinsi hâli: Başka bir state değişip bileşen yeniden çizilince, gizlice büyümüş dizi birden ekrana gelir. Biz denedik: iki Ekle'den sonra Takım (0) yazarken alakasız bir sayacı artırınca başlık bir anda Takım (2) oldu. Bu yüzden hata "bazen çalışıyor" gibi görünür.
Nasıl düzelir? Yeni dizi kur: setTakim([...takim, yeni]). Tablodaki sağ sütunu kullan.
Obje state'i
Tablonun son satırı, state'in kendisi tek bir obje olduğunda geçerli. Takım kurucuda altın ve seviye için iki ayrı state açacağız, ama ikisini tek bir objede de tutabilirdin:
const [oyuncu, setOyuncu] = useState({ ad: "Ahri", altin: 300, seviye: 1 }) function seviyeAtla() { setOyuncu((onceki) => ({ ...onceki, seviye: onceki.seviye + 1 }))}Kural dizideki gibi: oyuncu.seviye = 2 yazıp aynı objeyi verme, yeni obje kur. ...onceki değişmeyen alanları (ad, altin) yeni objeye taşır, arkasından gelen seviye eskisinin üstüne yazılır. Ok fonksiyonu obje döndürdüğü için süslü parantezi normal parantezin içine aldık (Ders 10); yoksa JavaScript onu fonksiyonun gövdesi sanır.
Yaymayı unutup yalnız değişen alanı verirsen React iki objeyi birleştirmez: eskisini atar, yerine verdiğini koyar. setOyuncu({ seviye: 2 }) denedik; ekrandaki ad ve altın silindi. TypeScript bunu önceden yakalar; mesajın sonu is missing the following properties ...: ad, altin, yani "ad ve altin alanları eksik."
Encountered two children with the same keyNe zaman çıkar? Listede iki öğeye aynı key verince. Takım listesinde key={birim.ad} yazıp Garen'i iki kez eklersen:
Encountered two children with the same key, `Garen`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version."İki çocuk aynı anahtara sahip. Anahtarlar benzersiz olmalı; yoksa öğeler çoğalabilir ya da kaybolabilir." Dükkânda ad yeterliydi, çünkü her birim bir kez listeleniyor; takımda aynı birimden birden çok olabilir.
Nasıl düzelir? Her takım birimine eklenirken benzersiz bir kimlik ver ve key={birim.id} yaz. Isınmadaki gibi sıra numarasını da kullanma; nedenini hemen göreceksin.
key ve satırın kendi hâli
Ders 40'ta sıra numarasını key yapmanın, öğelerin kendi hâli olunca sorun çıkaracağını söylemiştik. Şimdi elimizde silinebilen bir liste var; kısa bir deneyle görelim. Takım listesinde key'i geçici olarak sıra numarası yap ve adın yanına not yazmak için bir kutu koy:
{takim.map((birim, sira) => ( <li key={sira}> <span>{birim.ad} {"★".repeat(birim.yildiz)}</span> <input placeholder="not" size={8} />map'in ikinci parametresi sıra numarası (Ders 11). Garen'i ve Lux'u ekle, Garen'in kutusuna ön sıra yaz, sonra Garen'in Çıkar'ına bas:


Garen gitti ama notu Lux'a kaldı. Kutudaki yazıyı React'in state'i değil, tarayıcı tutuyor (böyle kutulara kontrolsüz denir; karşıtını bir sonraki bölümde göreceksin). React satırları key'e göre eşleştirir: önce 0 Garen, 1 Lux idi; çıkarmadan sonra 0 Lux. React "0 duruyor, yalnız yazısı değişti, 1 gitti" diye düşündü: 0'ın kutusunu yerinde bıraktı, ikinci satırı sildi. Bir bileşenin state'i de aynı kurala uyar: key'i aynı kalan öğede kalır. Aynı deneyi key={birim.id} ile yap: Garen'in satırı kutusuyla birlikte gider, Lux'un kutusu boş kalır. Deneyden sonra kutuyu sil ve map((birim) => ile key={birim.id}'yi geri koy.
Kontrollü girdi: arama kutusu
Dükkânın üstüne bir arama kutusu koyacağız: yazdıkça liste süzülsün, Enter'a basınca görünen ilk birim takıma eklensin ve kutu boşalsın. Kutudaki metni de bir state'te tutacağız. Bunun adı kontrollü girdi: kutunun değeri her zaman state'ten gelir, her tuş vuruşu da önce state'e gider.
App'in en üstüne, takim'in altına yeni state'i ve süzülmüş listeyi ekle:
function App() { const [takim, setTakim] = useState<TakimBirimi[]>([]) const [arama, setArama] = useState("") const gorunenler = birimler.filter((birim) => birim.ad.toLowerCase().includes(arama.trim().toLowerCase()) )gorunenler, arama metnine uyan birimler: "Ahri".toLowerCase() içinde "ah" geçiyor mu? Kutu boşken includes("") her ad için true döner, yani bütün liste görünür. Şimdi formun gönderilmesini yaz. yildizArtir'ın altına:
function aramaGonder(olay: React.SubmitEvent<HTMLFormElement>) { olay.preventDefault() if (gorunenler.length === 0) { return } ekle(gorunenler[0]) setArama("") }- Formun içinde Enter'a basmak
submitolayını başlatır. Ders 23'teki gibipreventDefault()sayfanın yenilenmesini durdurur. - Fonksiyon ayrı tanımlandığı için
olay'ın tipini sen yazdın:React.SubmitEvent<HTMLFormElement>, "bir formun gönderme olayı".React.öneki için bir şey içe aktarmana gerek yok; bu ad React'in tip paketinden gelir. Nereden bilecektin? VS Code'da fareyi JSX'tekionSubmit'in üstüne getirinceSubmitEventHandler<HTMLFormElement>yazar:SubmitEventalan bir fonksiyon. setArama("")kutuyu boşaltır. Kontrollü girdinin gücü bu: kutunun içeriğini koddan istediğin an değiştirebilirsin, çünkü kutu state'i gösteriyor.
Son olarak dükkân bölümünde <h2>Dükkân</h2>'nin altına formu koy ve listeyi birimler yerine gorunenler'den çiz:
<h2>Dükkân</h2> <form onSubmit={aramaGonder}> <input placeholder="Birim ara, Enter ile ekle" value={arama} onChange={(olay) => setArama(olay.target.value)} /> </form> <ul className="liste"> {gorunenler.map((birim) => ( <li key={birim.ad}> <span>{birim.ad}</span> <span className="soluk">{birim.maliyet} altın</span> <button onClick={() => ekle(birim)}>Ekle</button> </li> ))} </ul> {gorunenler.length === 0 && <p className="soluk">Bu adla birim yok.</p>}Kutudaki iki prop birlikte çalışır:
value={arama}: kutuda ne yazacağına state karar verir.onChange={(olay) => setArama(olay.target.value)}: her tuş vuruşunda React bu fonksiyonu olay nesnesiyle çağırır.olay.targetkutunun kendisi,valueiçindeki metin; Ders 22'deki gibi. Biz onu state'e yazarız, React yeniden çizer, kutu yeni değeri gösterir.
onChange'deki fonksiyonu satırın içine yazdığımız için TypeScript olay'ın tipini kendisi çıkarır; orada tip notu gerekmez.
Kutuya te yazıp Enter'a bastık: Teemo takıma girdi, kutu boşaldı. Sonra an yazdık:

Listeyi formun dışına koyduğumuza dikkat et. Form içindeki bir button'ın varsayılan türü submit'tir (Ders 19); Ekle düğmeleri formun içinde olsaydı her tıklama formu da gönderirdi.
You provided a value prop to a form field without an onChange handler.Ne zaman çıkar? Kutuya value verip onChange vermeyince.
<form onSubmit={aramaGonder}> <input placeholder="Birim ara, Enter ile ekle" value={arama} /> </form>You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.Kutuya ne yazarsan yaz, hiçbir harf görünmez. Mesaj "bu kutu salt okunur çizilecek" diyor: değeri state'ten geliyor, ama kimse state'i değiştirmiyor. Her tuştan sonra React kutuyu yine state'teki boş metinle çizer.
Nasıl düzelir? onChange={(olay) => setArama(olay.target.value)} ekle. Kutunun gerçekten salt okunur olmasını istiyorsan mesajın önerdiği gibi readOnly yaz.
Sayı kutusu: altın
Takım kurucunun bir de "kaç altınım var?" kutusu olacak. arama'nın altına bir state daha ekle:
const [altin, setAltin] = useState(20)<h1>'in hemen altına:
<div className="ayarlar"> <label> Altın <input type="number" min="0" value={altin} onChange={(olay) => setAltin(Number(olay.target.value))} /> </label> </div>type="number" olsa da kutunun value'su her zaman metindir (Ders 23'teki "kutudan gelen her şey metin" kuralı). Number(...) onu sayıya çevirir. Unutursan:
Argument of type 'string' is not assignable to parameter of type 'SetStateAction<number>'.Ne zaman çıkar? Sayı tutan bir state'e kutudan gelen metni doğrudan verince: onChange={(olay) => setAltin(olay.target.value)}.
PS C:\kod\tft-kurucu> npm run build > tft-kurucu@0.0.0 build> tsc -b && vite build src/App.tsx:47:54 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'SetStateAction<number>'. 47 onChange={(olay) => setAltin(olay.target.value)} ~~~~~~~~~~~~~~~~~ Found 1 error.SetStateAction<number>, setAltin'in kabul ettiği şey: bir sayı ya da sayı döndüren bir güncelleme fonksiyonu. Sen metin verdin.
Vite geliştirme sunucusu tip denetimi yapmaz; uyarıyı görmezden gelirsen sayfa yine açılır ve hata sessizce işler. Görmek istersen </label>'ın altına geçici olarak <p>Beş tur sonra: {altin + 5}</p> ekle ve kutuya 30 yaz. Ekranda Beş tur sonra: 305 çıkar: state artık "30" metni ve + onu 5 ile yapıştırıyor (Ders 1). TypeScript tam olarak bu hatayı önceden haber veriyordu. Denemeden sonra o satırı sil.
Nasıl düzelir? setAltin(Number(olay.target.value)). VS Code'daki kırmızı çizgileri görmezden gelme; arada bir npm run build ile bütün projeyi denetle.
State'i ortak ebeveyne taşımak
App.tsx uzadı: dükkân, arama, takım listesi ve ayarlar aynı fonksiyonda. Ders 40'taki gibi ekranı bileşenlere bölelim: solda Dukkan, sağda TakimPaneli.
Bölerken asıl soru şu: takim state'i nerede duracak? Dukkan ona birim ekliyor, TakimPaneli onu gösteriyor ve ondan birim çıkarıyor. State'i Dukkan'a koyarsan TakimPaneli onu göremez: React'te veri yalnız yukarıdan aşağı, props ile akar; iki kardeş bileşen arasında doğrudan yol yoktur. Çözüm, state'i ikisinin en yakın ortak ebeveynine, yani App'e koymak. App veriyi props ile aşağı verir; çocuklar bir şeyi değiştirmek isteyince App'in onlara verdiği fonksiyonu çağırır.
takim, altin ve seviye App'te durur (seviye'yi birazdan ekleyeceksin). Veri props ile aşağı iner; değişiklik isteği fonksiyon çağrısıyla yukarı çıkar. Kardeşler birbiriyle doğrudan konuşmaz.arama ise yalnız Dukkan'ı ilgilendiriyor; o Dukkan'ın içinde kalır. Kural: state'i, onu kullanan bileşenlerin en yakın ortak ebeveynine koy; daha yukarıya değil.
src klasöründe iki yeni dosya aç. Önce Dukkan.tsx; aramayla ilgili her şey buraya taşınıyor:
import { useState } from "react"import { birimler, type Birim } from "./birimler.ts" type DukkanProps = { onEkle: (birim: Birim) => void} function Dukkan({ onEkle }: DukkanProps) { const [arama, setArama] = useState("") const gorunenler = birimler.filter((birim) => birim.ad.toLowerCase().includes(arama.trim().toLowerCase()) ) function aramaGonder(olay: React.SubmitEvent<HTMLFormElement>) { olay.preventDefault() if (gorunenler.length === 0) { return } onEkle(gorunenler[0]) setArama("") } return ( <section className="panel"> <h2>Dükkân</h2> <form onSubmit={aramaGonder}> <input placeholder="Birim ara, Enter ile ekle" value={arama} onChange={(olay) => setArama(olay.target.value)} /> </form> <ul className="liste"> {gorunenler.map((birim) => ( <li key={birim.ad}> <span>{birim.ad}</span> <span className="soluk">{birim.maliyet} altın</span> <button onClick={() => onEkle(birim)}>Ekle</button> </li> ))} </ul> {gorunenler.length === 0 && <p className="soluk">Bu adla birim yok.</p>} </section> )} export default DukkanonEkle: (birim: Birim) => void bir fonksiyon tipi: "Birim alan ve bir şey döndürmeyen fonksiyon". Dukkan takımı hiç görmüyor; yalnız "bu birimi ekle" diye haber veriyor. Prop'un adını on ile başlatmak React'te gelenek: "şu olunca bunu çağır".
Sonra TakimPaneli.tsx:
import type { TakimBirimi } from "./birimler.ts" type TakimPaneliProps = { takim: TakimBirimi[] onCikar: (id: number) => void onYildiz: (id: number) => void} function TakimPaneli({ takim, onCikar, onYildiz }: TakimPaneliProps) { return ( <section className="panel"> <h2>Takım ({takim.length})</h2> <ul className="liste"> {takim.map((birim) => ( <li key={birim.id}> <span>{birim.ad} {"★".repeat(birim.yildiz)}</span> <button onClick={() => onYildiz(birim.id)} disabled={birim.yildiz === 3} > Yıldız </button> <button onClick={() => onCikar(birim.id)}>Çıkar</button> </li> ))} </ul> </section> )} export default TakimPaneliApp.tsx artık kısa. arama, gorunenler ve aramaGonder'i sildik; takim, altin ve onları değiştiren fonksiyonlar kaldı:
import { useState } from "react"import Dukkan from "./Dukkan.tsx"import TakimPaneli from "./TakimPaneli.tsx"import type { Birim, TakimBirimi } from "./birimler.ts" function App() { const [takim, setTakim] = useState<TakimBirimi[]>([]) const [altin, setAltin] = useState(20) function ekle(birim: Birim) { const yeni = { ...birim, id: Date.now(), yildiz: 1 } setTakim([...takim, yeni]) } function cikar(id: number) { setTakim(takim.filter((birim) => birim.id !== id)) } function yildizArtir(id: number) { setTakim(takim.map((birim) => birim.id === id ? { ...birim, yildiz: birim.yildiz + 1 } : birim )) } return ( <main> <h1>TFT takım kurucu</h1> <div className="ayarlar"> <label> Altın <input type="number" min="0" value={altin} onChange={(olay) => setAltin(Number(olay.target.value))} /> </label> </div> <div className="sutunlar"> <Dukkan onEkle={ekle} /> <TakimPaneli takim={takim} onCikar={cikar} onYildiz={yildizArtir} /> </div> </main> )} export default AppSayfa öncekiyle aynı çalışır. Değişen, verinin yolu: Dukkan'daki Ekle'ye basınca onEkle(birim) çağrılır; o aslında App'in ekle'si. App'in state'i değişir, App yeniden çizilir ve yeni takim dizisi props ile TakimPaneli'ne iner.
Türetilmiş değerler
Son iki özellik: oyuncu seviyesi ve uyarılar. Seviye, tahtaya kaç birim koyabileceğini belirler; App'e bir state daha:
const [takim, setTakim] = useState<TakimBirimi[]>([]) const [altin, setAltin] = useState(20) const [seviye, setSeviye] = useState(4)Ayarlar satırına, </label>'ın altına iki düğme. Yeni seviye eskisinden hesaplandığı için güncelleme fonksiyonu kullanıyoruz; Math.max ve Math.min (Ders 9) seviyeyi 1 ile 10 arasında tutar:
<span>Seviye {seviye}</span> <button onClick={() => setSeviye((s) => Math.max(1, s - 1))}>−</button> <button onClick={() => setSeviye((s) => Math.min(10, s + 1))}>+</button>Şimdi takımın toplam maliyetini göstermek istiyoruz. İki yıldızlı bir birim 3 kopya, üç yıldızlı 9 kopya eder. birimler.ts'in sonuna bir yardımcı fonksiyon ekle:
export function kopyaSayisi(yildiz: number) { if (yildiz === 3) { return 9 } if (yildiz === 2) { return 3 } return 1}Yanlış yol: her şeyi state yapmak
"Ekranda değişen bir sayı, demek ki state" diye düşünmek doğal. toplamMaliyet adında bir state açıp ekle'de artırdığını düşün:
const [toplamMaliyet, setToplamMaliyet] = useState(0) function ekle(birim: Birim) { const yeni = { ...birim, id: Date.now(), yildiz: 1 } setTakim([...takim, yeni]) setToplamMaliyet(toplamMaliyet + birim.maliyet) }Ekle'de doğru çalışır. Ama cikar ve yildizArtir da toplamı değiştiriyor ve ikisine de aynı hesabı eklemeyi unutabilirsin. Unutunca olan şu: Lux'u (3) ve Ahri'yi (4) ekledik, Lux'u iki yıldıza çıkardık, Ahri'yi çıkardık.

Hata mesajı yok, ekran yalan söylüyor. Aynı bilgi iki yerde tutuldu (takim dizisinde ve toplamMaliyet'te), ikisinin aynı kalması da senin dikkatine kaldı. Isınmadaki oyuncu_sayisi sütununun aynısı.
Doğru yol: her çizimde hesapla
Toplam, takim'den her an hesaplanabilir. O hâlde onu tutma, çizim sırasında hesapla. Bileşen her çizimde baştan çalıştığı için hesap hep güncel takim'den yapılır; unutulacak bir yer kalmaz. TakimPaneli'ne altin ve seviye'yi de props olarak verip toplamı ve uyarıları orada hesaplayacağız. Uygulamanın tamamında göreceğin TakimPaneli'nin yeni kısmı:
function TakimPaneli({ takim, altin, seviye, onCikar, onYildiz }: TakimPaneliProps) { const toplamMaliyet = takim.reduce( (toplam, birim) => toplam + birim.maliyet * kopyaSayisi(birim.yildiz), 0 ) const uyarilar: string[] = [] if (takim.length > seviye) { uyarilar.push(`Seviye ${seviye}: tahtaya en fazla ${seviye} birim sığar.`) } if (toplamMaliyet > altin) { uyarilar.push(`Altının yetmiyor: ${toplamMaliyet - altin} altın eksik.`) }toplamMaliyet ve uyarilar sıradan const'lar. Burada push serbest: uyarilar state değil, bu çizimde yeni açılan yerel bir dizi; çizim bitince kaybolur. Yasak olan, React'in sakladığı state'i yerinde değiştirmek. Arama bölümündeki gorunenler de aynı türden bir değerdi: arama'dan hesaplanıyordu.
Bir değerin state olup olmaması gerektiğini üç soruyla anlarsın:
- Props ile mi geliyor? O zaman state değil.
- Hiç değişmiyor mu (
birimlerlistesi gibi)? O zaman state değil, sıradan bir sabit. - Başka state'lerden ya da props'tan hesaplanabiliyor mu? O zaman state değil, türetilmiş değer.
Üçüne de "hayır" diyorsan state'tir. Takım kurucuda yalnız dört state var: takim, altin, seviye ve arama.
Uygulamanın tamamı
birimler.ts ve Dukkan.tsx yukarıdaki gibi kalıyor. App.tsx'in son hâli:
import { useState } from "react"import Dukkan from "./Dukkan.tsx"import TakimPaneli from "./TakimPaneli.tsx"import type { Birim, TakimBirimi } from "./birimler.ts" function App() { const [takim, setTakim] = useState<TakimBirimi[]>([]) const [altin, setAltin] = useState(20) const [seviye, setSeviye] = useState(4) function ekle(birim: Birim) { const yeni = { ...birim, id: Date.now(), yildiz: 1 } setTakim([...takim, yeni]) } function cikar(id: number) { setTakim(takim.filter((birim) => birim.id !== id)) } function yildizArtir(id: number) { setTakim(takim.map((birim) => birim.id === id ? { ...birim, yildiz: birim.yildiz + 1 } : birim )) } return ( <main> <h1>TFT takım kurucu</h1> <div className="ayarlar"> <label> Altın <input type="number" min="0" value={altin} onChange={(olay) => setAltin(Number(olay.target.value))} /> </label> <span>Seviye {seviye}</span> <button onClick={() => setSeviye((s) => Math.max(1, s - 1))}>−</button> <button onClick={() => setSeviye((s) => Math.min(10, s + 1))}>+</button> </div> <div className="sutunlar"> <Dukkan onEkle={ekle} /> <TakimPaneli takim={takim} altin={altin} seviye={seviye} onCikar={cikar} onYildiz={yildizArtir} /> </div> </main> )} export default AppTakimPaneli.tsx'in son hâli:
import { kopyaSayisi, type TakimBirimi } from "./birimler.ts" type TakimPaneliProps = { takim: TakimBirimi[] altin: number seviye: number onCikar: (id: number) => void onYildiz: (id: number) => void} function TakimPaneli({ takim, altin, seviye, onCikar, onYildiz }: TakimPaneliProps) { const toplamMaliyet = takim.reduce( (toplam, birim) => toplam + birim.maliyet * kopyaSayisi(birim.yildiz), 0 ) const uyarilar: string[] = [] if (takim.length > seviye) { uyarilar.push(`Seviye ${seviye}: tahtaya en fazla ${seviye} birim sığar.`) } if (toplamMaliyet > altin) { uyarilar.push(`Altının yetmiyor: ${toplamMaliyet - altin} altın eksik.`) } return ( <section className="panel"> <h2>Takım ({takim.length}/{seviye})</h2> {takim.length === 0 && <p className="soluk">Takım boş. Dükkândan birim ekle.</p>} <ul className="liste"> {takim.map((birim) => ( <li key={birim.id}> <span>{birim.ad} {"★".repeat(birim.yildiz)}</span> <button onClick={() => onYildiz(birim.id)} disabled={birim.yildiz === 3} > Yıldız </button> <button onClick={() => onCikar(birim.id)}>Çıkar</button> </li> ))} </ul> <p>Toplam maliyet: <b>{toplamMaliyet}</b> altın</p> {uyarilar.map((uyari) => ( <p key={uyari} className="uyari">{uyari}</p> ))} </section> )} export default TakimPaneliLux, Jinx, Ahri, Aurelion Sol ve Garen'i ekleyip Lux'u iki yıldıza çıkardık ve seviyeyi bir düşürdük:

Toplamı kendin doğrula: Lux iki yıldız 3 × 3 = 9, Jinx 3, Ahri 4, Aurelion Sol 5, Garen 1; toplam 22. Altın 20 olduğu için 2 eksik.
Sınırlarda dene
"Çalışıyor" ile "doğru" aynı şey değil. Şunları tek tek dene:
- Seviye 1'deyken −'ye bas: 1'de kalmalı. 10'dayken +: 10'da kalmalı.
- Takım boşken: "Takım boş" yazısı,
Toplam maliyet: 0 altın, hiç uyarı yok. Altını 0 yapsan da uyarı çıkmamalı, çünkü 0 > 0 değil. - Toplam tam altına eşitken (ör. altın 3, yalnız Lux): uyarı yok. Bir birim daha ekle: uyarı gelir.
- Birim sayısı seviyeye eşitken uyarı yok; bir fazlası uyarı verir.
- Takımda yalnız Lux varken onu üç yıldıza çıkar: toplam 3 × 9 = 27 olur ve Yıldız düğmesi soluklaşır.
- Arama kutusuna
xyzyazıp Enter'a bas: "Bu adla birim yok." yazar, takıma bir şey eklenmez.
Sık hatalar
Bu derste kartların çoğunu ilgili bölümlerin içinde gördün. İki tane daha:
Ne zaman çıkar? Aynı olayda aynı state'i değerle iki kez değiştirince: setAltin(altin + 1) iki kez çalışır, altın yalnız 1 artar. Hata mesajı yoktur.
Nasıl düzelir? Yeni değer eskisinden hesaplanıyorsa güncelleme fonksiyonu ver: setAltin((onceki) => onceki + 1). React fonksiyonları sırayla zincirler. Bir de şunu unutma: setX'ten hemen sonraki satırda x hâlâ eski değerdir; yeni değeri kullanmak istiyorsan onu bir değişkende hesapla ve hem setX'e hem kullanacağın yere onu ver.
A component is changing an uncontrolled input to be controlled.Ne zaman çıkar? State'i başlangıç değeri vermeden açıp kutuya value olarak verince: useState<string>(). İlk çizimde değer undefined'dır; React bu kutuyu kontrolsüz sanar. İlk harfi yazınca değer metne döner ve React itiraz eder:
A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-componentsNasıl düzelir? Kontrollü girdinin state'ine her zaman bir başlangıç değeri ver: metin için useState(""), sayı için useState(0).
Görevler
Görevleri tft-kurucu projesinde, her birini src içinde ayrı bir dosyada yap: Gorev1.tsx, Gorev2.tsx… Takım kurucuyu bozmadan denemek için src/main.tsx'teki import App from './App.tsx' satırını geçici olarak import App from './Gorev1.tsx' yap. Görev dosyasındaki bileşen export default ile dışa aktarıldığı için App adıyla gelir (Ders 14'teki varsayılan dışa aktarma). Bütün görevler bitince satırı yeniden ./App.tsx yap. Önce kendin dene, takılırsan ipuçlarını sırayla aç.
Mana barı
Mana 100'den başlasın ve ekranda Mana: 100 / 100 yazsın. İki düğme:
- Yetenek (30 mana): manayı 30 düşürür. Mana 30'un altındaysa hiçbir şey yapmaz ve düğme soluk (
disabled) görünür. - Mavi buff (+25): manayı 25 artırır, ama 100'ü asla geçmez.
Beklenen: üç yetenekten sonra Mana: 10 / 100 ve yetenek düğmesi soluk. Bir buff: 35. Üç buff daha: 100 (110 değil). Sınır: mana tam 30 iken yetenek çalışmalı ve 0 olmalı.
İpucu 1
Tek state yeter: const [mana, setMana] = useState(100). Yeni mana eskisinden hesaplandığı için güncelleme fonksiyonu kullan.
İpucu 2
Üst sınır için seviye düğmesindeki gibi Math.min(100, onceki + 25). Düğmeyi soluklaştırmak için disabled={mana < 30}.
Çözümü göster
import { useState } from "react" function ManaBari() { const [mana, setMana] = useState(100) function yetenekKullan() { setMana((onceki) => (onceki >= 30 ? onceki - 30 : onceki)) } function buffAl() { setMana((onceki) => Math.min(100, onceki + 25)) } return ( <main> <h1>Mana: {mana} / 100</h1> <button onClick={yetenekKullan} disabled={mana < 30}> Yetenek (30 mana) </button> <button onClick={buffAl}>Mavi buff (+25)</button> </main> )} export default ManaBariDenedik: üç yetenek 10 ve soluk düğme; bir buff 35; üç buff daha 100. Sınırı da denedik: üç yetenek, iki buff ve bir yetenekle mana tam 30 oldu; bir yetenek daha çalıştı, mana 0 oldu ve düğme soluklaştı. disabled olsa bile yetenekKullan'daki denetimi bırak: düğme ileride klavye kısayoluyla da çağrılabilir; kural, düğmenin görünüşüne değil fonksiyona ait.
Sihirdar adı
Kontrollü bir yazı kutusu kur:
- Kutunun altında
7/16gibi bir sayaç: yazılan karakter sayısı ve sınır. - Kutuya 16 karakterden fazlası yazılamasın; 17. karakter kutuya hiç girmesin.
- Altında
Merhaba, Deniz!gibi bir selam. Kutu boşken ya da yalnız boşluk varkenMerhaba, sihirdar!yazsın. - Temizle düğmesi kutuyu boşaltsın; kutu zaten boşken soluk görünsün.
maxLength niteliğini kullanma; sınırı state'le koy.
İpucu 1
Kutunun değerini değiştiren tek yer onChange. Oraya gelen metni state'e yazmadan önce kesebilirsin: olay.target.value.slice(0, 16) (Ders 9).
İpucu 2
Selamdaki ad bir türetilmiş değer: ad.trim() boşsa "sihirdar", değilse kırpılmış ad. Ayrı bir state açma.
Çözümü göster
import { useState } from "react" function SihirdarAdi() { const [ad, setAd] = useState("") const gorunenAd = ad.trim() === "" ? "sihirdar" : ad.trim() return ( <main> <input placeholder="Sihirdar adın" value={ad} onChange={(olay) => setAd(olay.target.value.slice(0, 16))} /> <p>{ad.length}/16</p> <h1>Merhaba, {gorunenAd}!</h1> <button onClick={() => setAd("")} disabled={ad === ""}> Temizle </button> </main> )} export default SihirdarAdiSınır denemesi: GeceKurduAhri1234567 (20 karakter) yazınca kutuda GeceKurduAhri123 kalır, sayaç 16/16, selam Merhaba, GeceKurduAhri123! olur. Yalnız üç boşluk yazınca sayaç 3/16 ama selam Merhaba, sihirdar!. Temizle'den sonra 0/16. Bu kesme işini kontrolsüz bir kutuyla yapamazdın; burada kutunun içeriğine her tuşta state karar veriyor.
Envanter
LoL'de bir şampiyon en fazla 6 eşya taşır. Bir envanter yap:
- Kutuya eşya adı yazıp Enter'a ya da Ekle'ye basınca eşya listeye eklensin ve kutu boşalsın.
- Boş ad (ya da yalnız boşluk) eklenmesin.
- Aynı eşya ikinci kez eklenmesin; büyük-küçük harf fark etmesin (
KılıçvarkenKILIÇde eklenmesin). Bu durumda "Bu eşya zaten envanterde." uyarısı görünsün. - 6 eşya dolunca ekleme olmasın ve "Envanter dolu." yazsın.
- Her eşyanın yanında Sat düğmesi onu listeden çıkarsın.
- Başlıkta
Envanter (3/6)gibi sayı.
İpucu 1
İki state: eşya dizisi (useState<string[]>([])) ve kutudaki taslak metin. Formun onSubmit'inde preventDefault, ekleme ve kutuyu boşaltma.
İpucu 2
İpucu 3
Ekle düğmesini formun içine koyarsan tıklaması formu gönderir; ayrıca onClick yazmana gerek kalmaz. disabled={!eklenebilir} hem düğmeyi soluklaştırır hem Enter ile gönderimi durdurur. Yine de gonder'in içinde aynı denetimi yap.
Çözümü göster
import { useState } from "react" function Envanter() { const [esyalar, setEsyalar] = useState<string[]>([]) const [taslak, setTaslak] = useState("") const ad = taslak.trim() const dolu = esyalar.length >= 6 const zatenVar = esyalar.some( (esya) => esya.toLocaleLowerCase("tr") === ad.toLocaleLowerCase("tr") ) const eklenebilir = ad !== "" && !zatenVar && !dolu function gonder(olay: React.SubmitEvent<HTMLFormElement>) { olay.preventDefault() if (!eklenebilir) { return } setEsyalar([...esyalar, ad]) setTaslak("") } function sat(satilan: string) { setEsyalar(esyalar.filter((esya) => esya !== satilan)) } return ( <main> <h1>Envanter ({esyalar.length}/6)</h1> <form onSubmit={gonder}> <input placeholder="Eşya adı" value={taslak} onChange={(olay) => setTaslak(olay.target.value)} /> <button disabled={!eklenebilir}>Ekle</button> </form> {zatenVar && <p className="uyari">Bu eşya zaten envanterde.</p>} {dolu && <p className="uyari">Envanter dolu.</p>} <ul className="liste"> {esyalar.map((esya) => ( <li key={esya}> <span>{esya}</span> <button onClick={() => sat(esya)}>Sat</button> </li> ))} </ul> </main> )} export default EnvanterDenedik: Kılıç eklendi; KILIÇ yazınca uyarı çıktı ve Enter bir şey eklemedi. Yalnız boşlukla Enter: başlık (1/6)'da kaldı. Beş eşya daha: (6/6) ve "Envanter dolu."; yedinci eklenmedi. İlkini satınca (5/6) ve uyarı kayboldu. Aynı ad iki kez giremediği için key={esya} güvenli.
Hata avı: kill sayacı
Bu bileşende üç hata var. Gözle aramadan önce çalıştır, belirtiyi gör, Console'u oku; bir hatayı düzeltip yeniden dene.
import { useState } from "react" function KillSayaci() { const [sampiyon, setSampiyon] = useState("") const [kill, setKill] = useState(0) function killAl() { setKill(kill + 1) } function ikiliKill() { killAl() killAl() } return ( <main> <input placeholder="Şampiyon" value={sampiyon} /> <h1>{sampiyon || "Şampiyon"}: {kill} kill</h1> <button onClick={killAl()}>Kill</button> <button onClick={ikiliKill}>Çift kill</button> <button onClick={() => setKill(0)}>Sıfırla</button> </main> )} export default KillSayaciHepsi düzelince: kutuya Jinx yazınca başlık Jinx: 0 kill; Kill 1; Çift kill 3; Sıfırla 0.
İpucu 1
İlk belirti bomboş bir sayfa ve Console'da Too many re-renders. Hangi satır, sayfa çizilirken bir state'i değiştiriyor?
İpucu 2
Sayfa açılınca kutuya yazmayı dene. Console'daki uyarı hangi prop'un eksik olduğunu söylüyor. VS Code'da setSampiyon'un soluk ya da altı çizili göründüğüne de dikkat et: tanımlanmış ama hiç kullanılmamış.
İpucu 3
Çift kill 2 değil 1 ekliyorsa killAl aynı anlık görüntüyü iki kez okuyordur.
Çözümü göster
import { useState } from "react" function KillSayaci() { const [sampiyon, setSampiyon] = useState("") const [kill, setKill] = useState(0) function killAl() { setKill((onceki) => onceki + 1) } function ikiliKill() { killAl() killAl() } return ( <main> <input placeholder="Şampiyon" value={sampiyon} onChange={(olay) => setSampiyon(olay.target.value)} /> <h1>{sampiyon || "Şampiyon"}: {kill} kill</h1> <button onClick={killAl}>Kill</button> <button onClick={ikiliKill}>Çift kill</button> <button onClick={() => setKill(0)}>Sıfırla</button> </main> )} export default KillSayaciSırasıyla: onClick={killAl()} çizim sırasında state değiştiriyordu (Too many re-renders); parantezi sildik. Kutuda value var ama onChange yoktu (You provided a value prop… uyarısı, yazılamayan kutu); ekledik. setKill(kill + 1) iki kez aynı değeri veriyordu; güncelleme fonksiyonuna çevirdik. Denedik: Jinx: 1 kill, sonra Jinx: 3 kill, sonra Jinx: 0 kill.
Kule savunması
Üç kulenin canını gösteren bir ekran yap. Başlangıç verisi şu olsun:
{ id: 1, ad: "Dış kule", hp: 100 },{ id: 2, ad: "İç kule", hp: 100 },{ id: 3, ad: "İnhibitör kulesi", hp: 100 }- Her kulenin satırında adı ve canı:
Dış kule: 70. Canı 0 olan kuleDış kule: yıkıldıyazsın. - Her satırda bir Vur düğmesi yalnız o kulenin canını 30 düşürsün; can 0'ın altına inmesin. Yıkılan kulenin Vur'u soluk görünsün.
- Başlıkta ayakta kalan kule sayısı:
Ayakta: 3/3. - Altta bir Onar düğmesi, ayakta kalan her kulenin canını 100'e çıkarsın; yıkılan kule yıkık kalsın.
Beklenen: Dış kuleye üç kez vur: 70, 40, 10. Dördüncü vuruş canı -20 değil 0 yapar: Dış kule: yıkıldı, Ayakta: 2/3. İç kuleye bir kez vur: 70. Onar: İç kule 100, Dış kule hâlâ yıkıldı.
İpucu 1
Tek state yeter: kule objelerinden oluşan bir dizi. Başlangıç dizisini birimler gibi bileşenin dışında bir const'ta tut; useState(baslangic) tipini oradan çıkarır.
İpucu 2
Vur, takım kurucudaki yildizArtir'ın kardeşi: map ile yeni dizi kur, kimliği tutan kule için yeni obje ver ({ ...kule, hp: ... }), ötekileri olduğu gibi bırak. Alt sınır için Math.max(0, ...).
İpucu 3
Onar da bir map, ama kimliğe değil cana bakar: kule.hp > 0 ise yeni obje, değilse kule olduğu gibi. Ayakta sayısı bir türetilmiş değer: filter(...).length.
Çözümü göster
import { useState } from "react" type Kule = { id: number ad: string hp: number} const baslangic: Kule[] = [ { id: 1, ad: "Dış kule", hp: 100 }, { id: 2, ad: "İç kule", hp: 100 }, { id: 3, ad: "İnhibitör kulesi", hp: 100 }] function KuleSavunmasi() { const [kuleler, setKuleler] = useState(baslangic) const ayakta = kuleler.filter((kule) => kule.hp > 0).length function vur(id: number) { setKuleler(kuleler.map((kule) => kule.id === id ? { ...kule, hp: Math.max(0, kule.hp - 30) } : kule )) } function onar() { setKuleler(kuleler.map((kule) => kule.hp > 0 ? { ...kule, hp: 100 } : kule )) } return ( <main> <h1>Ayakta: {ayakta}/{kuleler.length}</h1> <ul className="liste"> {kuleler.map((kule) => ( <li key={kule.id}> <span>{kule.ad}: {kule.hp === 0 ? "yıkıldı" : kule.hp}</span> <button onClick={() => vur(kule.id)} disabled={kule.hp === 0}> Vur </button> </li> ))} </ul> <button onClick={onar}>Onar</button> </main> )} export default KuleSavunmasiBeklenen satırların hepsini tıklayarak denedik. Sınırlar: canı 10 olan kuleye vurunca 0 oldu, -20 değil; üç kule de yıkıkken Onar hiçbir şeyi değiştirmedi ve başlık Ayakta: 0/3 kaldı. Her işlem yeni bir dizi kuruyor, değişen kule de yeni bir obje oluyor; baslangic dizisi hiç bozulmuyor.
Skor tablosu: state'i yukarı taşı
Elinde iki takımın kill'ini sayan bir tablo var. Her TakimSkoru kendi kill'ini kendi state'inde tutuyor. Kolaylık olsun diye iki bileşen aynı dosyada:
import { useState } from "react" type TakimSkoruProps = { ad: string} function TakimSkoru({ ad }: TakimSkoruProps) { const [kill, setKill] = useState(0) return ( <section className="panel"> <h2>{ad}: {kill}</h2> <button onClick={() => setKill((onceki) => onceki + 1)}>Kill</button> </section> )} function SkorTablosu() { return ( <main> <h1>Skor tablosu</h1> <div className="sutunlar"> <TakimSkoru ad="Mavi" /> <TakimSkoru ad="Kırmızı" /> </div> </main> )} export default SkorTablosuİstenenler:
- Başlığın altında
Önde: Mavi (+2) · Toplam kill: 2gibi bir satır. Kırmızı öndeyseÖnde: Kırmızı (+1), eşitseBerabere. - Sıfırla düğmesi iki takımı birden 0 yapsın.
- Her takımda bir de Geri al düğmesi kill'i 1 azaltsın; 0'ın altına inmesin, 0'dayken soluk görünsün.
Beklenen: başta Berabere · Toplam kill: 0. Mavi 2 kill: Önde: Mavi (+2) · Toplam kill: 2. Kırmızı 3 kill: Önde: Kırmızı (+1) · Toplam kill: 5. Kırmızı bir geri alma: Berabere · Toplam kill: 4. Sıfırla: Berabere · Toplam kill: 0 ve iki Geri al da soluk.
İpucu 1
SkorTablosu iki takımın kill'ini göremiyor; her biri bir çocuğun içinde kilitli. Sıfırlamak da imkânsız. İki state'i ortak ebeveyne, yani SkorTablosu'na taşı.
İpucu 2
TakimSkoru artık state tutmasın: kill sayısını ve bir değiştirme fonksiyonunu props olarak alsın. onDegistir: (fark: number) => void gibi tek bir fonksiyon hem +1 hem -1 için yeter.
İpucu 3
"Önde kim" bir türetilmiş değer: mavi - kirmizi farkının işaretine bak. Sıfırın altına inmemek için Math.max(0, onceki + fark).
Çözümü göster
import { useState } from "react" type TakimSkoruProps = { ad: string kill: number onDegistir: (fark: number) => void} function TakimSkoru({ ad, kill, onDegistir }: TakimSkoruProps) { return ( <section className="panel"> <h2>{ad}: {kill}</h2> <button onClick={() => onDegistir(1)}>Kill</button> <button onClick={() => onDegistir(-1)} disabled={kill === 0}> Geri al </button> </section> )} function SkorTablosu() { const [mavi, setMavi] = useState(0) const [kirmizi, setKirmizi] = useState(0) const fark = mavi - kirmizi let durum = "Berabere" if (fark > 0) { durum = `Önde: Mavi (+${fark})` } if (fark < 0) { durum = `Önde: Kırmızı (+${-fark})` } function sifirla() { setMavi(0) setKirmizi(0) } return ( <main> <h1>Skor tablosu</h1> <p>{durum} · Toplam kill: {mavi + kirmizi}</p> <button onClick={sifirla}>Sıfırla</button> <div className="sutunlar"> <TakimSkoru ad="Mavi" kill={mavi} onDegistir={(f) => setMavi((onceki) => Math.max(0, onceki + f))} /> <TakimSkoru ad="Kırmızı" kill={kirmizi} onDegistir={(f) => setKirmizi((onceki) => Math.max(0, onceki + f))} /> </div> </main> )} export default SkorTablosuBeklenen satırların hepsini tıklayarak denedik. durum ve toplam kill state değil; her çizimde iki sayıdan hesaplanıyor. TakimSkoru artık yalnız gösteriyor ve haber veriyor; asıl veri ebeveynde, SkorTablosu'nda. Sınır: berabere hem 0-0'da hem 2-2'de doğru çıkıyor, çünkü yalnız farka bakıyoruz.
Kendini sına
Soru 1
kill 0 iken bu fonksiyon bir kez çalışınca ekranda kaç yazar?
function tikla() { setKill(kill + 1) setKill(kill + 1) setKill(kill + 1)}Cevabı göster
1. Üç satır da bu çizimin anlık görüntüsünü okur; kill üçünde de 0'dır. React üç kez "1 yap" emri alır. Üç artış için setKill((k) => k + 1) yazmalısın.
Soru 2
kill 0 iken bu fonksiyon bir kez çalışınca ekranda kaç yazar?
function tikla() { setKill(kill + 5) setKill((k) => k + 1)}Cevabı göster
6. React sırayı izler: ilk satır "5 yap" der; ikinci satır bir güncelleme fonksiyonu olduğu için bir önceki sonucu (5) alır ve 6 döndürür. Biz denedik, ekranda Kill: 6 yazdı.
Soru 3
Hangisi takım state'ini doğru günceller? Ekranı güncelleyip yine de yanlış olan var mı?
// atakim.push(yeni)setTakim(takim) // bsetTakim([...takim, yeni]) // ctakim[0].yildiz = 2setTakim([...takim])Cevabı göster
Doğrusu b. a ekranı güncellemez: React aynı diziyi görür ve çizmez. c ekranı günceller, çünkü dizi yeni; ama ilk birimin objesini yerinde değiştirdi. Yeni dizi eski objeleri paylaşır (Ders 12'deki sığ kopya), yani eski state de bozuldu. Doğrusu map ile o birimin yerine yeni bir obje koymak.
Soru 4
Takım kurucuda bunlardan hangileri state olmalı?
takim dizisi · toplamMaliyet · takımdaki birim sayısı · arama kutusundaki metin · birimler listesi · uyarılar
Cevabı göster
State olanlar: takim ve arama metni. Zamanla değişiyorlar ve başka bir şeyden hesaplanamıyorlar. toplamMaliyet, birim sayısı (takim.length) ve uyarılar takim'den (ve props'tan) hesaplanır: türetilmiş değer. birimler hiç değişmiyor: sıradan bir sabit.
Soru 5
Hangileri düğmeye tıklanınca sat fonksiyonunu çalıştırır, hangileri yanlış?
<button onClick={sat}>Sat</button><button onClick={sat()}>Sat</button><button onClick={() => sat(esya)}>Sat</button><button onClick={sat(esya)}>Sat</button>Cevabı göster
1. ve 3. doğru. 1. fonksiyonu verir (parametresiz sat için). 3. tıklamada sat(esya)'yı çağıran yeni bir fonksiyon verir. 2. ve 4. fonksiyonu çizim sırasında çağırır: tıklamayı beklemezler. sat state değiştiriyorsa Too many re-renders hatası çıkar; TypeScript de onClick'in altını çizer.
Soru 6
Dükkândaki arama metnini sağdaki TakimPaneli de göstermek isteseydi arama state'ini nereye taşırdın?
Cevabı göster
App'e: Dukkan ile TakimPaneli'nin en yakın ortak ebeveyni. App arama'yı ikisine props ile verir; Dukkan'a bir de onAramaDegistir gibi bir fonksiyon geçer. Şu anki yeri doğru, çünkü yalnız Dukkan kullanıyor; state ihtiyaç doğmadan yukarı taşınmaz.
Soru 7
Mesajı oku: sebep ne, nasıl düzelir?
Encountered two children with the same key, `Ahri`. Keys should be unique so that components maintain their identity across updates.Cevabı göster
Bir listede iki öğe aynı key'i (Ahri) almış. Büyük ihtimalle takım listesi key={birim.ad} ile çiziliyor ve Ahri iki kez eklendi. Her takım birimine eklenirken benzersiz bir id ver ve key={birim.id} yaz.
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 takım kurucu açılmalı. Vite şablonu .gitignore'u kendisi getirdi; node_modules ve dist Git'e girmez.
Sıradaki derste bileşen ekrana geldiğinde bir API'den veri çekmeyi öğreneceksin: useEffect, yükleniyor ve hata durumları, bir de effect'in kendi sonsuz döngü tuzağı.