Türkçe

Veri çekme işlemleri için React Suspense'te ustalaşın. Yükleme durumlarını bildirimsel olarak yönetmeyi, geçişlerle kullanıcı deneyimini iyileştirmeyi ve Hata Sınırları ile hataları ele almayı öğrenin.

React Suspense Sınırları: Bildirimsel Yükleme Durumu Yönetimine Derinlemesine Bir Bakış

Modern web geliştirme dünyasında, sorunsuz ve duyarlı bir kullanıcı deneyimi yaratmak her şeyden önemlidir. Geliştiricilerin karşılaştığı en kalıcı zorluklardan biri yükleme durumlarını yönetmektir. Bir kullanıcı profili için veri çekmekten bir uygulamanın yeni bir bölümünü yüklemeye kadar, bekleme anları kritiktir. Tarihsel olarak bu, bileşenlerimize dağılmış isLoading, isFetching ve hasError gibi bir dizi boolean bayrağı içeren karmaşık bir yapı gerektiriyordu. Bu zorunlu yaklaşım, kodumuzu karmaşıklaştırır, mantığı zorlaştırır ve yarış koşulları gibi hataların sıkça kaynağıdır.

İşte bu noktada React Suspense devreye giriyor. Başlangıçta React.lazy() ile kod bölme (code-splitting) için tanıtılmış olsa da, yetenekleri React 18 ile birlikte özellikle veri çekme gibi asenkron işlemleri yönetmek için güçlü, birinci sınıf bir mekanizma haline gelecek şekilde önemli ölçüde genişledi. Suspense, yükleme durumlarını bildirimsel (declarative) bir şekilde yönetmemize olanak tanır ve bileşenlerimizi yazma ve onlar hakkında akıl yürütme şeklimizi temelden değiştirir. Bileşenlerimiz "Yükleniyor muyum?" diye sormak yerine, basitçe "Render olmak için bu veriye ihtiyacım var. Ben beklerken, lütfen bu yedek arayüzü göster." diyebilir.

Bu kapsamlı kılavuz, sizi geleneksel durum yönetimi yöntemlerinden React Suspense'in bildirimsel paradigmasına doğru bir yolculuğa çıkaracak. Suspense sınırlarının ne olduğunu, hem kod bölme hem de veri çekme için nasıl çalıştıklarını ve kullanıcılarınızı sinirlendirmek yerine onları memnun eden karmaşık yükleme arayüzlerini nasıl düzenleyeceğinizi keşfedeceğiz.

Eski Yöntem: Manuel Yükleme Durumlarının Angaryası

Suspense'in zarafetini tam olarak takdir edebilmek için, çözdüğü sorunu anlamak çok önemlidir. useEffect ve useState hook'larını kullanarak veri çeken tipik bir bileşene göz atalım.

