Türkçe

Kod bölmenin ötesinde veri çekme işlemleri için React Suspense'i keşfedin. Fetch-As-You-Render, hata yönetimi ve küresel uygulamalar için geleceğe dönük desenleri anlayın.

React Suspense ile Kaynak Yükleme: Modern Veri Çekme Desenlerinde Uzmanlaşma

Web geliştirmenin dinamik dünyasında, kullanıcı deneyimi (UX) en üst sırada yer alır. Uygulamaların, ağ koşullarından veya cihaz yeteneklerinden bağımsız olarak hızlı, duyarlı ve keyifli olması beklenir. React geliştiricileri için bu genellikle karmaşık durum yönetimi, komplike yükleme göstergeleri ve veri çekme şelalelerine (waterfalls) karşı sürekli bir mücadele anlamına gelir. İşte bu noktada, özellikle veri çekme olmak üzere asenkron işlemleri ele alış şeklimizi temelden dönüştürmek için tasarlanmış, güçlü ancak genellikle yanlış anlaşılan bir özellik olan React Suspense devreye giriyor.

Başlangıçta React.lazy() ile kod bölme için tanıtılan Suspense'in gerçek potansiyeli, bir API'den gelen veriler de dahil olmak üzere *herhangi bir* asenkron kaynağın yüklenmesini yönetme yeteneğinde yatmaktadır. Bu kapsamlı rehber, performanslı ve dayanıklı küresel uygulamalar oluşturmak için temel kavramlarını, temel veri çekme desenlerini ve pratik hususları keşfederek, kaynak yüklemesi için React Suspense'in derinliklerine dalacaktır.

React'te Veri Çekmenin Evrimi: Emrediciden Bildirimsele

Uzun yıllar boyunca, React bileşenlerinde veri çekme işlemi öncelikle yaygın bir desene dayanıyordu: bir API çağrısı başlatmak için useEffect kancasını kullanmak, useState ile yükleme ve hata durumlarını yönetmek ve bu durumlara göre koşullu olarak render işlemi yapmak. İşlevsel olmasına rağmen, bu yaklaşım genellikle birkaç zorluğa yol açtı:

Suspense olmadan tipik bir veri çekme senaryosunu ele alalım:

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setIsLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error(`HTTP hatası! durum: ${response.status}`);
        }
        const data = await response.json();
        setUser(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (isLoading) {
    return <p>Kullanıcı profili yükleniyor...</p>;
  }

  if (error) {
    return <p style={"color: red;"}>Hata: {error.message}</p>;
  }

  if (!user) {
    return <p>Kullanıcı verisi mevcut değil.</p>;
  }

  return (
    <div>
      <h2>Kullanıcı: {user.name}</h2>
      <p>E-posta: {user.email}</p>
      <!-- Daha fazla kullanıcı detayı -->
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Uygulamaya Hoş Geldiniz</h1>
      <UserProfile userId={"123"} />
    </div>
  );
}

Bu desen her yerde karşımıza çıkar, ancak bileşeni kendi asenkron durumunu yönetmeye zorlar, bu da genellikle arayüz (UI) ile veri çekme mantığı arasında sıkı bir şekilde bağlı bir ilişkiye yol açar. Suspense, daha bildirimsel ve akıcı bir alternatif sunar.

Kod Bölmenin Ötesinde React Suspense'i Anlamak

Çoğu geliştirici Suspense ile ilk olarak, bir bileşenin kodunu ihtiyaç duyulana kadar yüklemeyi ertelemenize olanak tanıyan kod bölme için React.lazy() aracılığıyla tanışır. Örneğin:

import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./MyHeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Bileşen yükleniyor...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

Bu senaryoda, eğer MyHeavyComponent henüz yüklenmemişse, <Suspense> sınırı lazy() tarafından fırlatılan promise'i yakalayacak ve bileşenin kodu hazır olana kadar fallback'i gösterecektir. Buradaki anahtar nokta şudur ki, Suspense, render sırasında fırlatılan promise'leri yakalayarak çalışır.

