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

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 useState ile tutabilirsin
  • Yeni değer eskisinden hesaplanıyorsa güncelleme fonksiyonu yazıp eski değer tuzağından kaçınabilirsin
  • onClick, onChange ve onSubmit ile tıklamaya, yazmaya ve form gönderimine tepki veren kontrollü girdiler kurabilirsin
  • Dizi ve obje state'ini ..., map ve filter ile 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 ve onChange'siz value uyarısını tanıyıp düzeltebilirsin
Bu sayfada
  1. Yeni proje: tft-kurucu
  2. Düz değişken neden yetmez?
  3. useState: React'in hatırladığı değer
  4. Güncelleme fonksiyonu ve eski değer tuzağı
  5. Olaylara tepki vermek
  6. Dizi state'i: takıma birim eklemek
  7. Kontrollü girdi: arama kutusu
  8. State'i ortak ebeveyne taşımak
  9. Türetilmiş değerler
  10. Uygulamanın tamamı
  11. Sık hatalar
  12. Görevler
  13. Kendini sına
  14. 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:

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

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

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

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

src/App.tsx
  const [count, setCount] = useState(0)
src/App.tsx
          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.css dosyasını ve src/assets klasö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.
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;} 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:

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

onClick={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:

localhost:5173
Koyu zeminde Altın: 0 başlığı ve Altın topla düğmesi.
Üç tıklamadan sonra: ekranda hâlâ Altın: 0 yazıyor.
Console (F12) · üç tıklamadan sonra
altin: 1altin: 2altin: 3

Değişken gerçekten 3 oldu, Console bunu söylüyor. Ama ekranda hâlâ Altın: 0 var. İki ayrı sebep var:

  1. 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.
  2. 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 = 0 satı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:

src/App.tsx
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
localhost:5173
Koyu zeminde Altın: 3 başlığı ve Altın topla düğmesi.
useState ile, üç tıklamadan sonra: Altın: 3.

Üç satır değişti:

  • import { useState } from "react": React paketinden useState fonksiyonunu al (Ders 14'teki adlı içe aktarma).
  • const [altin, setAltin] = useState(0): React'ten bir hafıza kutusu iste, ilk değeri 0 olsun. useState iki 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, gelenek x ve setX.
  • 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:

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

Console (F12) · üç tıklamadan sonra
duz: 1duz: 1duz: 1

Ekran üç 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.

1. çizim sayfa açılınca App() ilk kez çağrılır let duz = 0 duz: 0 useState(0) 0 döner (ilk değer) Ekran: Altın: 0 setAltin(1) React yeniden çizer 2. çizim tıklamadan sonra App() yeniden çağrılır let duz = 0 duz yine 0 useState(0) 1 döner: React hatırladı Ekran: Altın: 1
Şekil 41.1 Her çizim 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:

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

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

src/App.tsx
    function turGec() {        setAltin(altin + 1)        console.log("altin:", altin)    }
Console (F12) · üç tıklamadan sonra; ekranda Altın: 3
altin: 0altin: 1altin: 2

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

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

Değer vermek Bu çizimde altin: 0 setAltin(altin + 1) yani setAltin(1): 1 yap setAltin(altin + 1) yine setAltin(1): 1 yap Sonuç: 1 Güncelleme fonksiyonu React sırayla uygular: (onceki) => onceki + 1 onceki: 0, döner: 1 (onceki) => onceki + 1 onceki: 1, döner: 2 Sonuç: 2
Şekil 41.2 Değer verince iki çağrı da aynı anlık görüntüyü okur. Güncelleme fonksiyonu verince React ikisini zincirler.

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:

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

onClick={() => 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:

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

src/App.tsx✗ Yanlış
            <h1>Altın: {altin}</h1>            <button onClick={altinEkle(1)}>+1 altın</button>            <button onClick={altinEkle(5)}>+5 altın</button>
Console (F12)
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:

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

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

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

Yeni olan birkaç parça:

  • type Birim içe aktarırken başına type yazdı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 kutuda TakimBirimi dizisi duracak". Ders 39'da gördüğün Array<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'teki Date.now()'u kullandık.

Üç farklı birimin Ekle düğmesine bas. Console:

Console (F12) · üç Ekle'den sonra
takimda 1 birim vartakimda 2 birim vartakimda 3 birim var

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

ders41.js
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)
Çıktı
true 2false 2 3

push'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ı dizi takim.push(jinx) setTakim(takim) eski yeni [Lux, Jinx] Aynı dizi: React çizmez Yayma: yeni dizi setTakim([...takim, jinx]) eski yeni [Lux] [Lux, Jinx] Yeni dizi: yeniden çizer
Şekil 41.3 React yeni state'i eskisiyle "aynı kutu mu?" diye karşılaştırır. 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ı:

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

localhost:5173
İki sütun. Solda Dükkân: sekiz birim, her birinin maliyeti ve Ekle düğmesi. Sağda Takım (3): Lux iki yıldız, Jinx bir yıldız, Ahri bir yıldız; her satırda Yıldız ve Çıkar düğmeleri.
Takımda Lux iki yıldızlı, Jinx ve Ahri tek yıldızlı.

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:

ders41.js
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])
Çıktı
[ { id: 1, ad: 'Lux', yildiz: 1 }, { id: 2, ad: 'Jinx', yildiz: 2 } ]1true false

Eski takımdaki Jinx hâlâ 1 yıldızlı; eski state bozulmadı. Bu tablo hangi işlemi nasıl yazacağını özetliyor:

İşlemYerinde değiştiren (state'te yasak)Kopya kuran (doğru)
Sona ekletakim.push(yeni)[...takim, yeni]
Çıkartakim.splice(i, 1)takim.filter((b) => b.id !== id)
Bir elemanı değiştirtakim[i].yildiz = 2map içinde { ...b, yildiz: 2 }
Sıralatakim.sort(...)takim.toSorted(...)
Objenin bir alanıoyuncu.altin = 50{ ...oyuncu, altin: 50 }
Sık hatapush sonrası ekran güncellenmiyor

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

TSX
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."

Sık hataEncountered two children with the same key

Ne zaman çıkar? Listede iki öğeye aynı key verince. Takım listesinde key={birim.ad} yazıp Garen'i iki kez eklersen:

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

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

localhost:5173
Takım (2): Garen satırının kutusunda ön sıra yazıyor, Lux satırının kutusu boş.
Çıkar'dan önce: not Garen'in satırında.
localhost:5173
Takım (1): yalnız Lux kaldı ve onun kutusunda ön sıra yazıyor.
Garen'i çıkardıktan sonra: not Lux'a geçti.

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:

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

src/App.tsx
    function aramaGonder(olay: React.SubmitEvent<HTMLFormElement>) {        olay.preventDefault()        if (gorunenler.length === 0) {            return        }        ekle(gorunenler[0])        setArama("")    }
  • Formun içinde Enter'a basmak submit olayını başlatır. Ders 23'teki gibi preventDefault() 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'teki onSubmit'in üstüne getirince SubmitEventHandler<HTMLFormElement> yazar: SubmitEvent alan 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:

src/App.tsx
                    <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.target kutunun kendisi, value iç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:

localhost:5173
Solda Dükkân: arama kutusunda an yazıyor, altında yalnız Annie 2 altın ve Ekle düğmesi. Sağda Takım (1): Teemo bir yıldız.
Kutuda an yazıyor; dükkânda yalnız Annie kaldı. Enter'a basılırsa Annie eklenir ve kutu boşalır.

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.

Sık hataYou provided a value prop to a form field without an onChange handler.

Ne zaman çıkar? Kutuya value verip onChange vermeyince.

src/App.tsx✗ Yanlış
                    <form onSubmit={aramaGonder}>                        <input                            placeholder="Birim ara, Enter ile ekle"                            value={arama}                        />                    </form>
Console (F12)
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:

src/App.tsx
    const [altin, setAltin] = useState(20)

<h1>'in hemen altına:

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

Sık hataArgument 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)}.

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

App state: takim, altin, seviye fonksiyonlar: ekle, cikar, yildizArtir onEkle(birim) onCikar(id) Dukkan kendi state'i: arama props: onEkle TakimPaneli props: takim, altin, seviye, onCikar, onYildiz props: veri aşağı iner çağrı: haber yukarı çıkar
Şekil 41.4 Uygulamanın son hâli: 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:

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

onEkle: (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:

src/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 TakimPaneli

App.tsx artık kısa. arama, gorunenler ve aramaGonder'i sildik; takim, altin ve onları değiştiren fonksiyonlar kaldı:

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

Sayfa ö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:

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

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

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

App.tsx: toplamı ayrı bir state'te tutmak✗ Yanlış
    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.

localhost:5173
Üstte Altın 20 ve Seviye 4. Sağda Takım (1): Lux iki yıldız. Altında Toplam maliyet: 7 altın yazıyor; doğrusu 9 olmalıydı.
Takımda yalnız iki yıldızlı Lux var (3 × 3 = 9 altın), toplam ise 7 altın diyor.

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

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

  1. Props ile mi geliyor? O zaman state değil.
  2. Hiç değişmiyor mu (birimler listesi gibi)? O zaman state değil, sıradan bir sabit.
  3. 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:

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

TakimPaneli.tsx'in son hâli:

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

Lux, Jinx, Ahri, Aurelion Sol ve Garen'i ekleyip Lux'u iki yıldıza çıkardık ve seviyeyi bir düşürdük:

localhost:5173
Üstte Altın 20, Seviye 3, eksi ve artı düğmeleri. Solda Dükkân listesi. Sağda Takım (5/3): Lux iki yıldız, Jinx, Ahri, Aurelion Sol ve Garen birer yıldız. Toplam maliyet: 22 altın. Altında iki kırmızı uyarı: Seviye 3: tahtaya en fazla 3 birim sığar. Altının yetmiyor: 2 altın eksik.
Seviye 3'te beş birim ve 20 altınla 22 altınlık takım: iki uyarı birden.

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 xyz yazı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:

Sık hataEski değer tuzağı: iki çağrı, bir artış

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.

Sık hataA 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:

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

Nası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ç.

Görev 1

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

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

Görev 2

Sihirdar adı

Kontrollü bir yazı kutusu kur:

  • Kutunun altında 7/16 gibi 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 varken Merhaba, 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
src/Gorev2.tsx
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 SihirdarAdi

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

Görev 3

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ıç varken KILIÇ 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

"Zaten var mı?", "dolu mu?", "eklenebilir mi?" üçü de türetilmiş değer. some (Ders 11) ile karşılaştır. Türkçe harflerde toLowerCase yanılır; Ders 9'daki toLocaleLowerCase("tr")'yi kullan.

İ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
src/Gorev3.tsx
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 Envanter

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

Görev 4

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.

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

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

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

Görev 5

Kule savunması

Üç kulenin canını gösteren bir ekran yap. Başlangıç verisi şu olsun:

TypeScript
{ 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 kule Dış 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
src/Gorev5.tsx
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 KuleSavunmasi

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

Görev 6

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:

src/Gorev6.tsx
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: 2 gibi bir satır. Kırmızı öndeyse Önde: Kırmızı (+1), eşitse Berabere.
  • 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
src/Gorev6.tsx
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 SkorTablosu

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

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

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

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

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

Hata çıktısı
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ğı.

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