Kullanıcı verilerini alması ve görüntülemesi gereken bir bileşen hayal edin:


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(() => {
    // Yeni userId için durumu sıfırla
    setIsLoading(true);
    setUser(null);
    setError(null);

    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Ağ yanıtı uygun değildi');
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // userId değiştiğinde yeniden veri çek

  if (isLoading) {
    return <p>Profil yükleniyor...</p>;
  }

  if (error) {
    return <p>Hata: {error.message}</p>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>E-posta: {user.email}</p>
    </div>
  );
}

Bu desen işlevseldir, ancak birkaç dezavantajı vardır:

Karşınızda React Suspense: Bir Paradigma Değişimi

Suspense, bu modeli tersine çevirir. Bileşenin yükleme durumunu dahili olarak yönetmesi yerine, asenkron bir işleme olan bağımlılığını doğrudan React'e iletir. İhtiyaç duyduğu veri henüz mevcut değilse, bileşen render işlemini "askıya alır".

Bir bileşen askıya alındığında, React en yakın Suspense Sınırını bulmak için bileşen ağacında yukarı doğru ilerler. Bir Suspense Sınırı, ağacınızda <Suspense> kullanarak tanımladığınız bir bileşendir. Bu sınır, içindeki tüm bileşenler veri bağımlılıklarını çözene kadar bir yedek arayüz (spinner veya iskelet yükleyici gibi) render edecektir.

Temel fikir, veri bağımlılığını ona ihtiyaç duyan bileşenle aynı yerde tutarken, yükleme arayüzünü bileşen ağacında daha yüksek bir seviyede merkezileştirmektir. Bu, bileşen mantığını temizler ve kullanıcının yükleme deneyimi üzerinde size güçlü bir kontrol sağlar.

Bir Bileşen Nasıl "Askıya Alınır"?

Suspense'in arkasındaki sihir, ilk başta alışılmadık görünebilecek bir desende yatar: bir Promise fırlatmak. Suspense uyumlu bir veri kaynağı şu şekilde çalışır:

  1. Bir bileşen veri istediğinde, veri kaynağı verinin önbellekte olup olmadığını kontrol eder.
  2. Veri mevcutsa, senkron olarak döndürür.
  3. Veri mevcut değilse (yani, şu anda çekiliyorsa), veri kaynağı devam eden veri çekme isteğini temsil eden Promise'i fırlatır.

React, bu fırlatılan Promise'i yakalar. Uygulamanızı çökertmez. Bunun yerine, bunu bir sinyal olarak yorumlar: "Bu bileşen henüz render olmaya hazır değil. Duraklat ve bir yedek arayüz göstermek için üzerinde bir Suspense sınırı ara." Promise çözüldüğünde, React bileşeni yeniden render etmeyi dener, bu sefer verisini alır ve başarıyla render olur.

<Suspense> Sınırı: Yükleme Arayüzü Bildiriciniz

<Suspense> bileşeni bu desenin kalbidir. Kullanımı inanılmaz derecede basittir ve tek bir zorunlu prop alır: fallback.


import { Suspense } from 'react';

function App() {
  return (
    <div>
      <h1>Uygulamam</h1>
      <Suspense fallback={<p>İçerik yükleniyor...</p>}>
        <VeriCekenBilesen />
      </Suspense>
    </div>
  );
}

Bu örnekte, eğer VeriCekenBilesen askıya alınırsa, kullanıcı veri hazır olana kadar "İçerik yükleniyor..." mesajını görecektir. Yedek arayüz, basit bir metinden karmaşık bir iskelet bileşenine kadar geçerli herhangi bir React düğümü olabilir.

Klasik Kullanım Alanı: React.lazy() ile Kod Bölme

Suspense'in en yerleşik kullanımı kod bölme (code splitting) içindir. Bir bileşenin JavaScript'inin yüklenmesini, gerçekten ihtiyaç duyulana kadar ertelemenize olanak tanır.


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

// Bu bileşenin kodu başlangıç paketinde (bundle) olmayacak.
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h2>Hemen yüklenen bazı içerikler</h2>
      <Suspense fallback={<div>Bileşen yükleniyor...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

Burada, React yalnızca ilk render etmeye çalıştığında HeavyComponent için JavaScript'i getirecektir. Getirilip ayrıştırılırken, Suspense yedeği görüntülenir. Bu, başlangıç sayfa yükleme sürelerini iyileştirmek için güçlü bir tekniktir.

Modern Ufuk: Suspense ile Veri Çekme

React, Suspense mekanizmasını sağlarken, belirli bir veri çekme istemcisi sağlamaz. Veri çekme için Suspense kullanmak için, onunla entegre olan bir veri kaynağına ihtiyacınız vardır (yani, veri beklemedeyken bir Promise fırlatan bir kaynak).

Relay ve Next.js gibi framework'lerin Suspense için yerleşik, birinci sınıf desteği vardır. TanStack Query (eski adıyla React Query) ve SWR gibi popüler veri çekme kütüphaneleri de deneysel veya tam destek sunar.

Kavramı anlamak için, fetch API'sini Suspense uyumlu hale getirmek üzere çok basit, kavramsal bir sarmalayıcı oluşturalım. Not: Bu, eğitim amaçlı basitleştirilmiş bir örnektir ve üretim ortamı için hazır değildir. Uygun önbellekleme ve hata yönetimi inceliklerinden yoksundur.


// data-fetcher.js
// Sonuçları saklamak için basit bir önbellek
const cache = new Map();

export function fetchData(url) {
  if (!cache.has(url)) {
    cache.set(url, { status: 'pending', promise: fetchAndCache(url) });
  }

  const record = cache.get(url);

  if (record.status === 'pending') {
    throw record.promise; // Büyü burada!
  }
  if (record.status === 'error') {
    throw record.error;
  }
  if (record.status === 'success') {
    return record.data;
  }
}

async function fetchAndCache(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Veri çekme ${response.status} durumuyla başarısız oldu`);
    }
    const data = await response.json();
    cache.set(url, { status: 'success', data });
  } catch (e) {
    cache.set(url, { status: 'error', error: e });
  }
}

Bu sarmalayıcı her URL için basit bir durumu korur. fetchData çağrıldığında, durumu kontrol eder. Eğer beklemedeyse, promise'i fırlatır. Başarılıysa, veriyi döndürür. Şimdi, UserProfile bileşenimizi bunu kullanarak yeniden yazalım.


// UserProfile.js
import React, { Suspense } from 'react';
import { fetchData } from './data-fetcher';

// Veriyi gerçekten kullanan bileşen
function ProfileDetails({ userId }) {
  // Veriyi okumayı dene. Eğer hazır değilse, bu askıya alınacak.
  const user = fetchData(`https://api.example.com/users/${userId}`);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>E-posta: {user.email}</p>
    </div>
  );
}

// Yükleme durumu arayüzünü tanımlayan ebeveyn bileşen
export function UserProfile({ userId }) {
  return (
    <Suspense fallback={<p>Profil yükleniyor...</p>}>
      <ProfileDetails userId={userId} />
    </Suspense>
  );
}

Farka bakın! ProfileDetails bileşeni temiz ve yalnızca veriyi render etmeye odaklanmış durumda. Hiçbir isLoading veya error durumu yok. Sadece ihtiyacı olan veriyi talep ediyor. Bir yükleme göstergesi gösterme sorumluluğu, ne beklerken ne gösterileceğini bildirimsel olarak belirten ebeveyn bileşen UserProfile'a taşındı.

Karmaşık Yükleme Durumlarını Yönetme

Suspense'in gerçek gücü, birden çok asenkron bağımlılığa sahip karmaşık arayüzler oluşturduğunuzda ortaya çıkar.

Kademeli Bir Arayüz için İç İçe Suspense Sınırları

Daha rafine bir yükleme deneyimi oluşturmak için Suspense sınırlarını iç içe kullanabilirsiniz. Bir kenar çubuğu, bir ana içerik alanı ve son etkinliklerin bir listesi olan bir kontrol paneli sayfası düşünün. Bunların her biri kendi veri çekme işlemini gerektirebilir.


function DashboardPage() {
  return (
    <div>
      <h1>Kontrol Paneli</h1>
      <div className="layout">
        <Suspense fallback={<p>Navigasyon yükleniyor...</p>}>
          <Sidebar />
        </Suspense>

        <main>
          <Suspense fallback={<ProfileSkeleton />}>
            <MainContent />
          </Suspense>

          <Suspense fallback={<ActivityFeedSkeleton />}>
            <ActivityFeed />
          </Suspense>
        </main>
      </div>
    </div>
  );
}

Bu yapıyla:

Bu, kullanıcıya yararlı içeriği mümkün olan en kısa sürede göstermenizi sağlar ve algılanan performansı önemli ölçüde artırır.

Arayüz "Patlamasından" Kaçınma

Bazen kademeli yaklaşım, birden fazla spinner'ın art arda hızlı bir şekilde görünüp kaybolduğu, genellikle "patlama" (popcorning) olarak adlandırılan rahatsız edici bir etkiye yol açabilir. Bunu çözmek için Suspense sınırını ağaçta daha yukarı taşıyabilirsiniz.


function DashboardPage() {
  return (
    <div>
      <h1>Kontrol Paneli</h1>
      <Suspense fallback={<DashboardSkeleton />}>
        <div className="layout">
          <Sidebar />
          <main>
            <MainContent />
            <ActivityFeed />
          </main>
        </div>
      </Suspense>
    </div>
  );
}

Bu versiyonda, tüm alt bileşenler (Sidebar, MainContent, ActivityFeed) verilerini hazır edene kadar tek bir DashboardSkeleton gösterilir. Ardından tüm kontrol paneli bir kerede görünür. İç içe sınırlar ile tek bir üst düzey sınır arasındaki seçim, Suspense'in uygulanmasını çok kolaylaştırdığı bir kullanıcı deneyimi tasarım kararıdır.

Hata Sınırları ile Hata Yönetimi

Suspense, bir promise'in bekleme (pending) durumunu yönetir, peki ya reddedilme (rejected) durumu? Bir bileşen tarafından fırlatılan promise reddedilirse (örneğin, bir ağ hatası), React'teki diğer herhangi bir render hatası gibi ele alınacaktır.

Çözüm, Hata Sınırları (Error Boundaries) kullanmaktır. Bir Hata Sınırı, özel bir yaşam döngüsü metodu olan componentDidCatch() veya statik bir metod olan getDerivedStateFromError()'ı tanımlayan bir sınıf bileşenidir. Alt bileşen ağacının herhangi bir yerindeki JavaScript hatalarını yakalar, bu hataları kaydeder ve bir yedek arayüz görüntüler.

İşte basit bir Hata Sınırı bileşeni:


import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Bir sonraki render'ın yedek arayüzü göstermesi için durumu güncelle.
    return { hasError: true, error: 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 <h1>Bir şeyler ters gitti. Lütfen tekrar deneyin.</h1>;
    }

    return this.props.children; 
  }
}