Bu mekanizma sadece kod yüklemeye özel değildir. Render sırasında çağrılan ve bir promise fırlatan (örneğin, bir kaynak henüz mevcut olmadığı için) herhangi bir işlev, bileşen ağacında daha yukarıdaki bir Suspense sınırı tarafından yakalanabilir. Promise çözüldüğünde, React bileşeni yeniden render etmeye çalışır ve eğer kaynak artık mevcutsa, fallback gizlenir ve gerçek içerik görüntülenir.

Veri Çekme için Suspense'in Temel Kavramları

Veri çekme için Suspense'ten yararlanmak amacıyla birkaç temel prensibi anlamamız gerekiyor:

1. Promise Fırlatma

Promise'leri çözmek için async/await kullanan geleneksel asenkron kodun aksine, Suspense, veri hazır değilse bir promise *fırlatan* bir fonksiyona dayanır. React, böyle bir fonksiyonu çağıran bir bileşeni render etmeye çalıştığında ve veri hala beklemedeyse, promise fırlatılır. React daha sonra o bileşenin ve alt öğelerinin render işlemini 'duraklatır' ve en yakın <Suspense> sınırını arar.

2. Suspense Sınırı

<Suspense> bileşeni, promise'ler için bir hata sınırı (error boundary) gibi davranır. Alt bileşenlerinden (veya onların alt öğelerinden) herhangi biri askıya alındığında (yani bir promise fırlattığında) render edilecek kullanıcı arayüzü olan bir fallback prop'u alır. Alt ağacı içinde fırlatılan tüm promise'ler çözüldüğünde, fallback gerçek içerikle değiştirilir.

Tek bir Suspense sınırı birden fazla asenkron işlemi yönetebilir. Örneğin, aynı <Suspense> sınırı içinde iki bileşeniniz varsa ve her birinin veri çekmesi gerekiyorsa, fallback, *her iki* veri çekme işlemi de tamamlanana kadar gösterilecektir. Bu, kısmi bir kullanıcı arayüzü göstermeyi önler ve daha koordine bir yükleme deneyimi sağlar.

3. Önbellek/Kaynak Yöneticisi (Geliştiricinin Sorumluluğu)

Önemli bir nokta, Suspense'in kendisinin veri çekme veya önbellekleme işlemlerini yönetmemesidir. Bu sadece bir koordinasyon mekanizmasıdır. Suspense'in veri çekme için çalışmasını sağlamak için, şunları yapan bir katmana ihtiyacınız vardır:

Bu 'kaynak yöneticisi' genellikle her kaynağın durumunu (beklemede, çözüldü veya hatalı) depolamak için basit bir önbellek (örneğin, bir Map veya bir nesne) kullanılarak uygulanır. Bunu gösterim amacıyla manuel olarak oluşturabilirsiniz, ancak gerçek bir uygulamada, Suspense ile entegre olan sağlam bir veri çekme kütüphanesi kullanırsınız.

4. Eşzamanlı Mod (React 18'in Geliştirmeleri)

Suspense, React'in eski sürümlerinde de kullanılabilirken, tam gücü Eşzamanlı React (Concurrent React) ile ortaya çıkar (React 18'de createRoot ile varsayılan olarak etkindir). Eşzamanlı Mod, React'in render işini kesmesine, duraklatmasına ve devam etmesine olanak tanır. Bu şu anlama gelir:

Suspense ile Veri Çekme Desenleri

Suspense'in ortaya çıkışıyla veri çekme desenlerinin evrimini keşfedelim.

Desen 1: Çek-Sonra-Render Et (Geleneksel, Suspense ile Sarılmış)

Bu, verinin çekildiği ve ancak ondan sonra bileşenin render edildiği klasik bir yaklaşımdır. Veri için 'promise fırlatma' mekanizmasını doğrudan kullanmasa da, *sonunda* veriyi render eden bir bileşeni bir Suspense sınırıyla sararak bir fallback sağlayabilirsiniz. Bu daha çok, iç veri çekme mekanizması hala geleneksel useEffect tabanlı olsa bile, sonunda hazır olan bileşenler için Suspense'i genel bir yükleme arayüzü düzenleyicisi olarak kullanmakla ilgilidir.

import React, { Suspense, useState, useEffect } from 'react';

