TanStack Query (React Query) ile Veri Yönetimi

TanStack Query (React Query) ile Veri Yönetimi
TanStack Query (eski adıyla React Query), React uygulamalarında sunucu state yönetimini devrim niteliğinde kolaylaştıran bir kütüphanedir. Veri çekme, önbellekleme, senkronizasyon, arka plan güncelleme ve pagination gibi işlemleri otomatik olarak yönetir. Bu rehberde, TanStack Query'nin temellerinden ileri düzey kullanım senaryolarına kadar kapsamlı bir şekilde ele alacağız.
Server State vs Client State
Modern uygulamalarda iki tür state vardır: Client state (UI durumu, form verileri, tema) ve Server state (API'den gelen veriler). TanStack Query, server state'i yönetmek için özelleşmiştir. Otomatik caching, stale-while-revalidate stratejisi ve akıllı refetch mekanizmasıyla server state yönetimini basitleştirir.
Kurulum ve Yapılandırma
// npm install @tanstack/react-query @tanstack/react-query-devtools
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 dakika — veri "taze" kabul edilir
gcTime: 30 * 60 * 1000, // 30 dakika — kullanılmayan cache temizlenir
retry: 3, // Hata durumunda 3 kez dene
refetchOnWindowFocus: true, // Pencere odaklanınca yeniden çek
refetchOnReconnect: true, // İnternet bağlantısı gelince yeniden çek
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MainApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Temel Sorgular: useQuery
import { useQuery } from '@tanstack/react-query';
interface Product {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
}
// API fonksiyonu — query fonksiyonundan ayrı tutun
async function fetchProducts(category?: string): Promise<Product[]> {
const url = category
? `/api/products?category=${category}`
: '/api/products';
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Ürünler yüklenemedi: ${response.status}`);
}
return response.json();
}
async function fetchProductById(id: string): Promise<Product> {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
throw new Error('Ürün bulunamadı');
}
return response.json();
}
// Component'te kullanım
function ProductList({ category }: { category?: string }) {
const {
data: products,
isLoading,
isError,
error,
isFetching,
refetch,
} = useQuery({
queryKey: ['products', category], // category değişince otomatik yeniden çeker
queryFn: () => fetchProducts(category),
staleTime: 10 * 60 * 1000, // 10 dakika
placeholderData: (previousData) => previousData, // Yeni veri gelene kadar eskisini göster
});
if (isLoading) return <div>Ürünler yükleniyor...</div>;
if (isError) return <div>Hata: {error.message}</div>;
return (
<div>
{isFetching && <span className="text-sm text-blue-500">Güncelleniyor...</span>}
<button onClick={() => refetch()}>Yenile</button>
{products?.map((product) => (
<div key={product.id} className="border-b py-3">
<h3>{product.name}</h3>
<p>{product.price} TL</p>
<span className={product.inStock ? 'text-green-600' : 'text-red-600'}>
{product.inStock ? 'Stokta' : 'Tükendi'}
</span>
</div>
))}
</div>
);
}
['products', { category, page }] gibi obje de kullanabilirsiniz.
Mutation: useMutation
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreateProductInput {
name: string;
price: number;
category: string;
}
function CreateProductForm() {
const queryClient = useQueryClient();
const createProduct = useMutation({
mutationFn: async (newProduct: CreateProductInput) => {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProduct),
});
if (!response.ok) throw new Error('Ürün oluşturulamadı');
return response.json();
},
// Optimistic Update
onMutate: async (newProduct) => {
// Devam eden sorguları iptal et
await queryClient.cancelQueries({ queryKey: ['products'] });
// Önceki veriyi yedekle
const previousProducts = queryClient.getQueryData(['products']);
// Cache'i optimistik güncelle
queryClient.setQueryData<Product[]>(['products'], (old) => [
...(old ?? []),
{ ...newProduct, id: 'temp-id', inStock: true },
]);
return { previousProducts };
},
// Hata durumunda geri al
onError: (err, variables, context) => {
if (context?.previousProducts) {
queryClient.setQueryData(['products'], context.previousProducts);
}
},
// Başarılı olsun veya olmasın, sorguyu yeniden çek
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createProduct.mutate({
name: formData.get('name') as string,
price: Number(formData.get('price')),
category: formData.get('category') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Ürün adı" required />
<input name="price" type="number" placeholder="Fiyat" required />
<input name="category" placeholder="Kategori" required />
<button type="submit" disabled={createProduct.isPending}>
{createProduct.isPending ? 'Ekleniyor...' : 'Ürün Ekle'}
</button>
{createProduct.isError && <p className="text-red-500">{createProduct.error.message}</p>}
{createProduct.isSuccess && <p className="text-green-500">Ürün başarıyla eklendi!</p>}
</form>
);
}
Infinite Scroll / Pagination
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';
interface PaginatedResponse {
data: Product[];
nextCursor: string | null;
hasMore: boolean;
}
function InfiniteProductList() {
const { ref, inView } = useInView();
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ['products', 'infinite'],
queryFn: async ({ pageParam }): Promise<PaginatedResponse> => {
const res = await fetch(`/api/products?cursor=${pageParam}&limit=20`);
return res.json();
},
initialPageParam: '',
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
if (isLoading) return <div>Yükleniyor...</div>;
return (
<div>
{data?.pages.map((page, i) =>
page.data.map((product) => (
<div key={product.id} className="border-b py-3">
<h3>{product.name}</h3>
<p>{product.price} TL</p>
</div>
))
)}
<div ref={ref} className="py-4 text-center">
{isFetchingNextPage ? 'Daha fazla yükleniyor...' : hasNextPage ? 'Aşağı kaydır' : 'Tüm ürünler yüklendi'}
</div>
</div>
);
}
Sonuç
TanStack Query, React uygulamalarında server state yönetiminin en iyi çözümüdür. Otomatik caching, stale-while-revalidate, optimistic updates ve infinite scroll gibi karmaşık senaryoları minimal kodla çözmenizi sağlar. useQuery ile veri okuma, useMutation ile veri yazma ve useInfiniteQuery ile sayfalama ihtiyaçlarınızı karşılayabilirsiniz. Server state'i TanStack Query'ye, client state'i Zustand'a bırakarak temiz bir mimari elde edebilirsiniz.