İçeriğe geç
Sedat Demir
Geri dön

TanStack Query v5: staleTime Infinity Pattern ve Suspense Entegrasyonu Rehberi

TanStack Query v5: staleTime Infinity Pattern ve Suspense Entegrasyonu Rehberi

Modern React uygulamalarında sunucu state yönetimi, uygulamanın performansını ve kullanıcı deneyimini doğrudan etkileyen kritik bir konudur. TanStack Query (eski adıyla React Query), bu alanda altın standart haline gelmiş bir kütüphanedir. V5 sürümüyle birlikte gelen yenilikler — özellikle staleTime: Infinity pattern'i ve birinci sınıf Suspense desteği — veri yönetimi stratejilerinizi kökten değiştirebilir.

Bu yazıda, bu iki güçlü pattern'i neden, ne zaman ve nasıl kullanacağınızı gerçek dünya örnekleriyle ele alacağız.


TanStack Query v5'te Neler Değişti?

TanStack Query v5, önceki sürümlerden önemli breaking change'ler içerir. Konumuza doğrudan etki eden değişikliklere bakalım:

Şimdi temel kurulumu hatırlayalım:

npm install @tanstack/react-query@5
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApplication />
    </QueryClientProvider>
  );
}

staleTime Nedir ve Neden Önemlidir?

TanStack Query'nin cache mekanizmasını anlamak için iki temel kavramı bilmek gerekir:

Kavram Varsayılan Açıklama
staleTime 0 Verinin "taze" kabul edilme süresi (ms)
gcTime 5 dakika Verinin cache'ten tamamen silinme süresi

Varsayılan olarak staleTime: 0 demek, her veri fetch edildikten hemen sonra "bayat" (stale) kabul edilir. Bu durumda TanStack Query, component mount olduğunda, pencere odaklandığında veya network yeniden bağlandığında otomatik olarak arka planda refetch yapar.

Bu davranış birçok senaryo için mükemmeldir — ama her durum için değil.

Gereksiz Refetch Sorunu

Şu senaryoyu düşünün: Bir e-ticaret sitesinde ürün kategorileri listesi nadiren değişir. Kullanıcı sayfalar arasında her gezindiğinde kategori listesini yeniden fetch etmek gereksiz network trafiği yaratır:

// ❌ Her component mount'ta ve window focus'ta refetch yapılır
function CategoryList() {
  const { data } = useQuery({
    queryKey: ['categories'],
    queryFn: fetchCategories,
    // staleTime varsayılan: 0
  });

  return (
    <ul>
      {data?.map(cat => <li key={cat.id}>{cat.name}</li>)}
    </ul>
  );
}

staleTime: Infinity Pattern'i

staleTime: Infinity pattern'i, verinin asla otomatik olarak bayat kabul edilmemesini sağlar. Bu, TanStack Query'nin otomatik refetch mekanizmalarını tamamen devre dışı bırakır ve veriyi yalnızca siz açıkça istediğinizde yeniden getirir.

// ✅ Veri bir kez fetch edilir, otomatik refetch yapılmaz
function CategoryList() {
  const { data } = useQuery({
    queryKey: ['categories'],
    queryFn: fetchCategories,
    staleTime: Infinity,
  });

  return (
    <ul>
      {data?.map(cat => <li key={cat.id}>{cat.name}</li>)}
    </ul>
  );
}

Ne Zaman staleTime: Infinity Kullanmalısınız?

Bu pattern aşağıdaki senaryolarda idealdir:

  1. Nadiren değişen veriler — Ülke listeleri, para birimleri, uygulama konfigürasyonları
  2. Kullanıcı oturumu boyunca sabit kalması gereken veriler — Kullanıcı profili, yetkilendirme bilgileri
  3. Hesaplama maliyeti yüksek API çağrıları — Raporlar, istatistikler
  4. Prefetch edilmiş veriler — Route geçişlerinde önceden yüklenen veriler

Global staleTime Konfigürasyonu

Her query'de tekrar tekrar yazmak yerine, global olarak tanımlayabilirsiniz:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // Tüm query'ler için 5 dakika
    },
  },
});

Veya belirli query'ler için farklı stratejiler belirleyebilirsiniz:

// Query factory pattern ile staleTime yönetimi
const categoryQueries = {
  all: () => ({
    queryKey: ['categories'] as const,
    queryFn: fetchCategories,
    staleTime: Infinity, // Kategoriler asla bayatlamaz
  }),
  detail: (id: string) => ({
    queryKey: ['categories', id] as const,
    queryFn: () => fetchCategory(id),
    staleTime: 1000 * 60 * 10, // 10 dakika
  }),
};