function UserDetails({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchUserData = async () => {
      setIsLoading(true);
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      setUser(data);
      setIsLoading(false);
    };
    fetchUserData();
  }, [userId]);

  if (isLoading) {
    return <p>Kullanıcı detayları yükleniyor...</p>;
  }

  return (
    <div>
      <h3>Kullanıcı: {user.name}</h3>
      <p>E-posta: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Çek-Sonra-Render Et Örneği</h1>
      <Suspense fallback={<div>Genel sayfa yükleniyor...</div>}>
        <UserDetails userId={"1"} />
      </Suspense>
    </div>
  );
}

Artıları: Anlaşılması basit, geriye dönük uyumlu. Küresel bir yükleme durumu eklemek için hızlı bir yol olarak kullanılabilir.

Eksileri: UserDetails içindeki tekrarlayan kodu (boilerplate) ortadan kaldırmaz. Bileşenler veriyi sırayla çekerse hala şelalelere (waterfalls) eğilimlidir. Verinin kendisi için Suspense'in 'fırlat-yakala' mekanizmasından gerçekten yararlanmaz.

Desen 2: Render Et-Sonra-Çek (Render İçinde Veri Çekme, Üretim İçin Değil)

Bu desen, öncelikle Suspense ile doğrudan ne yapılmaması gerektiğini göstermek içindir, çünkü dikkatli bir şekilde ele alınmazsa sonsuz döngülere veya performans sorunlarına yol açabilir. Bir bileşenin render aşamasında, *uygun bir önbellekleme mekanizması olmadan* doğrudan veri çekmeye veya askıya alan bir fonksiyonu çağırmayı içerir.

// UYGUN BİR ÖNBELLEKLEME KATMANI OLMADAN BUNU ÜRETİMDE KULLANMAYIN
// Bu, sadece doğrudan bir 'throw' işleminin kavramsal olarak nasıl çalışabileceğini göstermek içindir.

let fetchedData = null;
let dataPromise = null;

function fetchDataSynchronously(url) {
  if (fetchedData) {
    return fetchedData;
  }

  if (!dataPromise) {
    dataPromise = fetch(url)
      .then(res => res.json())
      .then(data => { fetchedData = data; dataPromise = null; return data; })
      .catch(err => { dataPromise = null; throw err; });
  }
  throw dataPromise; // Suspense'in devreye girdiği yer burası
}

function UserDetailsBadExample({ userId }) {
  const user = fetchDataSynchronously(`/api/users/${userId}`);
  return (
    <div>
      <h3>Kullanıcı: {user.name}</h3>
      <p>E-posta: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Render Et-Sonra-Çek (Gösterim Amaçlı, Doğrudan Önerilmez)</h1>
      <Suspense fallback={<div>Kullanıcı yükleniyor...</div>}>
        <UserDetailsBadExample userId={"2"} />
      </Suspense>
    </div>
  );
}

Artıları: Bir bileşenin nasıl doğrudan veri 'isteyebileceğini' ve hazır değilse askıya alabileceğini gösterir.

Eksileri: Üretim için oldukça sorunludur. Bu manuel, küresel fetchedData ve dataPromise sistemi basittir, birden fazla isteği, geçersiz kılmayı veya hata durumlarını sağlam bir şekilde yönetmez. Bu, benimsenecek bir desen değil, 'promise fırlatma' kavramının ilkel bir gösterimidir.

Desen 3: Render Ederken Çekme (Fetch-As-You-Render) (İdeal Suspense Deseni)

Bu, Suspense'in veri çekme için gerçekten sağladığı paradigma kaymasıdır. Bir bileşenin verilerini çekmeden önce render olmasını beklemek veya tüm verileri önceden çekmek yerine, Render Ederken Çekme, veri çekmeye *mümkün olan en kısa sürede*, genellikle render işleminden *önce* veya *eş zamanlı olarak* başlamanız anlamına gelir. Bileşenler daha sonra veriyi bir önbellekten 'okur' ve eğer veri hazır değilse, askıya alınırlar. Temel fikir, veri çekme mantığını bileşenin render mantığından ayırmaktır.

Render Ederken Çekme'yi uygulamak için şunları yapacak bir mekanizmaya ihtiyacınız var:

  1. Bileşenin render fonksiyonu dışında bir veri çekme işlemi başlatmak (örneğin, bir rotaya girildiğinde veya bir düğmeye tıklandığında).
  2. Promise'i veya çözülmüş veriyi bir önbellekte saklamak.
  3. Bileşenlerin bu önbellekten 'okuması' için bir yol sağlamak. Veri henüz mevcut değilse, okuma fonksiyonu bekleyen promise'i fırlatır.

Bu desen şelale (waterfall) sorununu çözer. İki farklı bileşenin veriye ihtiyacı varsa, istekleri paralel olarak başlatılabilir ve kullanıcı arayüzü yalnızca *her ikisi de* hazır olduğunda, tek bir Suspense sınırı tarafından yönetilerek görünür.

Manuel Uygulama (Anlamak İçin)

Altta yatan mekaniği kavramak için basitleştirilmiş bir manuel kaynak yöneticisi oluşturalım. Gerçek bir uygulamada, özel bir kütüphane kullanırdınız.

import React, { Suspense } from 'react';

// --- Basit Önbellek/Kaynak Yöneticisi --- //
const cache = new Map();

function createResource(promise) {
  let status = 'pending';
  let result;
  let suspender = promise.then(
    (r) => {
      status = 'success';
      result = r;
    },
    (e) => {
      status = 'error';
      result = e;
    }
  );

  return {
    read() {
      if (status === 'pending') {
        throw suspender;
      } else if (status === 'error') {
        throw result;
      } else if (status === 'success') {
        return result;
      }
    },
  };
}

function fetchData(key, fetcher) {
  if (!cache.has(key)) {
    cache.set(key, createResource(fetcher()));
  }
  return cache.get(key);
}

// --- Veri Çekme Fonksiyonları --- //
const fetchUserById = (id) => {
  console.log(`Kullanıcı ${id} çekiliyor...`);
  return new Promise(resolve => setTimeout(() => {
    const users = {
      '1': { id: '1', name: 'Alice Smith', email: 'alice@example.com' },
      '2': { id: '2', name: 'Bob Johnson', email: 'bob@example.com' },
      '3': { id: '3', name: 'Charlie Brown', email: 'charlie@example.com' }
    };
    resolve(users[id]);
  }, 1500));
};

const fetchPostsByUserId = (userId) => {
  console.log(`Kullanıcı ${userId} için gönderiler çekiliyor...`);
  return new Promise(resolve => setTimeout(() => {
    const posts = {
      '1': [{ id: 'p1', title: 'İlk Gönderim' }, { id: 'p2', title: 'Seyahat Maceraları' }],
      '2': [{ id: 'p3', title: 'Kodlama İçgörüleri' }],
      '3': [{ id: 'p4', title: 'Küresel Trendler' }, { id: 'p5', title: 'Yerel Mutfak' }]
    };
    resolve(posts[userId] || []);
  }, 2000));
};

// --- Bileşenler --- //
function UserProfile({ userId }) {
  const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  const user = userResource.read(); // Kullanıcı verisi hazır değilse askıya alır

  return (
    <div>
      <h3>Kullanıcı: {user.name}</h3>
      <p>E-posta: {user.email}</p>
    </div>
  );
}

function UserPosts({ userId }) {
  const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
  const posts = postsResource.read(); // Gönderi verisi hazır değilse askıya alır

  return (
    <div>
      <h4>{userId} Kullanıcısının Gönderileri:</h4>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
        {posts.length === 0 && <li>Gönderi bulunamadı.</li>}
      </ul>
    </div>
  );
}

// --- Uygulama --- //
let initialUserResource = null;
let initialPostsResource = null;

function prefetchDataForUser(userId) {
  initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}

// App bileşeni render edilmeden önce bazı verileri önceden çek
prefetchDataForUser('1');

