Sayfayı yeniliyorsunuz ve ekran boş kalıyor. Ya da belki bir düğmeye tıklamak CPU fanınızın hızını sonuna kadar artırıyor. Ardından konsol o korkunç uyarıyı kusuyor: Maximum update depth exceeded. React, bileşeniniz bir sonsuz döngüye takıldığı için frenlere asıldı. Bu, React uygulamalarında en sık karşılaşılan hatalardan biridir ve genellikle kodunuzun gerçekte ne zaman çalıştığına dair basit bir yanlış anlamadan kaynaklanır.
Bunu düzeltmek için, bir render işleminin tam olarak ne zaman bir re-render'a dönüştüğünü ve state değişikliklerinin neden render yolunun dışında kalması gerektiğini anlamanız gerekir.
React Bileşeninizi Nasıl Render Eder
React, kullanıcı arayüzlerini bileşenlerden oluşturur. Modern React'te bu bileşenler fonksiyonlardır. React, bileşeninizi ekranda göstermesi gerektiğinde, sadece o fonksiyonu çağırır. Fonksiyonun içinde, render'lar arasında bir şeyleri hatırlamak için state kullanabilirsiniz. State, React'e hangi verinin bileşene ait olduğunu ve en önemlisi, bir şeyin değiştiğini ve UI'ın güncellenmesi gerektiğini söyler.
State değiştiğinde, React yeni bir render planlar. Bileşen fonksiyonu tekrar çalışır, yeni bir JSX döndürür ve React, DOM'u buna uyacak şekilde günceller. Normal kullanımda bu döngü zararsızdır. Bir düğmeye tıklarsınız, bir olay işleyici (event handler) state'i günceller, React bir kez re-render yapar ve kullanıcı yeni metni veya rengi görür.
Hata, bir render işleminin kendisi başka bir state güncellemesini tetiklediğinde ortaya çıkar. Bu yeni state güncellemesi başka bir render'ı, o da başka bir state güncellemesini tetikler. React buna birkaç düzine döngü boyunca tolerans gösterir, ardından tarayıcının tamamen donmasını önlemek için maksimum derinlik hatasını fırlatır.
State'e Hızlı Bir Bakış
Döngüyü incelemeden önce, useState hook'unun nasıl çalıştığını hatırlayalım. Size tam olarak iki şey verir: mevcut değeri tutan bir değişken ve bu değeri değiştirmek için bir fonksiyon.
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
Burada, ilk render'da message değeri 'Welcome' şeklindedir. Eğer daha sonra setMessage('Goodbye') çağırırsanız, React değişikliği not eder, MessageComponent'i tekrar çağırır ve UI artık "Goodbye" gösterir. Her şey yolundadır çünkü bileşen gövdesinin kendisinde hiçbir şey setter'ı otomatik olarak çağırmıyordur. Döngü, setter harici bir olay olmaksızın render aşamasında tetiklendiğinde başlar.
SetState'i Doğrudan Gövde İçinde Çağırmak
Sonsuz döngü oluşturmanın en doğrudan yolu, bir state setter fonksiyonunu doğrudan bileşen gövdesi içinde çağırmaktır. Bileşen gövdesi her render'da çalıştığı için, setter de her render'da tetiklenir. Bu yeni state güncellemesi başka bir render'a neden olur. Döngü sonsuza kadar döner.
Hata şu şekilde görünür:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
Counter her render edildiğinde count değerini artırır. React yeni sayıyı göstermek için tekrar render eder, setCount(count + 1) ifadesini tekrar görür ve bir kez daha artırır. Çözüm basittir: Render sırasında bileşeninizin en üst seviyesinde asla bir state setter çağırmayın. State güncellemeleri, ekranı boyama eylemine değil, kullanıcı olaylarına veya yan etkilere (side effects) yanıt vermelidir. Bu güncellemeyi bir olay işleyicisine taşıyın:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
Render sırasında setter çağırmanın tek istisnası, props'lardan yeni bir state hesapladığınız durumdur; ancak o zaman bile değeri doğrudan türetmek veya kasıtlı olarak useEffect kullanmak gibi farklı bir desen kullanmalısınız.
Referans Yerine Fonksiyon Çağrısı Geçirmek
Bir diğer yaygın neden ise küçük bir JSX yazım hatasıdır. Bir olay işleyici eklediğinizde, fonksiyonun kendisini geçmeniz gerekir. Eğer yanlışlıkla fonksiyonu JSX içinde hemen orada çağırırsanız, render döngüsü sırasında anında çalışır.
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
Parantezlerle handleToggle() yazarak, React'e kullanıcı tıkladığında daha sonra çağırabileceği bir fonksiyon vermemiş olursunuz. React sanal DOM'u oluştururken onu hemen şimdi çağırıyorsunuz. handleToggle state'i güncellediği için bileşen re-render edilir. Bu re-render sırasında React tekrar handleToggle() görür ve onu tekrar çağırır. Döngü hiç bitmez. Doğru versiyon parantezleri kaldırır:
return <button onClick={handleToggle}>Toggle</button>;
Eğer argüman geçmeniz gerekiyorsa, çağrıyı anonim bir fonksiyon içine alın:
return <button onClick={() => handleToggle(true)}>Switch On</button>;
Bu ayrım, refactoring sırasında deneyimli geliştiricileri bile yanıltabilir. Parantezlere dikkat edin.
useEffect Bağımlılık Tuzağı
Effect'ler; veri çekme, tarayıcı API'leri ile senkronize olma veya DOM'u manuel olarak manipüle etme gibi yan etkiler (side effects) için doğru yerdir. Ancak useEffect, React render işlemini ekrana işledikten sonra çalışır. Eğer effect'iniz state'i güncelliyorsa, React re-render yapacaktır. Normalde bu sorun değildir. Ancak effect, her render'dan sonra çalışır ve her zaman aynı state'i güncellerse bir döngüye dönüşür.
Şu hatalı deseni ele alalım:
const UserProfile = () => {
const [user, setUser] = useState({});
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
});
return <div>{user.name}</div>;
};
Bağımlılık dizisi (dependency array) olmadığı için, bu efekt her render işleminden sonra çalışır. user değerini ayarlar, bu da bir render işlemini tetikler. Bu render işleminden sonra efekt tekrar çalışır ve user değerini yeniden ayarlar. React bu sarmalı fark eder ve hatayı fırlatır.
Çözüm, uygun bir bağımlılık dizisi sağlayarak React'e efektin gerçekte ne zaman çalışması gerektiğini söylemektir. Eğer efektin yalnızca mount anında bir kez çalışması gerekiyorsa, boş bir dizi geçin:
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
}, []);
Eğer efekt bir prop'a veya bir state parçasına bağlıysa, diziye yalnızca o değişkeni dahil edin. Ancak dikkatli olun. Her render'da değişen bir değişkeni dahil etmek, aynı döngüyü farklı bir kapıdan tekrar oluşturacaktır. Örneğin, bağımlılıklara bir nesne literal'ı (object literal) dahil ederseniz ve bu nesne her üst (parent) render işleminde yeniden oluşturulursa, efekt sonsuza kadar çalışacaktır. Bu durumlarda, nesne oluşturma işlemini bileşenin dışına taşımanız veya onu memoize etmeniz gerekebilir.
Pratik Hata Ayıklama Adımları
Bu hatayla karşılaştığınızda, React döngüyü halihazırda onlarca kez tekrarladığı için stack trace bunaltıcı görünebilir. Hangi bileşenin tekrar tekrar isimlendirildiğini bulmak için izleme kaydının en üstünü okuyarak başlayın. Ardından, şu üç yerde state setter'larını arayın:
- Herhangi bir handler veya hook dışında kalan, bileşenin ana gövdesi.
handleryerinehandler()yazmış olabileceğiniz JSX event öznitelikleri.- Bağımlılık dizisi eksik olan veya kararsız referanslara (unstable references) bağlı olan
useEffecthook'ları.
Hata durana kadar her bir state setter'ını geçici olarak yorum satırına alın. Bu, size tam olarak hangi güncellemenin suçlu olduğunu söyleyecektir. Eğer setter bir efektin içindeyse, orada gerçekten state'e ihtiyacınız olup olmadığını kendinize sorun. Bazen geliştiriciler, prop'u JSX içinde doğrudan kullanabilecekken, bir efekt içinde prop'tan yerel bir state ayarlarlar.
Asıl Çıkarılması Gereken Ders
Maximum update depth hatası gizemli bir React hatası değildir; bir güvenlik ağıdır. Bu, bileşeninizin harici bir sinyal beklemek yerine kendini yeniden render etmeye çalıştığı anlamına gelir. Render işlemlerini daha fazla state üretmesi gereken olaylar olarak görme alışkanlığından vazgeçin. Render işlemlerini nedenler olarak değil, state'in saf sonuçları olarak değerlendirin. State güncellemelerini event handler'lar, callback'ler veya dikkatle seçilmiş bağımlılıklara sahip efektler içinde tutun; o zaman bu hatayı bir daha asla görmezsiniz.
