Önemli: Diğer yazılarımla direkt bağlantılı bir yazıdır. İlk olarak bu yazıdan okumaya başladıysanız, eğitim serisine kısaca bir göz atmanızı tavsiye ederim.
Merhabalar.
Bir önceki yazımda spread ile state kontrolünü ve değişkenlerin bir önceki değerlerini alıp kullanmayı görmüştük. Ama React artık daha performanslı kullanımlar için prevState kullanımını öneriyor.
Bu yazımda Calculator uygulamamıza yeni eklemeler yaparak bu özelliği kullanmayı öğreneceğiz.
Şöyle bir özellik istiyorum. Calculator açık olduğu sürece yaptığım her işlem bir listede hafızada tutulsun istiyorum. Hesap makinesinin üstünde bir bölüme bunu konumlamak ve bir toggle butonla açılır kapanır duruma getirmek istiyorum. Tabi işlemlerim sırasında kullanıcıya vermek istediğim mesajlar için sayfanın üstünde çıkacak şekilde bir alertMessage bölümü oluşturmak istiyorum.
Öncelikle görselimizi oluşturalım.
Calculator.css dosyamıza aşağıdaki kodları ekleyelim.
.operation-info {
display: none;
}
.operation-info.open {
display: block;
}
.operation-info p {
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 10px;
transition: background-color 0.3s ease, color 0.3s ease;
cursor:pointer;
border-radius: 20px;
}
.operation-info p:hover {
background: rgb(77,83,173);
background: radial-gradient(circle, rgba(77,83,173,1) 0%, rgba(20,116,228,1) 100%);
color: white;
}
.info-container {
margin-top: 20px;
border-radius: 20px;
padding: 20px;
border: 1px solid #ddd;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.15);
height: 300px; /* adjust as needed */
overflow-x: auto;
overflow-y: auto;
}
.toggle-button {
width: 60px;
height: 35px;
position: relative;
background: #4caf50;
border-radius: 15px;
cursor: pointer;
transition: background 5s ease-in;
}
.toggle-button::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 26px;
height: 26px;
background: rgb(255, 255, 255);
border-radius: 50%;
transition: left 0.3s ease;
}
.toggle-button.active {
background: rgb(64, 89, 202);
}
.toggle-button.active::after {
left: 28px;
}
.mr{
margin-right: 10px;
}
.toggle-container{
display: flex;
justify-content: center;
align-items: center;
margin-top: 30px;
}
.header {
margin: 20px;
height: 50px;
width: 98%;
position: fixed;
top: 0;
left: 0;
background: rgb(214, 7, 18);
color: white;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
transition: all 2s ease;
}
Jsx kodlarımızın içine en üstten olacak şekilde className’i app olan div’in içine aşağıdaki kodları ekleyelim.
{allState.alertMessage ?
<div className="header">
<div className="alert">{allState.alertMessage}</div>
</div> : null
}
<div className={`operation-info info-container ${allState.operationInfoOpen ? 'open' : ''}`}>
<h4>Operation History</h4>
{history.map((process, index) => (
<p key={index}>{process.count}: {process.num1} {process.operation} {process.num2} = {process.result}</p>
))}
</div>
<div className="toggle-container">
<span className="mr">{allState.operationInfoOpen ? 'Close' : 'Open'} History</span>
<button className={`toggle-button ${allState.operationInfoOpen ? 'active' : ''}`} onClick={handleToggleOperationInfo}>
</button>
</div>
Böylece aşağıdaki görüntüyü oluşturmuş olduk.
History’i açıp kapatacak bir toggle butonumuz var.
Bu butona tıklandığında, history isimli list boşsa yukarda header bölgesinde açılacak bir message bölümümüz olacak. Bu mesaj açıldıktan 5 saniye sonra kapatılacak şekilde kodlarını geliştireceğiz.
Message Bölümü
Message bölümünün açılıp kapanması için gerekli state’imizi ve history bölümünün açılıp kapanması için gerekli state’imizi allState içine ekliyoruz.
operationInfoOpen history bölümünün değişkeni, ilk değeri false çünkü başlangıçta bölümün kapalı gelmesini istiyorum.
message bölümündeki yazılacak mesajın değişkeni alertMessage olacak ve ilk değeri boş string. Bu değer dolu olduğunda ilgili message bölümü açılacak ve içindeki mesaj o bölümde gösterilecek.
Yeni State’lerin Eklenmesi
const [allState, setAllState] = useState({
result: 0,
count: 0,
operationInfoOpen: false,
alertMessage: '',
});
History’de amacım şu; 5 + 5 = 10 işlemi yapıldığında her dizi elemanı bir count değişkeni ile bunun kaçıncı işlem olduğunu ve bir operation değişkeniyle 5 + 5 = 10 şeklindeki işlem bilgisini string olarak tutsun istiyorum. Bunun için bir history state’i oluşturuyorum.
const [history, setHistory] = useState([]);
State’leri Güncelleme Fonksiyonları
Bu alanların her birinin güncelleme fonksiyonlarını yazalım. Çünkü bunları çok fazla yer kullanacağız eğer fonksiyon şeklinde yazmasak çok fazla kod tekrarına düşeceğiz ve kodlarımızın okunurluğu da çok azalacak.
const updateAlertMessage = (message) => {
setAllState(prevState => ({
...prevState,
alertMessage: message,
}));
}
const updateResult = (result) => {
setAllState(prevState => ({
...prevState,
result: result,
count: prevState.count + 1,
}));
}
const updateOperationInfoOpen = (isOpen) => {
setAllState(prevState => ({
...prevState,
operationInfoOpen: isOpen,
}));
}
const updateHistory = (operation, num1, num2, result) => {
setHistory(prevList => [...prevList, { count: prevList.length + 1, operation: operation, num1: num1, num2: num2, result: result }]);
}
prevState’i Anlayalım
Tam burası prevState değişkenini anlama zamanı.
Bir tane fonksiyon üzerinden ele alıp inceleyelim.
const updateResult = (result) => {
setAllState(prevState => ({
...prevState,
result: result,
count: prevState.count + 1,
}));
}
Bu fonksiyon, bir önceki state (prevState
) referansını kullanarak yeni bir state oluşturur ve bu yeni state’i setAllState
fonksiyonuyla ana state’e atar. Burada kullanılan ...prevState
ifadesi, önceki state’in tüm özelliklerini (property) kopyalamak için spread operatörünü kullanır.
Yeni state, result
ve count
özelliklerini içerir. result
özelliği, fonksiyona parametre olarak geçirilen result
değeri ile güncellenir. count
özelliği ise bir arttırılarak güncellenir. Bu şekilde, fonksiyonun çağrıldığı her seferinde result
güncellenir ve count
bir artar.
Bu yorumlamaya göre diğer fonksiyonlara göz atabilirsiniz.
Toggle Operasyonu
Toggle operasyonunu gerçekleştirecek fonksiyona bakalım.
Aşağıdaki kodlar toggle butonuna tıklandığında, öncelikle history list’te bir değer var mı diye kontrol eder. Eğer yoksa alertMessage state’i yazılacak mesaj bilgisi ile güncellenir.
alertMessage artık dolu olduğu için üstte bu mesajı görürüz.
setTimeOut fonksiyonu sayesinde 5 saniye sonra alertMessage state’i yine boş stringe set edilir. Böylece mesaj bölümü kapanır. Yani mesaj ekranda 5 sn kalacak şekilde ayarlanmıştır. Bunu istediğiniz gibi güncelleyebilirsiniz.
const handleToggleOperationInfo = () => {
if (history.length !== 0) {
updateOperationInfoOpen(!allState.operationInfoOpen);
}
else {
updateAlertMessage('There is no operation history');
setTimeout(() => updateAlertMessage(''), 5000);
}
};
Calculate Operasyonu
Calculate işlemini yapacak fonksiyonumuzla devam edelim.
const handleCalculate = (event) => {
event.preventDefault();
const form = event.target;
const num1 = parseFloat(form.elements.num1.value);
const num2 = parseFloat(form.elements.num2.value);
const operation = form.elements.operation.value;
let result;
switch (operation) {
case 'add':
result = num1 + num2;
updateHistory('+', num1, num2, result);
break;
case 'subtract':
result = num1 - num2;
updateHistory('-', num1, num2, result);
break;
case 'multiply':
result = num1 * num2;
updateHistory('*', num1, num2, result);
break;
case 'divide':
if (num2 !== 0) {
result = num1 / num2;
updateHistory('/', num1, num2, result);
} else {
updateAlertMessage('Cannot divide by zero');
setTimeout(() => updateAlertMessage(''), 5000);
}
break;
default:
updateAlertMessage('Invalid operation');
setTimeout(() => updateAlertMessage(''), 5000);
}
updateResult(result);
};
Toplama işlemini ele alarak olayı inceleyelim.
case 'add':
result = num1 + num2;
updateHistory('+', num1, num2, result);
break;
Bir işlem add ile geliyorsa öncelikle toplama işlemi yapılır ve result oluşturulur. Sonra burada history listesine yeni bir kayıt ekleyebilmek için updateHistory fonksiyonu çağırılır. + , num1, num2 ve result parametreleri gönderilir.
Update History Fonksiyonu – Bir Kere Daha prevState
updateHistory fonksiyonuna göz atalım.
const updateHistory = (operation, num1, num2, result) => {
setHistory(prevList => [...prevList, { count: prevList.length + 1, operation: operation, num1: num1, num2: num2, result: result }]);
}
prevList ile bir önceki listenin tamamı alınır.
Burada şunu görmüş oluyoruz. prevState kavramı durumu anlatabilmek için kullanılan bir default değişken ismi. Siz listenin önceki değerlerini almak için oraya ister prevState yazarsınız ister prevList isterseniz adınızı yazın. Fonksiyon listenin geçmişteki elemanlarını buraya yazdığınız değişken içine koyacaktır.
Sonra bunun üzerine yeni bir obje daha eklenir. Yeni objenin değişkenlerinden biri count, işlem sayısını tutuyor. Diğeri operation ise yapılan işlemin ne olduğunu tutuyor. Ama string’in nasıl oluşacağını ben belirliyorum.
Burada key değerleri ile gönderdiğimiz değişkenler aynı olduğu için yukardaki fonksiyonu şu şekilde yazsak da sorun çıkmazdı. Bunu her zaman kullanabilirsiniz.
const updateHistory = (operation, num1, num2, result) => {
setHistory(prevList => [...prevList, { count: prevList.length + 1, operation, num1, num2, result }]);
}
prevState’i Liste Sonunda Çağırmak
Yeni eklenen değer listenin hep başına gelsin isterseniz …prevList’i objenin sonuna yazabilirsiniz.
const updateHistory = (operation, num1, num2, result) => {
setHistory(prevList => [{count: prevList.length + 1, operation, num1, num2, result }, ...prevList]);
}
Böylece içerde dizinin son eklenen elemanı en üste gelsin diye yaptığımız reverse() fonksiyonuna ihtiyacınız kalmazdı.
handleCalculate fonksiyonu içinde son olarak aşağıdaki kodla result state’i güncellenir.
updateResult(result);
updateResult Fonksiyonu
Result’ı güncelleyen fonksiyona bakalım.
Kendisine sadece result değerini gönderiyorum ve fonksiyon allState içinde result ve count değerlerini güncelliyor.
…prevState sayesinde burada güncellenmesi gerekmeyen operationInfoOpen ve alertMessage değişkenlerinin mevcut değerleri ne ise öyle alınmasını sağladık.
Eğer bunları almaz iseniz artık allState değişkeni içerisinde sadece result ve count değişkenleri kalmış olurdu.
const updateResult = (result) => {
setAllState(prevState => ({
...prevState,
result: result,
count: prevState.count + 1,
}));
}
JSX İçindeki Değişiklikler
Son olarak JSX olarak eklediğimiz kodları bir gözden geçirelim.
Burada yaptığımız şu alertMessage değişkeninin içeriğine bakılıyor eğer doluysa boş string değilse header bölümünde mesaj alanı açılıyor ve alertMessage’ın tuttuğu değer bu div içinde gösteriliyor.
{allState.alertMessage ?
<div className="header">
<div className="alert">{allState.alertMessage}</div>
</div> : null
}
Toogle Butonu
Burada butona her tıklandığında handleToggleOperationInfo fonksiyonu çalışır ve operationInfoOpen değişkenin değeri o an ne ise değiline set eder.
Yani bu şekilde açıkken bastığımızda kapalı duruma getirir, kapalı iken bastığımızda açık duruma getirebiliriz. Tabii burada history bölümünün açılabilmesi için fonksiyon içinde history listesinin içinde eleman olup olmadığı kontrol edilir. Eğer varsa açılır yoksa yukarda bir mesaj belirir ve alan açılmaz.
<div className="toggle-container">
<span className="mr">{allState.operationInfoOpen ? 'Close' : 'Open'} History</span>
<button className={`toggle-button ${allState.operationInfoOpen ? 'active' : ''}`} onClick={handleToggleOperationInfo}>
</button>
</div>
History Bölümü
Toggle butona açmak için bastığımızda bu işlemle beraber aşağıdaki History bölümü tetiklenir.
Çünkü kapsayıcı div’inin className’leri arasında operationInfoOpen değişkeninin değişimini takip eden bir kural vardır. Bu değer true ise className’e open eklenir. Bu open class’ı display görünür hale getirir. Eğer false ise değer open className’den kalkar başlangıçtaki hide durumu oluşur ve liste bölümü gizlenir.
Liste bölümü açıldığı anda gerçekleşen işlemse, history list’i üzerinde map ile dönüp her bir objesini process’e atayıp process’de bulunan değerler ile görünmesini istediğim liste elemanını oluşturmaktır. process ile beraber map fonksiyonunun aldığı index parametresi o anda kaçıncı eleman üzerinde çalışılıyorsa bunun bilgisini tutar. Her bir liste elemanının key değerinin uniq olmasını istediğim için key’e bu index değerini verdim.
<div className={`operation-info info-container ${allState.operationInfoOpen ? 'open' : ''}`}>
<h4>Operation History</h4>
{history.map((process, index) => (
<p key={index}>{process.count}: {process.num1} {process.operation} {process.num2} = {process.result}</p>
))}
</div>
Bu işlemler sayesinde istediğimiz sonuçları elde edebilmiş olduk. Şimdi testlerimizi yapalım.
Henüz listede elemanım yokken listeyi açmaya çalıştım başarıyla çalıştı.
Şimdi birkaç işlem gerçekleştiriyorum. 2 adet işlem gerçekleştirdim. Başarıyla yapıldı
.
Şimdi history bölümünü tekrar açabilirim. Her şey istediğim gibi mükemmel.
Bölümü kapatmak istediğimde de kapanacaktır.
Böylece prevState kavramını pek çok kavramla beraber öğrenmiş olduk.
Calculator.jsx dosyasının son halini tamamını görebilmeniz için aşağıda paylaşıyorum.
import "./Calculator.css";
import React, { useState } from 'react';
const Calculator = () => {
// #region States
const [allState, setAllState] = useState({
result: 0,
count: 0,
operationInfoOpen: false,
alertMessage: '',
});
const [history, setHistory] = useState([]);
// #endregion
// #region Functions
const updateAlertMessage = (message) => {
setAllState(prevState => ({
...prevState,
alertMessage: message,
}));
}
const updateResult = (result) => {
setAllState(prevState => ({
...prevState,
result: result,
count: prevState.count + 1,
}));
}
const updateOperationInfoOpen = (isOpen) => {
setAllState(prevState => ({
...prevState,
operationInfoOpen: isOpen,
}));
}
const updateHistory = (operation, num1, num2, result) => {
setHistory(prevList => [{count: prevList.length + 1, operation, num1, num2, result }, ...prevList]);
}
const handleToggleOperationInfo = () => {
if (history.length !== 0) {
updateOperationInfoOpen(!allState.operationInfoOpen);
}
else {
updateAlertMessage('There is no operation history');
setTimeout(() => updateAlertMessage(''), 5000);
}
};
const handleCalculate = (event) => {
event.preventDefault();
const form = event.target;
const num1 = parseFloat(form.elements.num1.value);
const num2 = parseFloat(form.elements.num2.value);
const operation = form.elements.operation.value;
let result;
switch (operation) {
case 'add':
result = num1 + num2;
updateHistory('+', num1, num2, result);
break;
case 'subtract':
result = num1 - num2;
updateHistory('-', num1, num2, result);
break;
case 'multiply':
result = num1 * num2;
updateHistory('*', num1, num2, result);
break;
case 'divide':
if (num2 !== 0) {
result = num1 / num2;
updateHistory('/', num1, num2, result);
} else {
updateAlertMessage('Cannot divide by zero');
setTimeout(() => updateAlertMessage(''), 5000);
}
break;
default:
updateAlertMessage('Invalid operation');
setTimeout(() => updateAlertMessage(''), 5000);
}
updateResult(result);
};
// #endregion
// #region Render
return (
<div className="app">
{allState.alertMessage ?
<div className="header">
<div className="alert">{allState.alertMessage}</div>
</div> : null
}
<div className={`operation-info info-container ${allState.operationInfoOpen ? 'open' : ''}`}>
<h4>Operation History</h4>
{history.map((process, index) => (
<p key={index}>{process.count}: {process.num1} {process.operation} {process.num2} = {process.result}</p>
))}
</div>
<div className="toggle-container">
<span className="mr">{allState.operationInfoOpen ? 'Close' : 'Open'} History</span>
<button className={`toggle-button ${allState.operationInfoOpen ? 'active' : ''}`} onClick={handleToggleOperationInfo}>
</button>
</div>
<div className="result">
<h1 id="result">{allState.result}</h1>
<span>İşlem Adeti: {allState.count}</span>
</div>
<form onSubmit={handleCalculate}>
<input type="number" name="num1" required />
<input type="number" name="num2" required />
<select name="operation" required>
<option value="">Select operation</option>
<option value="add">Add</option>
<option value="subtract">Subtract</option>
<option value="multiply">Multiply</option>
<option value="divide">Divide</option>
</select>
<button type="submit">Calculate</button>
</form>
</div>
)
// #endregion
}
export default Calculator;
Bir sonraki yazımda görüşmek üzere.