function App() {
  return (
    <div>
      <h1>Suspense ile Render Ederken Çekme</h1>
      <p>Bu, veri çekmenin Suspense tarafından koordine edilerek paralel olarak nasıl gerçekleşebileceğini gösterir.</p>

      <Suspense fallback={<div>Kullanıcı profili ve gönderileri yükleniyor...</div>}>
        <UserProfile userId={"1"} />
        <UserPosts userId={"1"} />
      </Suspense>

      <h2>Başka Bir Bölüm</h2>
      <Suspense fallback={<div>Farklı bir kullanıcı yükleniyor...</div>}>
        <UserProfile userId={"2"} />
      </Suspense>
    </div>
  );
}

Bu örnekte:

Render Ederken Çekme için Kütüphaneler

Sağlam bir kaynak yöneticisini manuel olarak oluşturmak ve sürdürmek karmaşıktır. Neyse ki, birkaç olgun veri çekme kütüphanesi Suspense'i benimsemiş veya benimsemekte olup, savaşta test edilmiş çözümler sunmaktadır:

Bu kütüphaneler, kaynak oluşturma ve yönetme, önbellekleme, yeniden doğrulama, iyimser güncellemeler ve hata yönetimi gibi karmaşıklıkları soyutlayarak Render Ederken Çekme'yi uygulamayı çok daha kolay hale getirir.

Desen 4: Suspense Uyumlu Kütüphanelerle Önceden Çekme (Prefetching)

Önceden çekme, bir kullanıcının yakın gelecekte ihtiyaç duyması muhtemel olan veriyi, henüz açıkça talep etmeden önce proaktif olarak çektiğiniz güçlü bir optimizasyondur. Bu, algılanan performansı önemli ölçüde artırabilir.

Suspense uyumlu kütüphanelerle, önceden çekme sorunsuz hale gelir. Bir bağlantının üzerine gelme veya bir düğmenin üzerine fareyi getirme gibi, arayüzü hemen değiştirmeyen kullanıcı etkileşimlerinde veri çekme işlemlerini tetikleyebilirsiniz.

import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// Bunların API çağrılarınız olduğunu varsayın
const fetchProductById = async (id) => {
  console.log(`Ürün ${id} çekiliyor...`);
  return new Promise(resolve => setTimeout(() => {
    const products = {
      'A001': { id: 'A001', name: 'Global Widget X', price: 29.99, description: 'Uluslararası kullanım için çok yönlü bir widget.' },
      'B002': { id: 'B002', name: 'Universal Gadget Y', price: 149.99, description: 'Son teknoloji gadget, dünya çapında seviliyor.' },
    };
    resolve(products[id]);
  }, 1000));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true, // Varsayılan olarak tüm sorgular için Suspense'i etkinleştir
    },
  },
});

function ProductDetails({ productId }) {
  const { data: product } = useQuery({
    queryKey: ['product', productId],
    queryFn: () => fetchProductById(productId),
  });

  return (
    <div style={{"border": "1px solid #ccc", "padding": "15px", "margin": "10px 0"}}>
      <h3>{product.name}</h3>
      <p>Fiyat: ${product.price.toFixed(2)}</p>
      <p>{product.description}</p>
    </div>
  );
}

function ProductList() {
  const handleProductHover = (productId) => {
    // Kullanıcı bir ürün bağlantısının üzerine geldiğinde veriyi önceden çek
    queryClient.prefetchQuery({
      queryKey: ['product', productId],
      queryFn: () => fetchProductById(productId),
    });
    console.log(`Ürün ${productId} önceden çekiliyor`);
  };

  return (
    <div>
      <h2>Mevcut Ürünler:</h2>
      <ul>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('A001')}
             onClick={(e) => { e.preventDefault(); /* Yönlendir veya detayları göster */ }}
          >Global Widget X (A001)</a>
        </li>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('B002')}
             onClick={(e) => { e.preventDefault(); /* Yönlendir veya detayları göster */ }}
          >Universal Gadget Y (B002)</a>
        </li>
      </ul>
      <p>Önceden çekme işlemini görmek için bir ürün bağlantısının üzerine gelin. Gözlemlemek için ağ (network) sekmesini açın.</p>
    </div>
  );
}

