Yasin Ateş

Loading, Error ve Empty State Nasıl Daha İyi Yönetilir? ✨

Loading, Error ve Empty State Nasıl Daha İyi Yönetilir? ✨

Bir component deisLoading, isError, isSuccess ve bir de isIdle state'i birlikte yönetmeye çalıştınız mı? Başta masum görünüyor. Sonra bir gün hem isLoading hem isError aynı anda true oluyor ve ekranınız ne göstereceğini bilemez hâle geliyor. Bu yazıda bu "boolean tuzağı"nın neden oluştuğunu, nasıl kullanılmayan state'lere yol açtığını ve bunun yerine tek bir status sabitiyle nasıl temiz ve öngörülebilir bir yapı kurulacağını inceleyeceğiz.

Diyelim backend’deki bir API’ye istek attık ve bir ürün listesi ekrana çizeceğiz. Çoğumuzun yazdığı klasik kod şöyle olur:

const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [data, setData] = useState([]);

Bunu yazarken işi basit hale getirdiğimizi düşünüyoruz. Her state’i ayrı ayrı kontrol altında tutmak istiyoruz. Peki ama

isLoading = true ve isError = true aynı anda olabilir mi?

JavaScript’e göre: evet, olabilir. Sizi engelleyen hiçbir şey yok.

Peki ekrana ne render edilir? Hem skeleton hem hata mesajı mı? Bu kafa karıştırıcı bir durum ve uygulamamızın asla girmemesi gereken ama girebileceği bir durum

Çünkü başlangıçta gerçekten basit görünüyor. Ama uygulama büyüdükçe kontrol edemez hâle geliyoruz.

🔢 Matematiksel Gerçek: 4 Boolean = 16 Olası Durum

4 boolean değer 16 durum

Elinizde 4 tane boolean varsa, bunların kombinasyon sayısı ²⁴ yani 16'dır. Gerçekte bu 16 kombinasyonun kaçı anlamlı?

★ isIdle=true, isLoading=false, isError=false, isSuccess=false → ✅ Geçerli: başlangıç

★ isIdle=false, isLoading=true, isError=false, isSuccess=false → ✅ Geçerli: yükleniyor

★ isIdle=false, isLoading=false, isError=false, isSuccess=true → ✅ Geçerli: başarılı

★ isIdle=false, isLoading=false, isError=true, isSuccess=false → ✅ Geçerli: hata

Sadece 4 tanesi geçerli. Geri kalan 12 kombinasyon tamamen geçersiz ama JavaScript sizi bu kombinasyonlardan korumak için hiçbir şey yapmıyor.

İşte tam bu noktada bug’lar doğuyor. setIsSuccess(true) çağırırken isError'ı false yapmayı unuttunuz mu? Artık hem hata ekranı hem başarı mesajı aynı anda gösterilebilir.

Gelin bunu bir örnekle somutlaştıralım:

const fetchData = async () => {
 setIsLoading(true);

 try {
 const result = await api.getProducts();
 setData(result);
 setIsSuccess(true);
 // Unutulan satır: setIsLoading(false) ❌
 // Artık isLoading=true ve isSuccess=true aynı anda!
 } catch (err) {
 setIsError(true);
 // Unutulan satır: setIsLoading(false) ❌
 // Artık isLoading=true ve isError=true aynı anda!
 }
};
  • Her state geçişinde birden fazla set fonksiyonu çağırmak zorunda kalıyoruz; birini atlamak yeterli.
  • setIsLoading(false) hem try hem catch bloğunda tekrarlanmak zorunda
  • Kod büyüdükçe hangi kombinasyonun mümkün olduğunu zihinsel olarak takip etmek imkansızlaşıyor.

Çözüm Basit 😁 Sabit Değerler ve Tek Bir status state'i

Bir uygulama aynı anda yalnızca tek bir durumda olabilir.

Trafik lambası düşünelim hem kırmızı hem yeşil yanmaz. Aynı şekilde İletişim formu da hem submit edilmiyor hem de başarıyla tamamlanmış olamaz.

Bunu uygulamanın doğru yolu, olası state değerlerini önce birer const sabiti olarak tanımlamak, sonra tek bir status değişkeninde kullanmak.

Hadi adım adım ilerleyelim. Önce sabit değerleri tanımlayalım:

// ✅ Adım 1: Status değerlerini sabit olarak tanımla
const STATUS = {
 IDLE: 'idle',
 LOADING: 'loading',
 SUCCESS: 'success',
 ERROR: 'error',
};

Kritik noktalar:

  • STATUS.LOADING kullanmak, 'laoding' (belki farketmediniz okurken ama o ve a harfi yer değiştirmiş durumda 🥲) gibi yazım hatalarını önlüyor . Yanlış yazılan bir key undefined döner ve hatayı bulmak için çaba sarfetmemize yol açar.
  • Tüm geçerli state değerleri tek bir yerde; yeni biri ekleneceğinde sadece buraya bakmak yeterli oluyor.
  • Modül düzeyinde const olarak tanımlandığı için yeniden atanamaz; ayrı bir kilitleme mekanizmasına (Object.freeze vb.) gerek yok.

Şimdi bu sabitleri state’te kullanalım. error bilgisini ayrı bir useState ile tutmak, başlangıçta anlattığım senkronizasyon problemini farklı bir biçimde geri getiriyor: status === STATUS.ERROR iken error state'i hâlâ null kalabilir. Bunun yerine her şeyi tek bir state içinde taşıyalım:

// ✅ Adım 2: Tüm veriyi tek bir state ile yönetelim
const [state, setState] = useState({ status: STATUS.IDLE });