Daha sonra Hata Sınırlarını Suspense ile birleştirerek bekleme, başarı ve hata olmak üzere üç durumu da yöneten sağlam bir sistem oluşturabilirsiniz.


import { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
import { UserProfile } from './UserProfile';

function App() {
  return (
    <div>
      <h2>Kullanıcı Bilgileri</h2>
      <ErrorBoundary>
        <Suspense fallback={<p>Yükleniyor...</p>}>
          <UserProfile userId={123} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Bu desenle, eğer UserProfile içindeki veri çekme işlemi başarılı olursa, profil gösterilir. Eğer beklemedeyse, Suspense yedeği gösterilir. Eğer başarısız olursa, Hata Sınırının yedeği gösterilir. Mantık bildirimsel, birleştirilebilir ve anlaşılması kolaydır.

Geçişler: Engellemeyen Arayüz Güncellemelerinin Anahtarı

Yapbozun son bir parçası daha var. Farklı bir kullanıcı profilini görüntülemek için "Sonraki" düğmesine tıklamak gibi yeni bir veri çekmeyi tetikleyen bir kullanıcı etkileşimini düşünün. Yukarıdaki kurulumla, düğmeye tıklandığı ve userId prop'u değiştiği anda, UserProfile bileşeni tekrar askıya alınacaktır. Bu, o anda görünür olan profilin kaybolacağı ve yerine yükleme yedeğinin geçeceği anlamına gelir. Bu, ani ve rahatsız edici hissedilebilir.

İşte burada geçişler (transitions) devreye giriyor. Geçişler, React 18'deki belirli durum güncellemelerini acil olmayan olarak işaretlemenizi sağlayan yeni bir özelliktir. Bir durum güncellemesi bir geçiş içine sarıldığında, React yeni içeriği arka planda hazırlarken eski arayüzü (eski içeriği) görüntülemeye devam eder. Arayüz güncellemesini yalnızca yeni içerik görüntülenmeye hazır olduğunda gerçekleştirir.

Bunun için birincil API useTransition hook'udur.


import React, { useState, useTransition, Suspense } from 'react';
import { UserProfile } from './UserProfile';

function ProfileSwitcher() {
  const [userId, setUserId] = useState(1);
  const [isPending, startTransition] = useTransition();

  const handleNextClick = () => {
    startTransition(() => {
      setUserId(id => id + 1);
    });
  };

  return (
    <div>
      <button onClick={handleNextClick} disabled={isPending}>
        Sonraki Kullanıcı
      </button>

      {isPending && <span> Yeni profil yükleniyor...</span>}

      <ErrorBoundary>
        <Suspense fallback={<p>Başlangıç profili yükleniyor...</p>}>
          <UserProfile userId={userId} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Şimdi olanlar şunlardır:

  1. userId: 1 için başlangıç profili yüklenir ve Suspense yedeği gösterilir.
  2. Kullanıcı "Sonraki Kullanıcı"ya tıklar.
  3. setUserId çağrısı startTransition içine sarılır.
  4. React, UserProfile'ı bellekte yeni userId olan 2 ile render etmeye başlar. Bu, onun askıya alınmasına neden olur.
  5. En önemlisi, React Suspense yedeğini göstermek yerine, eski arayüzü (kullanıcı 1'in profilini) ekranda tutmaya devam eder.
  6. useTransition tarafından döndürülen isPending boolean'ı true olur, bu da eski içeriği kaldırmadan ince, satır içi bir yükleme göstergesi göstermemizi sağlar.
  7. Kullanıcı 2 için veri çekildiğinde ve UserProfile başarıyla render olabildiğinde, React güncellemeyi uygular ve yeni profil sorunsuz bir şekilde görünür.

Geçişler, son kontrol katmanını sağlar ve asla rahatsız edici hissettirmeyen sofistike ve kullanıcı dostu yükleme deneyimleri oluşturmanıza olanak tanır.

En İyi Uygulamalar ve Genel Değerlendirmeler

Sonuç

React Suspense, sadece yeni bir özellikten daha fazlasını temsil eder; React uygulamalarında asenkronluğa yaklaşımımızda temel bir evrimdir. Manuel, zorunlu yükleme bayraklarından uzaklaşıp bildirimsel bir modeli benimseyerek, daha temiz, daha dayanıklı ve birleştirmesi daha kolay bileşenler yazabiliriz.

Bekleme durumları için <Suspense>, hata durumları için Hata Sınırları ve sorunsuz güncellemeler için useTransition'ı birleştirerek, elinizin altında eksiksiz ve güçlü bir araç setine sahip olursunuz. Basit yükleme spinner'larından karmaşık, kademeli kontrol paneli gösterimlerine kadar her şeyi minimal, öngörülebilir kodla düzenleyebilirsiniz. Suspense'i projelerinize entegre etmeye başladığınızda, yalnızca uygulamanızın performansını ve kullanıcı deneyimini iyileştirmekle kalmayıp, aynı zamanda durum yönetimi mantığınızı önemli ölçüde basitleştirdiğini ve gerçekten önemli olan şeye odaklanmanıza olanak tanıdığını göreceksiniz: harika özellikler oluşturmak.