// Kullanım
const { data } = useQuery(categoryQueries.all());
const { data: detail } = useQuery(categoryQueries.detail('electronics'));

Manuel Invalidation ile Kontrollü Güncelleme

staleTime: Infinity kullandığınızda, veriyi güncellemek istediğinizde manuel invalidation yapmanız gerekir:

import { useQueryClient, useMutation } from '@tanstack/react-query';

function AdminCategoryPanel() {
  const queryClient = useQueryClient();

  const addCategory = useMutation({
    mutationFn: createCategory,
    onSuccess: () => {
      // Cache'i geçersiz kıl, yeniden fetch tetikle
      queryClient.invalidateQueries({ queryKey: ['categories'] });
    },
  });

  // Veya manuel olarak herhangi bir yerde:
  const handleRefresh = () => {
    queryClient.invalidateQueries({ queryKey: ['categories'] });
  };

  return (
    <div>
      <button onClick={handleRefresh}>Kategorileri Yenile</button>
      {/* form ve diğer içerik */}
    </div>
  );
}

staleTime: Infinity ile gcTime İlişkisi

Kritik bir nokta: staleTime: Infinity verinin cache'ten silinmesini engellemez. Eğer bir query'yi kullanan tüm component'ler unmount olursa, gcTime süresi sonunda veri cache'ten silinir. Bunu önlemek için:

const { data } = useQuery({
  queryKey: ['app-config'],
  queryFn: fetchAppConfig,
  staleTime: Infinity,
  gcTime: Infinity, // Cache'ten asla silinmesin
});

⚠️ Dikkat: gcTime: Infinity bellek sızıntılarına yol açabilir. Sadece gerçekten kalıcı olması gereken veriler için kullanın.


React Suspense Entegrasyonu

TanStack Query v5'in en heyecan verici özelliklerinden biri, birinci sınıf Suspense desteğidir. useSuspenseQuery hook'u, veri yükleme durumunu deklaratif olarak yönetmenizi sağlar.

Geleneksel Yaklaşım vs Suspense Yaklaşımı

Geleneksel yaklaşımda her component kendi loading ve error durumunu yönetir:

// ❌ İmperatif loading/error yönetimi
function UserProfile({ userId }: { userId: string }) {
  const { data, isPending, isError, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  if (isPending) return <Spinner />;
  if (isError) return <ErrorMessage error={error} />;

  // data burada hâlâ undefined olabilir (TypeScript açısından)
  return <div>{data.name}</div>;
}

Suspense yaklaşımıyla bu sorumluluk component ağacının yukarısına taşınır:

// ✅ Deklaratif veri yükleme
import { useSuspenseQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  const { data } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  // data burada KESİNLİKLE tanımlıdır — TypeScript bunu garanti eder
  return <div>{data.name}</div>;
}

// Üst component'te boundary'ler ile sarmalayın
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

function UserPage({ userId }: { userId: string }) {
  return (
    <ErrorBoundary fallback={<ErrorMessage />}>
      <Suspense fallback={<Spinner />}>
        <UserProfile userId={userId} />
      </Suspense>
    </ErrorBoundary>
  );
}

useSuspenseQuery'nin Avantajları

  1. TypeScript'te data asla undefined değildir — tip güvenliği artışı
  2. Loading ve error mantığı component dışına çıkar — daha temiz component'ler
  3. Birden fazla query doğal olarak paralel çalışır — waterfall problemi yoktur
  4. React 18+ Suspense özellikleriyle tam uyum — startTransition, useTransition desteği

Birden Fazla Suspense Query

Birden fazla veri kaynağını aynı component'te kullanırken, useSuspenseQueries tercih edilmelidir:

import { useSuspenseQueries } from '@tanstack/react-query';

function Dashboard({ userId }: { userId: string }) {
  const [userQuery, postsQuery, notificationsQuery] = useSuspenseQueries({
    queries: [
      {
        queryKey: ['user', userId],
        queryFn: () => fetchUser(userId),
      },
      {
        queryKey: ['posts', userId],
        queryFn: () => fetchUserPosts(userId),
      },
      {
        queryKey: ['notifications', userId],
        queryFn: () => fetchNotifications(userId),
        staleTime: 1000 * 30, // 30 saniye
      },
    ],
  });

  // Tüm data'lar kesinlikle tanımlıdır
  return (
    <div>
      <h1>{userQuery.data.name}</h1>
      <PostList posts={postsQuery.data} />
      <NotificationBadge count={notificationsQuery.data.length} />
    </div>
  );
}

Suspense Boundary Stratejileri

Suspense boundary'lerini nereye koyacağınız, kullanıcı deneyimini doğrudan etkiler:

// Strateji 1: Tek büyük boundary — tüm içerik bir arada yüklenir
function Page() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Header />
      <Sidebar />
      <MainContent />
    </Suspense>
  );
}