const fetchData = async () => {
 setState({ status: STATUS.LOADING });

 try {
 const result = await api.getProducts();
 setState({ status: STATUS.SUCCESS, data: result });
 } catch (err) {
 setState({ status: STATUS.ERROR, error: err });
 }
};
  • Ayrı bir setError state'i yok; error bilgisi zaten STATUS.ERROR state'inin içinde. Senkronize edilecek ikinci bir değişken kalmıyor.
  • setState({ status: STATUS.LOADING }) tek değer, tek sorumluluk. Ne kadar basit olduğuna bakın 🙂
  • STATUS.SUCCESS state'indeki data ve STATUS.ERROR state'indeki error hep birlikte gelip gidiyor; tutarsız ara durum mümkün değil.

Render tarafında switch/case yerine bir object map kullanalım. Switch/case de isterseniz kullanabilrisiniz ama ben tercih etmiyorum bence object based daha okunaklı oluyor 😁

// ✅ Adım 3: Object map ile render — switch/case yok, () çağrısı yok
const IdleView = () => <StartButton onClick={fetchData} />;

const LoadingView = () => <SkeletonGrid />;

const ErrorView = () => (
 <ErrorState message={state.error?.message} onRetry={fetchData} />
);

const SuccessView = () => {
 if (state.data?.length === 0) return <EmptyState />;
 return <ProductList data={state.data} />;
};

const stateComponents = {
 [STATUS.IDLE]: IdleView,
 [STATUS.LOADING]: LoadingView,
 [STATUS.ERROR]: ErrorView,
 [STATUS.SUCCESS]: SuccessView,
};

const CurrentView = stateComponents[state.status];

return <CurrentView />;
  • Component isimleri büyük harfle başlıyor; React bunları gerçek component olarak tanıyor ve <CurrentView /> sözdizimi çalışıyor.
  • Yeni bir state eklemek istediğinizde STATUS sabitine ve stateComponents map'ine birer satır ekliyorsunuz; başka hiçbir koda dokunmuyorsunuz. ☕️

Örnek: Şimdi Herşeyi Birleştirelim 🚀

Şimdi hepsini bir araya getirelim. Bir componnet ile tam kullanımı şöyle görünür:

// ✅ Tam componnet — temiz, öngörülebilir, genişletilebilir
const STATUS = {
 IDLE: 'idle',
 LOADING: 'loading',
 SUCCESS: 'success',
 ERROR: 'error',
};

function ProductList() {
 const [state, setState] = useState({ status: STATUS.IDLE });

 const fetchData = async () => {
 setState({ status: STATUS.LOADING });
 try {
 const data = await api.getProducts();
 setState({ status: STATUS.SUCCESS, data });
 } catch (err) {
 setState({ status: STATUS.ERROR, error: err });
 }
 };

 useEffect(() => {
 fetchData();
 }, []);

 const IdleView = () => null;

 const LoadingView = () => <SkeletonGrid />;

 const ErrorView = () => (
 <ErrorState message={state.error?.message} onRetry={fetchData} />
 );

 const SuccessView = () => {
 if (state.data?.length === 0) return <EmptyState />;
 return <ProductList data={state.data} />;
 };

 const stateComponents = {
 [STATUS.IDLE]: IdleView,
 [STATUS.LOADING]: LoadingView,
 [STATUS.ERROR]: ErrorView,
 [STATUS.SUCCESS]: SuccessView,
 };

 const CurrentView = stateComponents[state.status];

 return <CurrentView />;
}
  • STATUS sabiti bileşenin dışında; birden fazla bileşen kullansa bile tek bir kaynak var.
  • fetchData içinde toplam 3 setState çağrısı var; her biri tam ve bağımsız bir state. Senkronizasyon sorunu yok.
  • renderers map'i state'e closure ile erişiyor; prop drilling olmadan state.error ve state.data'ya ulaşabiliyor.
📝 Tabii istersek, error, success ve loading gibi component’leri dosyalara ayırıp başka dosyalarda yazıp bu component içine import edebiliriz. Ben tüm örneği tek seferde vermek istediğim için bu şekilde bıraktım.

Ben makaleyi React üzerinden hazırladım ama bu state tasarımı heryerde kullanılabilir. Burada önemli olan konu kurguyu doğru şekilde anlamak ✨

Sonuç

isLoading, isError, isSuccess üçlüsüyle başlayan kod, zamanla kontrol edilemez bir boolean karmaşasına dönüşebilir. 16 kombinasyonun 12'si geçersiz, ama JavaScript sizi bunlardan korumaz.

Çözüm aslında çok basit: olası durumları sabit olarak tanımlayın, tek bir status state'inde taşıyın. Böylece okuması zor olan state'ler hayatımızdan çıkmış olur. Her setState birbirinden bağımsız olur, render tarafı ise sade bir map ile okunabilir hale gelir.

Bu makale bana şu sözü hatırlattı 🥹

“Simplicity is a great virtue but it requires hard work to achieve it.”
(“Sadelik büyük bir erdemdir ama onu elde etmek için çok çalışmak gerekir.”)
— Edsger W. Dijkstra

📬 Geri Bildirim

Makaleyi yazarken, kaynakları belirleme ve araştırma için kendi notlarımı, yazım denetimi ve ek araştırma için Claude Opus 4.6 modelini kullandım. Resimleri üretmek için ise Gemini 3 Pro Preview 2k (Nano Banana Pro) modelini kullandım.

Yazı ile ilgili tavsiye, öneri, eleştirileri dikkate alıyorum. İletişime geçmek isterseniz bana websitemdeki sosyal medya adreslerimden veya Linkedin üzerinden ulaşabilirsiniz.

Sevgiyle kalın, Yasin 🤗

📚 Makaleyi Yazarken Kullandığım Kaynaklar