State İçerisindeki Dizileri Güncelleme
Diziler JavaScript’te değiştirilebilirdir, ancak bunları state içinde depolarken değiştirilemez olarak ele almalısınız. Tıpkı nesnelerde olduğu gibi, state’te depolanan bir diziyi güncellemek istediğinizde yeni bir dizi oluşturmanız (veya var olanın bir kopyasını oluşturmanız) ve ardından yeni oluşturduğunuz diziyi kullanmak için state’i güncellemeniz gerekir.
Bunları öğreneceksiniz
- React state’indeki bir diziye öğeler nasıl eklenir, çıkarılır ya da değiştirilir
- Dizi içindeki nesne nasıl güncellenir
- Immer kullanarak dizi kopyalama işlemi daha az tekrarla nasıl yapılır
Dizileri değiştirmeden güncelleme
JavaScript’te diziler bir nesne türüdür. Nesnelerde olduğu gibi, React state’indeki dizileri salt okunur olarak görmelisiniz. Bu, arr[0] = 'bird'
şeklinde bir dizi içindeki öğeleri başka değerlere yeniden atamamanız, ayrıca push()
ve pop()
gibi dizileri mutasyona uğratan JavaScript metodlarını kullanmamanız gerektiği anlamına gelir.
Bu metodları kullanmak yerine, bir diziyi her güncellemek istediğinizde state setter fonksiyonunuza yeni bir dizi iletmelisiniz. Bunu yapmak için, filter()
ve map()
gibi diziyi mutasyona uğratmayan JavaScript metodlarını kullanarak orijinal diziden yeni bir dizi oluşturabilirsiniz. Ardından, state’inizi kopyaladığınız dizi olarak güncelleyebilirsiniz.
Aşağıda sık kullanılan dizi metodları tablo halinde gösterilmiştir. React state’indeki dizilerle çalışırken sol sütundaki metodları kullanmaktan kaçınarak sağ sütundaki metodları tercih etmelisiniz.
kaçınılacaklar (diziyi mutasyona uğratır) | tercih edilecekler (yeni bir dizi döndürür) | |
---|---|---|
eklemek | push , unshift | concat , [...arr] spread sözdizimi (örnek) |
çıkartmak | pop , shift , splice | filter , slice (örnek) |
değiştirmek | splice , arr[i] = ... ataması | map (örnek) |
sıralamak | reverse , sort | ilk önce diziyi kopyalayın (örnek) |
Alternatif olarak, her iki sütundaki metodları kullanmanıza izin veren [Immer’ı] (#write-concise-update-logic-with-immer) tercih edebilirsiniz.
Diziye öğe eklemek
push()
metodu diziyi mutasyona uğratacaktır, ki bunu istemezsiniz:
import { useState } from 'react'; let nextId = 0; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState([]); return ( <> <h1>İlham verici heykeltıraşlar:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={() => { artists.push({ id: nextId++, name: name, }); }}>Ekle</button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
Bunun yerine, mevcut öğeleri ve son eleman olarak yeni öğeyi içeren yeni diziyi oluşturun. Bunu yapmanın birden çok yolu vardır ancak en kolay yol ...
dizi spread sözdizimini kullanmaktır:
setArtists( // State'i yeni bir dizi
[ // ile değiştirin
...artists, // bu eski öğelerin tümünü
{ id: nextId++, name: name } // ve sona eklenecek yeni öğeyi içerir.
]
);
Şimdi doğru şekilde çalışmakta:
import { useState } from 'react'; let nextId = 0; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState([]); return ( <> <h1>İlham verici heykeltıraşlar:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={() => { setArtists([ ...artists, { id: nextId++, name: name } ]); }}>Ekle</button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
Dizi spread sözdizimi yeni öğeyi orijinal ...artists
’den önceye yerleştirerek dizinin başına eklemenizi de sağlar:
setArtists([
{ id: nextId++, name: name },
...artists // Eski öğeleri dizinin sonuna yerleştir
]);
Bu şekilde, spread sözdizimi push()
(öğeyi dizinin sonuna eklemek) ve unshift()
(öğeyi dizinin başına eklemek) metodlarının görevini yapabilir. Yukarıdaki sandbox’ta deneyebilirsiniz!
Diziden öğe çıkartma
Diziden bir öğeyi çıkartmanın en kolay yolu o öğeyi filtrelemektir. Bir başka deyişle, o öğeyi içermeyen yeni bir dizi oluşturmaktır. Bunu yapmak için filter
metodunu kullanabilirsiniz. Örneğin:
import { useState } from 'react'; let initialArtists = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye'}, { id: 2, name: 'Louise Nevelson'}, ]; export default function List() { const [artists, setArtists] = useState( initialArtists ); return ( <> <h1>İlham verici heykeltıraşlar:</h1> <ul> {artists.map(artist => ( <li key={artist.id}> {artist.name}{' '} <button onClick={() => { setArtists( artists.filter(a => a.id !== artist.id ) ); }}> Sil </button> </li> ))} </ul> </> ); }
“Sil” butonuna birkaç kez tıklayın ve tıklama yöneticisine bakın.
setArtists(
artists.filter(a => a.id !== artist.id)
);
Burada artists.filter(a => a.id !== artist.id)
ifadesi “artist
dizisini kullanarak ID’leri artist.id
’den farklı olan öğelerle yeni bir dizi oluştur” anlamına gelmektedir. Diğer bir deyişle, her bir artist’e karşılık gelen “Sil” butonu o artist’i diziden filtreleyecek ve nihai dizi ile yeniden render isteği gönderecektir. filter
metodunun orijinal diziyi değiştirmediğini unutmayın.
Diziyi dönüştürme
Dizideki bazı ya da tüm öğeleri değiştirmek isterseniz yeni bir dizi oluşturmak için map()
metodunu kullanabilirsiniz. map
’e ileteceğiniz fonksiyon, verisine veya indeksine (veya her ikisine) bağlı olarak her bir öğeyle ne yapacağınızı belirler.
Bu örnekte dizi, iki daire ve bir karenin koordinatlarını içermektedir. Butona tıkladığınız zaman sadece daireler 50 piksel aşağı hareket etmektedir. Bunu, map()
metodunu kullanıp yeni bir veri dizisi oluşturarak yapar:
import { useState } from 'react'; let initialShapes = [ { id: 0, type: 'circle', x: 50, y: 100 }, { id: 1, type: 'square', x: 150, y: 100 }, { id: 2, type: 'circle', x: 250, y: 100 }, ]; export default function ShapeEditor() { const [shapes, setShapes] = useState( initialShapes ); function handleClick() { const nextShapes = shapes.map(shape => { if (shape.type === 'square') { // Değişiklik yok return shape; } else { // 50 piksel aşağıda yeni bir daire döndürür return { ...shape, y: shape.y + 50, }; } }); // Yeni dizi ile yeniden render et setShapes(nextShapes); } return ( <> <button onClick={handleClick}> Daireleri aşağı hareket ettir! </button> {shapes.map(shape => ( <div key={shape.id} style={{ background: 'purple', position: 'absolute', left: shape.x, top: shape.y, borderRadius: shape.type === 'circle' ? '50%' : '', width: 20, height: 20, }} /> ))} </> ); }
Dizideki öğeleri değiştirme
Bir dizideki bir veya daha fazla öğeyi değiştirmek oldukça sık istenmektedir. arr[0] = 'bird'
gibi atamalar yapmak orijinal diziyi mutasyona uğrattığı için burada da map
metodunu kullanmak mantıklı olacaktır.
Bir öğeyi değiştirmek için map
ile yeni bir dizi oluşturun. map
metodu içinde, ikinci argüman olarak öğenin indeksini alacaksınız. Bu indeksi orijinal öğeyi (fonksiyonun ilk argümanı) veya başka bir öğeyi döndürüp döndürmeyeceğinize karar vermek için kullanın:
import { useState } from 'react'; let initialCounters = [ 0, 0, 0 ]; export default function CounterList() { const [counters, setCounters] = useState( initialCounters ); function handleIncrementClick(index) { const nextCounters = counters.map((c, i) => { if (i === index) { // Tıklanan sayacı artır return c + 1; } else { // Geri kalan değişmez return c; } }); setCounters(nextCounters); } return ( <ul> {counters.map((counter, i) => ( <li key={i}> {counter} <button onClick={() => { handleIncrementClick(i); }}>+1</button> </li> ))} </ul> ); }
Dizide belli bir konuma öğe ekleme
Bazen bir öğeyi dizinin başına ya da sonuna değil de belirli bir konuma eklemek isteyebilirsiniz. Bunu yapmak için, ...
dizi spread sözdizimini slice()
metodu ile beraber kullanabilirsiniz. slice()
metodu diziden bir “dilim (slice)” almanızı sağlar. Yeni öğeyi eklemek için, eklemek istediğiniz konumdan önceki dilimden spread sözdizimi ile yeni bir dizi oluşturacak, ardından yeni öğeyi ekleyecek ve son olarak da orijinal dizinin geri kalanını ekleyeceksiniz.
Bu örnekte, Ekle butonu her zaman 1.
indekse ekler:
import { useState } from 'react'; let nextId = 3; const initialArtists = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye'}, { id: 2, name: 'Louise Nevelson'}, ]; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState( initialArtists ); function handleClick() { const insertAt = 1; // Herhangi bir indeks olabilir const nextArtists = [ // Eklemek istediğiniz konumdan önceki öğeler: ...artists.slice(0, insertAt), // Yeni öğe: { id: nextId++, name: name }, // Eklemek istediğiniz konumdan sonraki öğeler: ...artists.slice(insertAt) ]; setArtists(nextArtists); setName(''); } return ( <> <h1>İlham verici heykeltıraşlar:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={handleClick}> Ekle </button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
Dizide başka değişiklikler yapma
Spread sözdizimi veya map()
ve filter()
gibi diziyi mutasyona uğratmayan metodlarla yapamayacağınız bazı şeyler vardır. Örneğin, bir diziyi sıralamak veya dizi öğelerini ters çevirmek isteyebilirsiniz. JavaScript’in reverse()
ve sort()
metodları orijinal diziyi değiştirir, bu nedenle doğrudan bu metodları kullanamazsınız.
Ancak, önce diziyi kopyalayabilir ve sonra o dizi üzerinde değişiklikler yapabilirsiniz.
Örneğin:
import { useState } from 'react'; const initialList = [ { id: 0, title: 'Big Bellies' }, { id: 1, title: 'Lunar Landscape' }, { id: 2, title: 'Terracotta Army' }, ]; export default function List() { const [list, setList] = useState(initialList); function handleClick() { const nextList = [...list]; nextList.reverse(); setList(nextList); } return ( <> <button onClick={handleClick}> Ters Çevir </button> <ul> {list.map(artwork => ( <li key={artwork.id}>{artwork.title}</li> ))} </ul> </> ); }
Burada [...list]
spread sözdizimi kullanılarak orijinal dizinin bir kopyası oluşturulur. Artık bir kopyanız olduğuna göre nextList.reverse()
ya da nextList.sort()
gibi mutasyona sebep olan metodlar kullanabilir, hatta nextList[0] = "something"
ile öğeleri tek tek yeni değerlerine atayabilirsiniz.
Ancak, bir diziyi kopyalasanız bile dizinin içindeki öğeleri doğrudan mutasyona uğratamazsınız. Bunun nedeni yaptığınız kopyalamanın yüzeysel (shallow) olmasıdır. Yani yeni dizi, orijinal diziyle aynı öğeleri içermektedir. Dolayısıyla, kopyalanan dizinin içindeki bir nesneyi değiştirdiğiniz zaman mevcut state’i de mutasyona uğratmış olursunuz. Örneğin, aşağıdaki gibi bir kod sorunludur.
const nextList = [...list];
nextList[0].seen = true; // Sorun: list[0]'ı mutasyona uğratır
setList(nextList);
nextList
ve list
iki farklı dizi olmasına rağmen, nextList[0]
ve list[0]
ifadeleri aynı nesneyi işaret eder. Yani nextList[0].seen
değerini değiştirirseniz, aynı zamanda list[0].seen
değerini de değiştirmiş olursunuz. Bu, state’i mutasyona uğratmaktır ki bundan kaçınmalısınız! Bu sorunu iç içe JavaScript nesnelerini güncelleme yöntemine benzer şekilde, değiştirmek istediğiniz öğeleri mutasyona uğratmak yerine tek tek kopyalayarak çözebilirsiniz. Nasıl yapıldığını görelim.
Dizideki nesneleri güncelleme
Nesneler gerçekte dizilerin “içinde” yer almazlar. Yazdığınız kodda “içinde” gibi görünebilir ancak bir dizideki her nesne, dizinin “işaret ettiği” ayrı bir değerdir. Bu yüzden list[0]
gibi iç içe ifadeleri değiştirirken dikkatli olmalısınız. Başka bir kişinin sanat eseri listesi (artwork list), dizinin aynı öğesine işaret edebilir!
İç içe geçmiş state’i güncellerken, güncellemek istediğiniz noktadan en üst düzeye kadar kopyalar oluşturmanız gerekir. Şimdi bunun nasıl olduğunu görelim.
Bu örnekte, iki farklı sanat eseri listesi aynı başlangıç state’ine sahiptir. Bu listelerin izole olmaları gerekirdi ancak bir mutasyon nedeniyle yanlışlıkla state’leri paylaşmaktadırlar ve listedeki bir kutuyu işaretlemek diğer listeyi de etkilemektedir:
import { useState } from 'react'; let nextId = 3; const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export default function BucketList() { const [myList, setMyList] = useState(initialList); const [yourList, setYourList] = useState( initialList ); function handleToggleMyList(artworkId, nextSeen) { const myNextList = [...myList]; const artwork = myNextList.find( a => a.id === artworkId ); artwork.seen = nextSeen; setMyList(myNextList); } function handleToggleYourList(artworkId, nextSeen) { const yourNextList = [...yourList]; const artwork = yourNextList.find( a => a.id === artworkId ); artwork.seen = nextSeen; setYourList(yourNextList); } return ( <> <h1>Görülecek Sanat Eserleri Listesi</h1> <h2>Görmek istediğim eserler listesi:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>Senin görmek istediğin eserler listesi:</h2> <ItemList artworks={yourList} onToggle={handleToggleYourList} /> </> ); } function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> ); }
Bu koddaki sorun şudur:
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Sorun: mevcut öğeyi mutasyona uğratır
setMyList(myNextList);
myNextList
dizisi yeni bir dizi olmasına rağmen, dizi içindeki öğeler orijinal myList
dizisindeki öğeler ile aynıdır. Yani artwork.seen
’i değiştirmek orijinal sanat eserini de değiştirir. Bu sanat eseri yourList
dizisinde de olduğu için hata burdan kaynaklanmaktadır. Bunun gibi hataları düşünmek zor olabilir, ancak state’i mutasyona uğratmaktan kaçınırsanız bu hatalar ortadan kalkacaktır.
Eski bir öğeyi mutasyon olmadan güncellenmiş sürümüyle değiştirmek için map
metodunu kullanabilirsiniz.
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Değişikliklerle *yeni* bir nesne oluştur
return { ...artwork, seen: nextSeen };
} else {
// Değişiklik yok
return artwork;
}
}));
Burada ...
, bir nesnenin kopyasını oluşturmak için kullanılan nesne spread sözdizimidir.
Bu yaklaşımla, mevcut state öğelerinin hiçbiri mutasyona uğramamış olur ve hata düzeltilir:
import { useState } from 'react'; let nextId = 3; const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export default function BucketList() { const [myList, setMyList] = useState(initialList); const [yourList, setYourList] = useState( initialList ); function handleToggleMyList(artworkId, nextSeen) { setMyList(myList.map(artwork => { if (artwork.id === artworkId) { // Değişikliklerle *yeni* bir nesne oluştur return { ...artwork, seen: nextSeen }; } else { // Değişiklik yok return artwork; } })); } function handleToggleYourList(artworkId, nextSeen) { setYourList(yourList.map(artwork => { if (artwork.id === artworkId) { // Değişikliklerle *yeni* bir nesne oluştur return { ...artwork, seen: nextSeen }; } else { // Değişiklik yok return artwork; } })); } return ( <> <h1>Görülecek Sanat Eserleri Listesi</h1> <h2>Görmek istediğim eserler listesi:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>Senin görmek istediğin eserler listesi:</h2> <ItemList artworks={yourList} onToggle={handleToggleYourList} /> </> ); } function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> ); }
Genel olarak, yalnızca az önce oluşturduğunuz nesneleri mutasyona uğratmalısınız. Yeni bir sanat eseri ekliyorsanız, onu mutasyona uğratabilirsiniz, ancak zaten state’te olan bir eserde bir değişiklik yapıyorsanız, bir kopya oluşturmanız gerekmektedir.
Immer kullanarak kısa ve öz güncelleme mantığı yazmak
İç içe dizileri mutasyona uğratmadan güncellemek tıpkı nesnelerde olduğu gibi biraz tekrara binebilir:
- Genel olarak, state’i birkaç seviyeden daha derine güncellemeniz gerekmez. Ancak state nesneleriniz çok derinse, düz (flat) olmaları için onları farklı şekilde yeniden yapılandırmak isteyebilirsiniz.
- Eğer state yapınızı değiştirmek istemiyorsanız, Immer kullanmak isteyebilirsiniz. Immer, diziyi mutasyona uğratacak sözdizimlerini kullanmanıza izin vererek, kopyalama işlemlerini sizin yerinize kendisi yapar.
Immer ile yazılmış örneği aşağıda görebilirsiniz:
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
Immer ile artwork.seen = nextSeen
gibi mutasyonların artık sorun çıkarmadığına dikkat edin:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
Bunun nedeni, orijinal state’i mutasyona uğratmamanızdır. Burada Immer tarafından sağlanan özel bir draft
nesnesini mutasyona uğratmaktayız. Benzer şekilde, draft
nesnesine push()
ve pop()
gibi mutasyona neden olan metodları da uygulayabilirsiniz.
Arka planda Immer, draft
’a yaptığınız değişikliklere göre her zaman bir sonraki state’i sıfırdan oluşturur. Bu, olay yönetecilerinizi, state’i hiç mutasyona uğratmadan kısa ve öz olarak tutar.
Özet
- State’e dizi koyabilirsiniz ancak değiştiremezsiniz.
- Bir diziyi mutasyona uğratmak yerine o dizinin yeni bir sürümünü oluşturun ve state’i buna göre güncelleyin.
[...arr, newItem]
dizi spread sözdizimini kullanarak yeni öğelerle bir dizi oluşturabilirsinizfilter()
vemap()
metodlarını kullanarak filtrelenmiş ya da dönüştürülmüş yeni diziler oluşturabilirsiniz.- Kodunuzu kısa ve öz tutmak için Immer’ı kullanabilirsiniz.
Problem 1 / 4: Alışveriş sepetindeki ürünü güncelleyin
”+” butonuna tıklandığında ilgili sayının artması için handleIncreaseClick
mantığını doldurun:
import { useState } from 'react'; const initialProducts = [{ id: 0, name: 'Baklava', count: 1, }, { id: 1, name: 'Cheese', count: 5, }, { id: 2, name: 'Spaghetti', count: 2, }]; export default function ShoppingCart() { const [ products, setProducts ] = useState(initialProducts) function handleIncreaseClick(productId) { } return ( <ul> {products.map(product => ( <li key={product.id}> {product.name} {' '} (<b>{product.count}</b>) <button onClick={() => { handleIncreaseClick(product.id); }}> + </button> </li> ))} </ul> ); }