function App() {
  const [showProductA, setShowProductA] = React.useState(false);
  const [showProductB, setShowProductB] = React.useState(false);

  return (
    <QueryClientProvider client={queryClient}>
      <h1>React Suspense ile Önceden Çekme (React Query)</h1>
      <ProductList />

      <button onClick={() => setShowProductA(true)}>Global Widget X'i Göster</button>
      <button onClick={() => setShowProductB(true)}>Universal Gadget Y'yi Göster</button>

      {showProductA && (
        <Suspense fallback={<p>Global Widget X yükleniyor...</p>}>
          <ProductDetails productId="A001" />
        </Suspense>
      )}
      {showProductB && (
        <Suspense fallback={<p>Universal Gadget Y yükleniyor...</p>}>
          <ProductDetails productId="B002" />
        </Suspense>
      )}
    </QueryClientProvider>
  );
}

Bu örnekte, bir ürün bağlantısının üzerine gelmek `queryClient.prefetchQuery` fonksiyonunu tetikler, bu da veri çekme işlemini arka planda başlatır. Eğer kullanıcı daha sonra ürün detaylarını göstermek için düğmeye tıklarsa ve veri önceden çekme sayesinde zaten önbellekteyse, bileşen askıya alınmadan anında render edilecektir. Eğer önceden çekme hala devam ediyorsa veya başlatılmadıysa, Suspense veri hazır olana kadar fallback'i gösterecektir.

Suspense ve Hata Sınırları (Error Boundaries) ile Hata Yönetimi

Suspense, bir fallback göstererek 'yükleme' durumunu ele alırken, 'hata' durumlarını doğrudan yönetmez. Eğer askıya alan bir bileşen tarafından fırlatılan bir promise reddedilirse (yani, veri çekme başarısız olursa), bu hata bileşen ağacında yukarı doğru yayılır. Bu hataları zarif bir şekilde ele almak ve uygun bir arayüz göstermek için Hata Sınırları (Error Boundaries) kullanmanız gerekir.

Bir Hata Sınırı, componentDidCatch veya static getDerivedStateFromError yaşam döngüsü metotlarından birini uygulayan bir React bileşenidir. Çocuk bileşen ağacının herhangi bir yerindeki JavaScript hatalarını yakalar, buna Suspense'in normalde beklemedeyken yakalayacağı promise'ler tarafından fırlatılan hatalar da dahildir.

import React, { Suspense, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// --- Hata Sınırı Bileşeni --- //
class MyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Bir sonraki render'da yedek arayüzün gösterilmesi için state'i güncelle.
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // Hatayı bir hata raporlama servisine de kaydedebilirsiniz
    console.error("Bir hata yakalandı:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // İstediğiniz özel bir yedek arayüzü render edebilirsiniz
      return (
        <div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
          <h2>Bir şeyler ters gitti!</h2>
          <p>{this.state.error && this.state.error.message}</p>
          <p>Lütfen sayfayı yenilemeyi deneyin veya destekle iletişime geçin.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>Tekrar Dene</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// --- Veri Çekme (hata potansiyeliyle) --- //
const fetchItemById = async (id) => {
  console.log(`Öğe ${id} çekilmeye çalışılıyor...`);
  return new Promise((resolve, reject) => setTimeout(() => {
    if (id === 'error-item') {
      reject(new Error('Öğe yüklenemedi: Ağa ulaşılamıyor veya öğe bulunamadı.'));
    } else if (id === 'slow-item') {
      resolve({ id: 'slow-item', name: 'Yavaş Teslim Edildi', data: 'Bu öğe biraz zaman aldı ama ulaştı!', status: 'success' });
    } else {
      resolve({ id, name: `Öğe ${id}`, data: `Öğe ${id} için veri` });
    }
  }, id === 'slow-item' ? 3000 : 800));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      retry: false, // Gösterim amacıyla, hatanın anında olması için yeniden denemeyi devre dışı bırak
    },
  },
});

function DisplayItem({ itemId }) {
  const { data: item } = useQuery({
    queryKey: ['item', itemId],
    queryFn: () => fetchItemById(itemId),
  });

  return (
    <div>
      <h3>Öğe Detayları:</h3>
      <p>ID: {item.id}</p>
      <p>İsim: {item.name}</p>
      <p>Veri: {item.data}</p>
    </div>
  );
}

