State'i Korumak ve Sıfırlamak
State bileşenler arasında izole edilmiştir. React, kullanıcı arayüzü (UI) ağacındaki yerlerine göre hangi state’in hangi bileşene ait olduğunu takip eder. Yeniden render’lar arasında state’in ne zaman korunacağını ve ne zaman sıfırlanacağını kontrol edebilirsiniz.
Bunları öğreneceksiniz
- React bileşen yapılarını nasıl “görür”
- React state’i korumayı ya da sıfırlamaya ne zaman seçer
- React bileşenin state’ini sıfırlamaya nasıl zorlanır
- Anahtarlar ve tipler state’in korunup korunmamasını nasıl etkiler
Kullanıcı arayüzü (UI) ağacı
Tarayıcılar, kullanıcı arayüzünü modellemek için pek çok ağaç yapısı kullanırlar. DOM HTML elementlerini temsil eder, CSSOM aynı şeyi CSS için yapar. Bir Erişilebilirlik ağacı bile var!
React, oluşturduğunuz kullanıcı arayüzünü yönetmek ve modellek için de ağaç yapılarını kullanır. React, JSX’inizden kullanıcı arayüzü ağaçları oluşturur. Ardından React DOM, tarayıcı DOM elementlerini güncelleyerek kullanıcı arayüzü ağacı ile eşleşmesini sağlar. (React Native bu ağaçları mobil platformlara özgü elementlere çevirir.)
State ağaçtaki bir konuma bağlıdır
React arayüzünüzdeki bileşen yapısı için render ağaçları inşa eder.
Bir bileşene state verdiğinizde, state’in bileşen içinde “yaşadığını” düşünebilirsiniz. Aslında state, React içinde tutulur. React tuttuğu her bir state parçasını, bileşenin kullanıcı arayüzü ağacında bulunduğu yere göre doğru bileşenle ilişkilendirir.
Örneğin burada yalnızca bir <Counter />
JSX elemanı vardır, ancak bu eleman iki farklı konumda render edilir:
import { useState } from 'react'; export default function App() { const counter = <Counter />; return ( <div> {counter} {counter} </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Bir ekle </button> </div> ); }
Bunlar ağaç olarak nasıl görünüyor:
Bu sayaçlar iki farklı sayaçtır çünkü her ikisi de ağaçta kendi konumunda render edilir. Genellikle React’i kullanmak için bu konumları düşünmeniz gerekmez, ancak çalışma mantığını anlamak faydalı olabilir.
React’te, ekrandaki her bileşenin içindeki state’ler izole bir şekildedir. Örneğin, iki Counter
bileşenini yan yana render ederseniz, her birinin kendi bağımsız score
ve hover
state’leri olacaktır.
Her iki sayaca da tıklayın ve birbirlerini etkilemediklerini görün:
import { useState } from 'react'; export default function App() { return ( <div> <Counter /> <Counter /> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Bir ekle </button> </div> ); }
Gördüğünüz gibi, bir sayaç güncellendiği zaman sadece o bileşenin state’i güncellenmektedir:
React, aynı bileşeni aynı konumda render ettiğiniz sürece state’i koruyacaktır. Bunu görmek için her iki sayacı da artırın, ardından “İkinci sayacı render et” kutucuğunun işaretini kaldırarak ikinci bileşeni render etmeyi bırakın ve ardından kutucuğu tekrar işaretleyerek bileşeni yeniden render edin:
import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return ( <div> <Counter /> {showB && <Counter />} <label> <input type="checkbox" checked={showB} onChange={e => { setShowB(e.target.checked) }} /> İkinci sayacı render et </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Bir ekle </button> </div> ); }
İkinci sayacı render etmeyi bıraktığınız anda state’in nasıl tamamen kaybolduğuna dikkat edin. Bunun nedeni, React’in bir bileşeni render etmeyi bıraktığı zaman o bileşenin state’ini yok etmesidir.
”İkinci sayacı render et” kutucuğunu işaretlediğinizde, ikinci bir Counter
bileşeni ve state’i sıfırdan oluşturulur (score = 0
) ve DOM’a eklenir.
React, aynı bileşeni kullanıcı arayüzü ağacında aynı konumda render ettiğiniz sürece state’i koruyacaktır. Bileşen kaldırılırsa ya da aynı konumda başka bir bileşen render edilirse, React state’i yok edecektir.
Aynı konumdaki aynı bileşen state’i korur
Bu örnekte iki farklı <Counter />
elemanı var:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Süslü (fancy) stili kullan </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Bir ekle </button> </div> ); }
Kutucuğu işaretlediğinizde veya işareti kaldırdığınızda, sayacın state’i sıfırlanmaz. isFancy
ister true
ister false
olsun, kök App
bileşeninden döndürülen div
’in ilk alt elemanı her zaman bir <Counter />
bileşenidir:
Bu bileşen, aynı konumdaki aynı bileşendir. Bu nedenle React’in bakış açısından aynı sayaçtır.
Aynı konumdaki farklı bileşenler state’i sıfırlar
Bu örnekte kutucuğa tıkalamak, <Counter>
bileşenini <p>
ile değiştirecektir:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>Sonra görüşürüz!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Mola ver </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Bir ekle </button> </div> ); }
Burada, aynı konumda farklı bileşen tipleri arasında geçiş yapmaktayız. Başlangıçta, <div>
elementinin ilk alt elemanı bir Counter
içermekteydi. Ancak bunu bir p
ile değiştirdiğinizde React, Counter
’ı kullanıcı arayüzü ağacından kaldırır ve state’ini yok eder.
Aynı zamanda, aynı konumda farklı bir bileşen render ettiğinizde, tüm alt ağacın (subtree) state’ini sıfırlar. Nasıl çalıştığını görmek için sayacı artırın ve kutucuğu işaretleyin:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Süslü (fancy) stili kullan </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Bir ekle </button> </div> ); }
Sayaç state’i kutucuğa tıkladığınız zaman sıfırlanır. Counter
render etmenize rağmen, div
’in ilk alt elemanı div
’den section
’a dönüşür. Alt eleman olan div
DOM’dan kaldırıldığında, altındaki ağacın tamamı da (Counter
ve state’i de dahil olmak üzere) yok edilir.
Genel bir kural olarak, yeniden render’lar arasında state’i korumak istiyorsanız, ağacınızın yapısının render’lar arasında “eşleşmesi” gerekmektedir. Eğer yapı farklıysa, state yok edilecektir çünkü React bileşeni ağaçtan çıkardığında o bileşenin state’ini yok eder.
Aynı konumda state’i sıfırlamak
Varsayılan olarak React, aynı konumda kalan bir bileşenin state’ini korur. Genellikle istediğimiz davranış budur ve bu yüzden varsayılan olarak böyle davranmaktadır. Ancak bazen bir bileşenin state’ini sıfırlamak isteyebilirsiniz. İki oyuncunun her turdaki puanlarını takip etmesine izin veren bu uygulamayı ele alalım:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Taylor" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Sonraki oyuncu! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'ın skoru: {score}</h1> <button onClick={() => setScore(score + 1)}> Bir ekle </button> </div> ); }
Şu anda, oyuncuyu değiştirdiğinizde state korunmaktadır. Her iki Counter
bileşeni de aynı konumdadır ve bu yüzden React bunları, person
prop’u değişmiş aynı Counter
bileşeni olarak görür.
Ancak konsept olarak bu uygulamada iki farklı sayaç olmalıdır. Kullanıcı arayüzünde aynı konumda görünebilirler ama bir sayaç Taylor için diğer sayaç da Sarah için olmalıdır.
İki sayaç arasında geçiş yaparken state’i sıfırlamanın iki yolu vardır:
- Bileşenleri farklı konumlarda render edin
- Her bileşene bir
key
(anahtar
) prop’u verin
1. Seçenek: Bileşeni farklı bir konumda render etmek
Eğer bu iki Counter
bileşeninin bağımsız olmasını istiyorsanız, iki bileşeni farklı konumda render edebilirsiniz:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Taylor" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Sonraki oyuncu! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'ın skoru: {score}</h1> <button onClick={() => setScore(score + 1)}> Bir ekle </button> </div> ); }
- Başlangıçta
isPlayerA
state’itrue
’dur. Yani ilk konumCounter
state’ini içerir ve ikincisi boştur. - ”Sonraki oyuncu” butonuna tıkladığınzda ilk konum temizlenir ancak şimdi ikinci konum
Counter
’ı içerir.
Counter
bileşeni DOM’dan her silindiğinde state’i de yok edilir. Bu yüzden butona her tıkladığınızda sıfırlanırlar.
Bu çözüm, aynı konumda render edilen birkaç bağımsız bileşeniniz olduğunda kullanışlıdır. Bu örnekte yalnızca iki bileşeniniz var bu yüzden ikisini de JSX’te ayrı ayrı render etmek zor değildir.
2. Seçenek: State’i anahtar ile sıfırlamak
Bir bileşenin state’ini sıfırlamanın daha genel başka bir yolu da vardır.
Listeleri Render Etmek sayfasında anahtar
kullanımını görmüş olabilirsiniz. Anahtarlar sadece listeler için değildir! React’in herhangi bir bileşeni ayırt etmesini sağlamak için de anahtarları kullanabilirsiniz. Varsayılan olarak React, bileşenleri ayırt etmek için üst elemandaki sırayı (“ilk sayaç”, “ikinci sayaç”) kullanır. Ancak anahtarlar, React’e bunun yalnızca ilk sayaç veya ikinci sayaç değil de belirli bir sayaç olduğunu, örneğin Taylor’ın sayacı olduğunu söylemenizi sağlar. Bu şekilde React, ağaçta nerede olursa olsun Taylor’ın sayacı olduğunu bilecektir!
Bu örnekte, iki <Counter />
bileşeni JSX’te aynı yerde olsalar bile aynı state’i paylaşmamaktadırlar.
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Sonraki oyuncu! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'ın skoru: {score}</h1> <button onClick={() => setScore(score + 1)}> Bir ekle </button> </div> ); }
Taylor ve Sarah arasında geçiş yapmak state’i korumamaktadır. Çünkü onlara farklı key’ler (anahtar) verdiniz:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
key
(anahtar
) belirtmek, React’e üst elemandaki sıraları yerine key
’in kendisini konum olarak kullanmasını söyler. Bu nedenle, bileşenleri JSX’te aynı yerde render etseniz bile React onları iki farklı sayaç olarak görecektir ve state’lerini asla paylaşmayacaklardır. Bir sayaç ekranda göründüğü her sefer state’i oluşturulur. Sayaç her silindiğinde ise state’i yok edilir. Aralarında geçiş yapmak, state’lerini tekrar tekrar sıfırlar.
Formu anahtar ile sıfırlamak
State’i anahtar ile sıfırlamak formlarla uğraşırken çok kullanışlıdır.
Bu sohbet uygulamasında, <Chat>
bileşeni mesaj input state’ini içermektedir:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Input’a bir şey yazmayı deneyin ve ardından farklı bir alıcı seçmek için “Alice” veya “Bob” butonuna tıklayın. <Chat>
bileşeni ağaçta aynı konumda render edildiği için input state’inin korunduğunu göreceksiniz.
Bir çok uygulamada bu istenen davranış olabilir ancak bu uygulamada değil! Kullanıcının zaten yazdığı bir mesajı yanlış bir tıklama nedeniyle yanlış bir kişiye göndermesine izin vermek istemezsiniz. Bunu düzeltmek için key
(anahtar
) prop’u ekleyin:
<Chat key={to.id} contact={to} />
Bu, farklı bir alıcı seçtiğinizde Chat
bileşeninin, altındaki ağaçtaki herhangi bir state de dahil olmak üzere sıfırdan yeniden oluşturulmasını sağlar. React ayrıca DOM elementlerini tekrar kullanmak yerine yeniden oluşturur.
Şimdi alıcıyı değiştirmek yazılan mesajı temizleyecektir:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.id} contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Derinlemesine İnceleme
Gerçek bir sohbet uygulamasında, kullanıcı önceki alıcıyı tekrar seçtiğinde input state’ini kurtarmak istersiniz. Artık görünmeyen bir bileşenin state’ini “canlı” tutmanın birkaç yolu vardır:
- Yalnızca geçerli sohbeti göstermek yerine tüm sohbetleri render edebilir ve kullanmadıklarınızı CSS ile saklayabilirsiniz. Sohbetler ağaçtan silinmezler yani lokal state’leri korunmuş olur. Basit kullanıcı arayüzleri için iyi çalışan bir çözümdür. Ancak gizli ağaçlar büyükse ve çok sayıda DOM node’u içeriyorsa uygulamayı çok yavaşlatabilir.
- State’i yukarı kaldırabilir ve her alıcı için bekleyen mesajı üst bileşeninde tutabilirsiniz. Bu şekilde, alt bileşenlerin silinmesi önemli değildir çünkü önemli bilgileri tutan üst bileşendir. Bu en çok kullanılan çözümdür.
- React state’ine ek olarak başka bir kaynak da kullanabilirsiniz. Örneğin, kullanıcı yanlışlıkla sayfayı kapatsa bile mesaj taslağının korunmasını isteyebilirsiniz. Bunu yapmak için,
Chat
bileşeninin state’inilocalStorage'dan
okuyabilir ve taslakları da oraya kaydedebilirsiniz.
Hangi stratejiyi seçerseniz seçin, Alice ile sohbet, Bob ile sohbetten kavramsal olarak farklıdır. Bu nedele, <Chat>
ağacına mevcut alıcıya göre bir key
(anahtar
) vermek mantıklıdır.
Özet
- React, aynı bileşen aynı konumda render edildiği sürece state’i koruyacaktır.
- State, JSX elemanlarında tutulmaz. State, JSX’i koyduğunuz ağaç konumu ile alakalıdır.
- Bir alt ağaca farklı bir anahtar vererek state’ini sıfırlamaya zorlayabilirsiniz.
- Bileşen tanımlarını iç içe yapmayın, aksi takdirde yanlışlıkla state’i sıfırlarsınız.
Problem 1 / 5: Kaybolan input metnini düzeltin
Bu örnek butona tıkladığınız zaman bir mesaj göstermektedir. Ancak, butona tıklamak aynı zamanda input’u da sıfırlamaktadır. Bu niye olmakta? Butona tıklamanın input metnini sıfırlamayacağı şekilde düzeltin.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>İpucu: Favori şehriniz?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>İpucunu gizle</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>İpucunu göster</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }