React'te state'teki bir diziyi ya da nesneyi, yenisini oluşturup setter'a aktararak güncellersin. Mevcut değeri asla yerinde değiştirme: değişikliğini içeren bir kopya oluşturmak için spread (...), map, filter ya da slice kullan, sonra setItems(copy) çağır.
[...items, { ... }] bir öğe fazlası olan yeni bir dizidir, filter ise çıkarılan öğe olmadan yeni bir dizi döndürür. İkisi de eski diziye dokunmaz.
Mutation neden render etmez
Bir setter çağırdığında React yeni değeri eskisiyle Object.is kullanarak karşılaştırır. Diziler ve nesneler için bu, içerikleri değil referansları karşılaştırır. Bir diziyi push ile değiştirip aynı diziyi geri aktarırsan React aynı referansı görür, hiçbir şeyin değişmediğine karar verir ve render'ı atlar.
"push"a birkaç kez tıkla. Konsol dizinin büyüdüğünü gösterir, ama yeni bir render satırı görünmez ve metin a olarak kalır. Sonra "spread"e tıkla: bir render gerçekleşir ve push edilen her öğe aynı anda görünür, çünkü baştan beri dizideydiler. Mutation bir hata fırlatmaz, sadece başka bir şey render tetikleyene kadar ekranı güncel olmayan hâlde bırakır. Bundan sonra "push"a bir kez daha tıkla, bu sefer render edebilir: React işi her zaman baştan atlayamaz, bu yüzden bir mutation bazen görünür, bazen görünmez. State'i değiştirmemenin nedeni bu öngörülemezliktir.
Diziler: kullanılacak ve kaçınılacak metotlar
Kural basit: yeni bir dizi döndüren metotları kullan, çağrıldıkları diziyi değiştiren metotlardan kaçın.
| Amaç | Kaçın (değiştirir) | Kullan (yeni dizi döndürür) |
|---|---|---|
| Ekleme | push, unshift | [...arr, item], [item, ...arr] |
| Çıkarma | pop, shift, splice | filter, slice |
| Değiştirme | arr[i] = x, splice | map, with(i, x) |
| Araya ekleme | splice | [...arr.slice(0, i), x, ...arr.slice(i)], toSpliced |
| Sıralama, ters çevirme | sort, reverse | toSorted, toReversed ya da önce kopyala |
toSorted, toReversed, toSpliced ve with daha yeni dizi metotlarıdır (ES2023) ve 2023'ten beri tüm güncel büyük tarayıcılarda desteklenir. Değiştiren kuzenleriyle aynı işi bir kopya üzerinde yaparlar.
map ile tek bir öğeyi güncellemek
Tek bir öğeyi değiştirmek için dizi üzerinde map yap; istediğin öğe için yeni bir nesne, diğer her öğe için orijinal nesneyi döndür.
{ ...todo, done: !todo.done } kısmı önemlidir. Yaygın bir hata diziyi kopyalayıp içindeki nesneyi değiştirmektir:
const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);
Bu render eder, bu yüzden iyi görünür, ama eski dizi ile yeni dizi artık değiştirilmiş bir nesneyi paylaşır. Eski öğeyi yenisiyle karşılaştıran her şey (bir React.memo çocuğu, öğeye bağlı bir efekt) hiçbir fark görmez. Değiştirdiğin öğeyi kopyala.
Bir index'e ekleme yapmak
Değiştirmeyen bir insert yoktur, bu yüzden diziyi ikiye böl ve yeni öğeyi yarıların arasına koy. toSpliced(index, 0, item) aynısını tek çağrıda yapar.
Index'i 0 yap ve yeniden ekle: yeni C başa gider. next ifadesini letters.toSpliced(index, 0, 'C') ile değiştir; sonuç aynıdır.
Sıralamak ve ters çevirmek
sort ve reverse, diziyi döndürseler de çağrıldıkları diziyi değiştirir. Bu da setItems(items.sort()) ifadesini bir mutation ve ardından aynı referansla bir set yapar; bu, bu sayfanın başındaki hatadır. Bunun yerine bir kopyayı sırala.
[...scores].reverse() çalışır, çünkü spread önce kopyayı oluşturur ve reverse sonra yalnızca o kopyayı değiştirir.
Nesneleri ve iç içe nesneleri güncellemek
Nesneler de aynı kurala uyar. Eski nesneyi yenisine yay ve değişen alanları geçersiz kıl. İç içe bir nesne için alana inerken her seviyede yay: tek bir spread yalnızca en üst seviyeyi kopyalar, yani iç nesne hâlâ eskisi olurdu.
Kopyaladığın her seviye yeni bir nesnedir; dokunmadığın her seviye aynı nesne olarak kalır. Spread'i ucuz yapan budur: state uzun bir sipariş listesi de tutsaydı, { ...user } listenin kendisini değil listeye olan referansı kopyalardı.
State'teki bir nesne üç dört seviye derinliğindeyse ve sürekli uzun spread zincirleri yazıyorsan, bu çoğu zaman state'in daha düz olabileceğinin işaretidir. Öğeleri id'ye göre bir arama tablosunda saklamak ya da büyük bir nesneyi birkaç useState çağrısına bölmek her güncellemeyi kısa tutar. Birçok olay tek bir yapıyı değiştirdiğinde bir reducer da yardımcı olur (bkz. useReducer).
structuredClone ve spread
structuredClone(user) tek çağrıda derin bir kopya oluşturur, böylece kopyayı sonra özgürce değiştirebilirsin:
const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);
Doğrudur, ama değiştirmediğin parçalar dahil tüm nesneyi her seferinde kopyalar. Bu büyük state'te zaman alır ve her iç içe nesnenin yeni bir referans alması anlamına gelir; böylece user.orders alan memoize edilmiş bir çocuk, siparişler değişmemiş olsa bile yeniden render olur. structuredClone ayrıca fonksiyonlarda hata fırlatır ve class örnekleri düz nesneler olarak geri gelir. Spread yalnızca değiştirdiğin yolu kopyalar, bu yüzden olağan seçimdir; structuredClone'u nesnenin çoğunu gerçekten yeniden yazan nadir güncellemeler için sakla.
Immer ile mutation yazmak
Immer, mutation gibi görünen kod yazmanı sağlayan küçük bir kütüphanedir. Sana bir taslak verir, neyi değiştirdiğini kaydeder ve yalnızca değişen parçaları kopyalanmış yeni, immutable bir değer üretir. use-immer paketi onu bir hook'la sarar:
import { useImmer } from 'use-immer';
function Profile() {
const [user, updateUser] = useImmer({
name: 'Ada',
address: { city: 'London' },
});
function moveTo(city) {
updateUser((draft) => {
draft.address.city = city;
});
}
// ...
}
Immer, yalnızca React yükleyen bu sayfadaki editörlerde mevcut değil. State'in derin şekilde iç içe olduğunda ve spread'ler okunması zor hâle geldiğinde eklemeye değer. Düz state için düz spread ve map yeterlidir ve çoğu React kodunda göreceğin de bunlardır.
Sıkça Sorulan Sorular
React state'inde bir diziye nasıl öğe eklenir?
Eski öğeleri ve yenisini içeren yeni bir dizi oluştur ve setter'a aktar: setItems([...items, newItem]). items.push(newItem) çağırma, çünkü bu mevcut diziyi değiştirir ve React aynı referansı görür.
Bir diziyi güncellediğimde bileşenim neden yeniden render olmuyor?
Büyük ihtimalle diziyi yerinde değiştirdin (push, splice, sort ya da arr[i] = x) ve aynı diziyi geri aktardın. React eski ve yeni değeri Object.is ile karşılaştırır, aynı nesneyi bulur ve render'ı atlar. Bunun yerine yeni bir dizi aktar.
React'te bir dizinin içindeki tek bir nesne nasıl güncellenir?
map kullan; değişen öğe için yeni bir nesne, diğer her öğe için orijinal öğeyi döndür: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).
React state'inde iç içe bir nesne nasıl güncellenir?
Değiştirdiğin alana giden yoldaki her seviyeyi kopyala: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). Tek bir spread yalnızca en üst seviyeyi kopyalar, yani iç nesne hâlâ paylaşılıyor olurdu.
React state'i için structuredClone mu, spread operatörü mü kullanmalıyım?
Spread'i tercih et. Yalnızca değişeni kopyalar ve dokunulmayan parçaları aynı nesneler olarak tutar; bu React.memo'ya ve karşılaştırmalara yardımcı olur. structuredClone her seferinde her şeyi kopyalar; yazması daha basittir ama büyük state'te daha yavaştır ve değişmeyen parçalar için referans eşitliğini bozar.