// Strateji 2: Granüler boundary'ler — her bölüm bağımsız yüklenir
function Page() {
  return (
    <div className="layout">
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
      <Suspense fallback={<ContentSkeleton />}>
        <MainContent />
      </Suspense>
    </div>
  );
}

staleTime: Infinity + Suspense: Güçlü Kombinasyon

Bu iki pattern birlikte kullanıldığında, özellikle prefetching senaryolarında mükemmel sonuçlar verir:

// Router seviyesinde prefetch (React Router veya TanStack Router ile)
const queryClient = useQueryClient();

// Route loader'da veriyi önceden yükle
async function productLoader({ params }: { params: { id: string } }) {
  await queryClient.ensureQueryData({
    queryKey: ['product', params.id],
    queryFn: () => fetchProduct(params.id),
    staleTime: Infinity, // Bir kez yüklendi mi tekrar fetch etme
  });
  return null;
}

// Component'te Suspense ile kullan
function ProductDetail({ productId }: { productId: string }) {
  const { data: product } = useSuspenseQuery({
    queryKey: ['product', productId],
    queryFn: () => fetchProduct(productId),
    staleTime: Infinity,
  });

  // Prefetch sayesinde Suspense fallback'i gösterilmez,
  // veri zaten cache'te hazırdır
  return (
    <article>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
      <span>{product.price} TL</span>
    </article>
  );
}

Hover ile Prefetch Pattern'i

Kullanıcı bir linke hover yaptığında veriyi önceden yükleyerek sayfa geçişlerini anlık hale getirin:

function ProductCard({ product }: { product: ProductSummary }) {
  const queryClient = useQueryClient();

  const prefetchProduct = () => {
    queryClient.prefetchQuery({
      queryKey: ['product', product.id],
      queryFn: () => fetchProduct(product.id),
      staleTime: Infinity, // Prefetch edilen veri bayatlamaz
    });
  };

  return (
    <Link
      to={`/products/${product.id}`}
      onMouseEnter={prefetchProduct}
      onFocus={prefetchProduct}
    >
      <h3>{product.title}</h3>
      <p>{product.price} TL</p>
    </Link>
  );
}

Dikkat Edilmesi Gereken Noktalar

1. Suspense ile enabled Seçeneği Kullanamazsınız

useSuspenseQuery'de enabled: false yapılamaz. Koşullu sorgular için geleneksel useQuery kullanın veya component'i koşullu render edin:

// ✅ Doğru yaklaşım: Koşullu render
function UserDetails({ userId }: { userId: string | null }) {
  if (!userId) return <p>Kullanıcı seçiniz</p>;
  return (
    <Suspense fallback={<Spinner />}>
      <UserDetailsContent userId={userId} />
    </Suspense>
  );
}

2. placeholderData Suspense ile Çalışmaz

Suspense modunda placeholderData yerine initialData kullanabilirsiniz, ancak genellikle Suspense fallback'leri yeterlidir.

3. staleTime: Infinity Kullanırken Cache Boyutuna Dikkat Edin

Özellikle dinamik key'lere sahip query'lerde (örneğin arama sonuçları), staleTime: Infinity kullanmak cache'in süresiz büyümesine neden olabilir. Bu durumlarda makul bir gcTime belirleyin.


Sonuç

TanStack Query v5'in sunduğu staleTime: Infinity pattern'i ve Suspense entegrasyonu, modern React uygulamalarında veri yönetimini bir üst seviyeye taşıyan iki güçlü araçtır.

staleTime: Infinity, nadiren değişen veriler için gereksiz network isteklerini ortadan kaldırır ve manuel invalidation ile tam kontrol sağlar. useSuspenseQuery ise loading ve error durumlarını component'lerden ayırarak daha temiz, daha okunabilir ve tip-güvenli kod yazmanızı mümkün kılar.

Bu iki pattern birlikte kullanıldığında — özellikle prefetching stratejileriyle — kullanıcıya neredeyse anlık sayfa geçişleri sunabilirsiniz. Ancak her güçlü araçta olduğu gibi, kullanım senaryonuza uygun olup olmadığını değerlendirmek önemlidir. Sık değişen veriler için staleTime: Infinity yerine makul bir süre tercih edin; Suspense boundary'lerinizi kullanıcı deneyimini optimize edecek şekilde konumlandırın.

Veri yönetimi stratejinizi bu pattern'ler etrafında kurarak hem geliştirici deneyimini hem de kullanıcı deneyimini önemli ölçüde iyileştirebilirsiniz.


Share this post on:

Sonraki Yazı
React Concurrent Stores: Tearing Problemine React Core Çözümü