function App() {
  const [fetchType, setFetchType] = useState('normal-item');

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Suspense ve Hata Sınırları</h1>

      <div>
        <button onClick={() => setFetchType('normal-item')}>Normal Öğe Çek</button>
        <button onClick={() => setFetchType('slow-item')}>Yavaş Öğe Çek</button>
        <button onClick={() => setFetchType('error-item')}>Hatalı Öğe Çek</button>
      </div>

      <MyErrorBoundary>
        <Suspense fallback={<p>Öğe Suspense aracılığıyla yükleniyor...</p>}>
          <DisplayItem itemId={fetchType} />
        </Suspense>
      <MyErrorBoundary>
    </QueryClientProvider>
  );
}

Suspense sınırınızı (veya askıya alınabilecek bileşenleri) bir Hata Sınırı ile sararak, veri çekme sırasındaki ağ arızalarının veya sunucu hatalarının yakalanıp zarif bir şekilde ele alınmasını sağlarsınız, bu da tüm uygulamanın çökmesini önler. Bu, kullanıcıların sorunu anlamalarına ve potansiyel olarak yeniden denemelerine olanak tanıyan sağlam ve kullanıcı dostu bir deneyim sağlar.

Suspense ile Durum Yönetimi ve Veri Geçersiz Kılma

React Suspense'in öncelikle asenkron kaynakların ilk yükleme durumunu ele aldığını açıklığa kavuşturmak önemlidir. İstemci tarafı önbelleğini doğası gereği yönetmez, veri geçersiz kılmayı işlemez veya mutasyonları (oluşturma, güncelleme, silme işlemleri) ve bunların sonraki arayüz güncellemelerini düzenlemez.

İşte bu noktada Suspense uyumlu veri çekme kütüphaneleri (React Query, SWR, Apollo Client, Relay) vazgeçilmez hale gelir. Suspense'i şu özelliklerle tamamlarlar:

Sağlam bir veri çekme kütüphanesi olmadan, bu özellikleri manuel bir Suspense kaynak yöneticisinin üzerine uygulamak önemli bir çaba gerektirir ve esasen kendi veri çekme çerçevenizi oluşturmanızı gerektirir.

Pratik Hususlar ve En İyi Uygulamalar

Veri çekme için Suspense'i benimsemek önemli bir mimari karardır. İşte küresel bir uygulama için bazı pratik hususlar:

1. Her Veri Suspense'e İhtiyaç Duymaz

Suspense, bir bileşenin ilk render'ını doğrudan etkileyen kritik veriler için idealdir. Kritik olmayan veriler, arka plan çekimleri veya güçlü bir görsel etki olmadan tembel bir şekilde yüklenebilen veriler için, geleneksel useEffect veya önceden render etme hala uygun olabilir. Suspense'in aşırı kullanımı daha az granüler bir yükleme deneyimine yol açabilir, çünkü tek bir Suspense sınırı *tüm* alt öğelerinin çözülmesini bekler.

2. Suspense Sınırlarının Granülerliği

<Suspense> sınırlarınızı düşünceli bir şekilde yerleştirin. Uygulamanızın en üstündeki tek, büyük bir sınır, tüm sayfayı bir yükleme göstergesinin arkasına gizleyebilir, bu da sinir bozucu olabilir. Daha küçük, daha granüler sınırlar, sayfanızın farklı bölümlerinin bağımsız olarak yüklenmesine olanak tanır, bu da daha aşamalı ve duyarlı bir deneyim sağlar. Örneğin, bir kullanıcı profili bileşeni etrafında bir sınır ve önerilen ürünler listesi etrafında başka bir sınır.

<div>
  <h1>Ürün Sayfası</h1>
  <Suspense fallback={<p>Ana ürün detayları yükleniyor...</p>}>
    <ProductDetails id="prod123" />
  </Suspense>

  <hr />

  <h2>İlgili Ürünler</h2>
  <Suspense fallback={<p>İlgili ürünler yükleniyor...</p>}>
    <RelatedProducts category="electronics" />
  </Suspense>
</div>

