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:
- useQuery artık sadece tek bir obje parametresi alıyor — pozisyonel argümanlar kaldırıldı.
useSuspenseQueryhook'u resmi olarak stable hale geldi.status: 'loading'yerinestatus: 'pending'kullanılmaya başlandı.cacheTimeyerinegcTime(garbage collection time) adlandırması benimsendi.- TypeScript desteği daha da güçlendirildi.
Ş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:
- Nadiren değişen veriler — Ülke listeleri, para birimleri, uygulama konfigürasyonları
- Kullanıcı oturumu boyunca sabit kalması gereken veriler — Kullanıcı profili, yetkilendirme bilgileri
- Hesaplama maliyeti yüksek API çağrıları — Raporlar, istatistikler
- 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: Infinitybellek 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ı
- TypeScript'te
dataaslaundefineddeğildir — tip güvenliği artışı - Loading ve error mantığı component dışına çıkar — daha temiz component'ler
- Birden fazla query doğal olarak paralel çalışır — waterfall problemi yoktur
- 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.