Bu yaklaşım, kullanıcıların ilgili ürünler hala yükleniyor olsa bile ana ürün detaylarını görebileceği anlamına gelir.

3. Sunucu Tarafı Render (SSR) ve Akışlı HTML

React 18'in yeni akışlı SSR API'leri (renderToPipeableStream) Suspense ile tam entegre çalışır. Bu, sunucunuzun, sayfanın bazı bölümleri (veri bağımlı bileşenler gibi) hala yükleniyor olsa bile, hazır olur olmaz HTML göndermesine olanak tanır. Sunucu bir yer tutucu (Suspense fallback'inden) akıtabilir ve ardından veri çözüldüğünde gerçek içeriği akıtabilir, bu da tam bir istemci tarafı yeniden render gerektirmez. Bu, çeşitli ağ koşullarındaki küresel kullanıcılar için algılanan yükleme performansını önemli ölçüde artırır.

4. Aşamalı Benimseme

Suspense kullanmak için tüm uygulamanızı yeniden yazmanıza gerek yok. Onu aşamalı olarak, bildirimsel yükleme desenlerinden en çok fayda sağlayacak yeni özellikler veya bileşenlerle başlayarak tanıtabilirsiniz.

5. Araçlar ve Hata Ayıklama

Suspense bileşen mantığını basitleştirirken, hata ayıklama farklı olabilir. React DevTools, Suspense sınırları ve durumları hakkında içgörüler sağlar. Seçtiğiniz veri çekme kütüphanesinin iç durumunu nasıl ortaya çıkardığına aşina olun (örneğin, React Query Devtools).

6. Suspense Fallback'leri için Zaman Aşımları

Çok uzun yükleme süreleri için, Suspense fallback'inize bir zaman aşımı eklemek veya belirli bir gecikmeden sonra daha ayrıntılı bir yükleme göstergesine geçmek isteyebilirsiniz. React 18'deki useDeferredValue ve useTransition kancaları, bu daha incelikli yükleme durumlarını yönetmeye yardımcı olabilir, bu da yeni veri çekilirken arayüzün 'eski' bir sürümünü göstermenize veya acil olmayan güncellemeleri ertelemenize olanak tanır.

React'te Veri Çekmenin Geleceği: React Sunucu Bileşenleri ve Ötesi

React'te veri çekme yolculuğu istemci tarafı Suspense ile bitmiyor. React Sunucu Bileşenleri (RSC), istemci ve sunucu arasındaki çizgileri bulanıklaştırmayı ve veri çekmeyi daha da optimize etmeyi vaat eden önemli bir evrimi temsil ediyor.

React olgunlaşmaya devam ettikçe, Suspense yüksek performanslı, kullanıcı dostu ve sürdürülebilir uygulamalar oluşturma bulmacasının giderek daha merkezi bir parçası olacaktır. Geliştiricileri, asenkron işlemleri ele almak için daha bildirimsel ve dayanıklı bir yola iter, karmaşıklığı bireysel bileşenlerden iyi yönetilen bir veri katmanına taşır.

Sonuç

Başlangıçta kod bölme için bir özellik olan React Suspense, veri çekme için dönüştürücü bir araç haline geldi. Render Ederken Çekme (Fetch-As-You-Render) desenini benimseyerek ve Suspense uyumlu kütüphanelerden yararlanarak, geliştiriciler uygulamalarının kullanıcı deneyimini önemli ölçüde iyileştirebilir, yükleme şelalelerini ortadan kaldırabilir, bileşen mantığını basitleştirebilir ve pürüzsüz, koordine yükleme durumları sağlayabilir. Sağlam hata yönetimi için Hata Sınırları ve React Sunucu Bileşenlerinin gelecekteki vaadi ile birleştiğinde, Suspense bize sadece performanslı ve dayanıklı değil, aynı zamanda dünya genelindeki kullanıcılar için doğası gereği daha keyifli uygulamalar oluşturma gücü verir. Suspense odaklı bir veri çekme paradigmasına geçiş kavramsal bir ayarlama gerektirir, ancak kod netliği, performans ve kullanıcı memnuniyeti açısından faydaları önemli ve yatırıma kesinlikle değer.