Performance & Scalability serimizin bir parçası
Tam kılavuzu okuyunReact Query (TanStack): Veri Alma Modelleri
React'ta veri getirme şaşırtıcı derecede geniş bir yüzey alanına sahiptir: yükleme durumları, hata durumları, arka planı yeniden getirme, mutasyonlardan sonra önbelleği geçersiz kılma, iyimser güncellemeler, sonsuz kaydırma, önceden getirme ve tekilleştirme. Bu mantığın useEffect + useState ile elle yazılması yarış koşullarına, bellek sızıntılarına ve tutarsız kullanıcı arayüzü durumlarına yol açar. TanStack Query (eski adıyla React Query) v5, tüm bunları temiz, şekillendirilebilir bir API ile çözüyor.
Bu kılavuz, temel sorgulardan karmaşık mutasyon akışlarına, iyimser güncellemelere, sonsuz kaydırmaya ve Next.js Uygulama Yönlendirici entegrasyonuna kadar üretim React uygulamalarına yönelik TanStack Query v5 modellerini kapsar. Her model TypeScript ve hata işlemeyi içerir.
Önemli Çıkarımlar
- TanStack Query bir sunucu durumu kitaplığıdır — yalnızca istemci kullanıcı arayüzü durumu için kullanmayın (bunun için Zustand veya useState kullanın)
- Sorgu anahtarları önbellek adresidir — sorgu sonucunu etkileyen tüm değişkenleri içerir
staleTimearka planda yeniden getirmenin ne zaman tetikleneceğini kontrol eder;gcTimekullanılmayan verilerin ne zaman çöp olarak toplandığını kontrol eder- Mutasyonlar,
onSuccessiçindekiqueryClient.invalidateQueries()yoluyla ilgili sorguları geçersiz kılmalıdır- İyimser güncellemeler algılanan performansı artırır ancak
onError'da geri alma mantığı gerektirir- Sayfalandırılmış listeler için
useInfiniteQuerykullanın;useQuery+ manuel sayfa durumunu asla birleştirmeyin- Anında gezinme hissi için fareyle üzerine gelindiğinde önceden getirme:
queryClient.prefetchQuery()- Next.js Uygulama Yönlendiricisinde, sunucu tarafından getirilen verileri istemciye aktarmak için
HydrationBoundarykullanın
Kurulum ve Yapılandırma
pnpm add @tanstack/react-query @tanstack/react-query-devtools
// src/providers/query-provider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function QueryProvider({ children }: { children: React.ReactNode }) {
// Create QueryClient inside component to avoid shared state between requests (SSR safety)
const [queryClient] = useState(() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute — data is fresh for 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes — keep unused cache for 5 minutes
retry: 2, // Retry failed requests twice
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000), // Exponential backoff
refetchOnWindowFocus: true, // Refetch when user returns to tab
refetchOnReconnect: true, // Refetch after network reconnect
},
mutations: {
retry: 0, // Do not retry mutations (non-idempotent)
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
// src/app/[locale]/layout.tsx
import { QueryProvider } from '@/providers/query-provider';
export default function LocaleLayout({ children }: { children: React.ReactNode }) {
return (
<QueryProvider>
{children}
</QueryProvider>
);
}
Sorgu Anahtarları — Temel
Sorgu anahtarları verilerinizin önbellek adresidir. Sorgu sonucunu etkileyen her değişkenin anahtarda olması gerekir.
// src/lib/query-keys.ts
// Centralize query keys to avoid typos and enable easy invalidation
export const queryKeys = {
contacts: {
all: () => ['contacts'] as const,
lists: () => [...queryKeys.contacts.all(), 'list'] as const,
list: (filters: ContactFilters) =>
[...queryKeys.contacts.lists(), filters] as const,
details: () => [...queryKeys.contacts.all(), 'detail'] as const,
detail: (id: string) =>
[...queryKeys.contacts.details(), id] as const,
},
orders: {
all: () => ['orders'] as const,
list: (orgId: string, page: number) => ['orders', orgId, page] as const,
detail: (id: string) => ['orders', id] as const,
},
} as const;
// Usage in queries and invalidations:
// useQuery({ queryKey: queryKeys.contacts.list({ page: 1, limit: 20 }) })
// queryClient.invalidateQueries({ queryKey: queryKeys.contacts.lists() })
// — invalidates ALL list queries regardless of filter parameters
Temel Sorgular
// src/hooks/use-contacts.ts
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api';
import { queryKeys } from '@/lib/query-keys';
interface ContactFilters {
page: number;
limit: number;
search?: string;
status?: string;
}
interface ContactsResponse {
data: Contact[];
total: number;
page: number;
limit: number;
}
export function useContacts(filters: ContactFilters) {
return useQuery({
queryKey: queryKeys.contacts.list(filters),
queryFn: async () => {
const params = new URLSearchParams({
page: String(filters.page),
limit: String(filters.limit),
...(filters.search && { search: filters.search }),
...(filters.status && { status: filters.status }),
});
return apiFetch<ContactsResponse>(`/contacts?${params}`);
},
placeholderData: (previousData) => previousData, // Keep previous page data while fetching
staleTime: 30_000, // Contacts data: 30 second stale time
});
}
export function useContact(id: string) {
return useQuery({
queryKey: queryKeys.contacts.detail(id),
queryFn: () => apiFetch<Contact>(`/contacts/${id}`),
enabled: !!id, // Only fetch if id is provided
});
}
// Usage in a component
'use client';
export default function ContactsPage() {
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const { data, isLoading, isError, error, isFetching } = useContacts({
page,
limit: 20,
search: search || undefined,
});
if (isLoading) return <ContactsSkeleton />;
if (isError) return <ErrorState message={error.message} />;
return (
<div>
{isFetching && <RefetchIndicator />} {/* Shows during background refetch */}
<ContactsTable data={data.data} />
<Pagination
page={page}
total={data.total}
limit={20}
onChange={setPage}
/>
</div>
);
}
Önbellek Geçersiz Kılma ile Mutasyonlar
// src/hooks/use-contact-mutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api';
import { queryKeys } from '@/lib/query-keys';
import { toast } from '@/components/ui/toast';
export function useCreateContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateContactDto) =>
apiFetch<Contact>('/contacts', {
method: 'POST',
body: JSON.stringify(data),
}),
onSuccess: (newContact) => {
// Invalidate all contact list queries — they are now stale
queryClient.invalidateQueries({
queryKey: queryKeys.contacts.lists(),
});
// Seed the detail cache immediately — no need to refetch
queryClient.setQueryData(
queryKeys.contacts.detail(newContact.id),
newContact
);
toast({ title: 'Contact created', variant: 'success' });
},
onError: (error: Error) => {
toast({ title: 'Failed to create contact', description: error.message, variant: 'destructive' });
},
});
}
export function useDeleteContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
apiFetch(`/contacts/${id}`, { method: 'DELETE' }),
onSuccess: (_data, deletedId) => {
// Remove from all list queries
queryClient.invalidateQueries({ queryKey: queryKeys.contacts.lists() });
// Remove detail cache
queryClient.removeQueries({ queryKey: queryKeys.contacts.detail(deletedId) });
toast({ title: 'Contact deleted', variant: 'success' });
},
});
}
İyimser Güncellemeler
İyimser güncellemeler, sunucu onaylamadan önce kullanıcı arayüzünü güncelleyerek mutasyonların anında hissedilmesini sağlar:
export function useUpdateContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateContactDto }) =>
apiFetch<Contact>(`/contacts/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
// Called immediately before mutationFn
onMutate: async ({ id, data }) => {
// Cancel any in-flight refetches for this contact
await queryClient.cancelQueries({ queryKey: queryKeys.contacts.detail(id) });
// Snapshot the previous value for rollback
const previousContact = queryClient.getQueryData<Contact>(
queryKeys.contacts.detail(id)
);
// Optimistically update the UI
queryClient.setQueryData<Contact>(
queryKeys.contacts.detail(id),
(old) => old ? { ...old, ...data } : old
);
return { previousContact }; // Passed to onError context
},
onError: (_error, { id }, context) => {
// Roll back to the snapshot on error
if (context?.previousContact) {
queryClient.setQueryData(
queryKeys.contacts.detail(id),
context.previousContact
);
}
toast({ title: 'Update failed', variant: 'destructive' });
},
onSettled: (_data, _error, { id }) => {
// Always refetch after success or error to sync with server truth
queryClient.invalidateQueries({ queryKey: queryKeys.contacts.detail(id) });
},
});
}
Sonsuz Kaydırma / Sayfalandırma
// src/hooks/use-infinite-contacts.ts
import { useInfiniteQuery } from '@tanstack/react-query';
export function useInfiniteContacts(search?: string) {
return useInfiniteQuery({
queryKey: ['contacts', 'infinite', search],
queryFn: ({ pageParam = 1 }) =>
apiFetch<ContactsResponse>(`/contacts?page=${pageParam}&limit=20${search ? `&search=${search}` : ''}`),
initialPageParam: 1,
getNextPageParam: (lastPage) => {
const nextPage = lastPage.page + 1;
const maxPage = Math.ceil(lastPage.total / lastPage.limit);
return nextPage <= maxPage ? nextPage : undefined;
},
staleTime: 30_000,
});
}
// Infinite scroll component
'use client';
import { useInfiniteContacts } from '@/hooks/use-infinite-contacts';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';
export function InfiniteContactsList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteContacts();
const { ref, inView } = useInView({ threshold: 0.5 });
// Fetch next page when the sentinel element enters the viewport
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
if (isLoading) return <ContactsSkeleton />;
const contacts = data?.pages.flatMap((page) => page.data) ?? [];
return (
<div>
{contacts.map((contact) => (
<ContactCard key={contact.id} contact={contact} />
))}
{/* Sentinel element — triggers next page load when visible */}
<div ref={ref} className="h-4">
{isFetchingNextPage && <LoadingSpinner />}
</div>
</div>
);
}
Anında Gezinme için Ön Getirme
// Prefetch on hover — data is ready before the user clicks
'use client';
import { useQueryClient } from '@tanstack/react-query';
import { Link } from '@/i18n/navigation';
import { queryKeys } from '@/lib/query-keys';
import { apiFetch } from '@/lib/api';
export function ContactRow({ contact }: { contact: Contact }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.contacts.detail(contact.id),
queryFn: () => apiFetch<Contact>(`/contacts/${contact.id}`),
staleTime: 60_000, // Use cached if less than 1 minute old
});
};
return (
<tr onMouseEnter={handleMouseEnter}>
<td>
<Link href={`/dashboard/contacts/${contact.id}`}>{contact.name}</Link>
</td>
<td>{contact.email}</td>
</tr>
);
}
Next.js Uygulama Yönlendiricisi SSR + TanStack Sorgusu
// src/app/[locale]/dashboard/contacts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';
import { getServerContacts } from '@/lib/server-api';
import { ContactsClient } from './contacts-client';
export default async function ContactsPage({ params }: Props) {
const { locale } = await params;
const queryClient = new QueryClient();
// Prefetch on the server — data is embedded in the HTML
await queryClient.prefetchQuery({
queryKey: queryKeys.contacts.list({ page: 1, limit: 20 }),
queryFn: () => getServerContacts({ page: 1, limit: 20 }),
});
return (
// HydrationBoundary serializes the server QueryClient state
// into the HTML, which the client QueryClient then picks up
<HydrationBoundary state={dehydrate(queryClient)}>
<ContactsClient />
</HydrationBoundary>
);
}
// src/app/[locale]/dashboard/contacts/contacts-client.tsx ('use client')
'use client';
import { useContacts } from '@/hooks/use-contacts';
export function ContactsClient() {
const { data } = useContacts({ page: 1, limit: 20 });
// data is immediately available from the server-dehydrated state
// No loading state on first render — SSR data is used instantly
return <ContactsTable data={data?.data ?? []} />;
}
Sıkça Sorulan Sorular
TanStack Sorgusunu, SWR'yi ve düz getirmeyi ne zaman kullanmalıyım?
İhtiyacınız olduğunda TanStack Sorgusunu kullanın: mutasyonlardan sonra önbellek geçersiz kılma, iyimser güncellemeler, sonsuz kaydırma, önceden getirme, arka planda yeniden getirme veya tekilleştirme isteği. Daha küçük bir pakete ve yalnızca basit kullanım senaryolarına ihtiyacınız varsa SWR'yi kullanın. React'in yerel veri önbelleğe almasını istediğiniz Next.js Sunucu Bileşenlerinde düz fetch kullanın (cache() işlevi ve revalidatePath() modeli). TanStack Sorgusu, kullanıcı eylemlerinin sunucu verilerini etkilediği istemci tarafı etkileşimi içindir.
staleTime ile gcTime arasındaki fark nedir?
staleTime verilerin ne kadar süreyle taze kabul edildiğini gösterir; bu süre zarfında arka planda yeniden getirme gerçekleşmez ve önbelleğe alınan veriler hemen döndürülür. staleTime'in süresi dolduktan sonra veriler "eski" hale gelir ve bir sonraki kullanımda arka planda yeniden getirilir. gcTime (önceden cacheTime), kullanılmayan (aktif abone olmayan) verilerin çöp toplamadan önce bellekte ne kadar süre kalacağını belirtir. Verilerinizin ne kadar hızlı değiştiğine bağlı olarak staleTime değerini ayarlayın; Anında hissetmek için ne kadar süre uzaklaşıp geri dönmek istediğinize bağlı olarak gcTime değerini ayarlayın.
Kimlik doğrulama hatalarını (401) genel olarak nasıl ele alırım?
QueryClient varsayılan seçeneklerinize genel bir hata işleyicisi ekleyin. 401'de oturum açmaya yönlendirin ve sorgu önbelleğini temizleyin. TanStack Sorgu v5'te: defaultOptions: { queries: { throwOnError: (error) => { if (error.status === 401) { router.push('/auth/login'); queryClient.clear(); return false; } return true; } } }. Alternatif olarak, apiFetch yardımcınızda logout() işlevini çağırarak 401'i işleyin.
Sorgu durumunu kardeş bileşenler arasında pervane ayrıntısına girmeden nasıl paylaşırım?
Aynı sorgu anahtarıyla useQuery çağıran herhangi bir bileşen, önbelleğe alınmış aynı verileri paylaşır; TanStack Sorgusu, istekleri otomatik olarak tekilleştirir. İki ContactsTable bileşeni ve aynı filtrelerle useContacts({ page: 1, limit: 20 }) çağıran bir ContactCount rozetinin tümü, önbelleğe alınmış tek bir istekten okunacaktır. Pervane sondajı yok, bağlama gerek yok.
Sorgu anahtarları kullanıcının oturumunu veya kuruluş kimliğini içermeli mi?
Evet — verilerin kapsamı bir kullanıcı veya kuruluşa göre belirlenmişse sorgu anahtarına kapsam tanımlayıcıyı ekleyin. Bu, bir kullanıcının verilerinin bir başkası için görünmesini engeller (çok kiracılı uygulamalar için kritik öneme sahiptir). Ayrıca oturum kapatıldığında sorgu önbelleğinin tamamını temizleyin: queryClient.clear() veya queryClient.removeQueries() — aksi takdirde önceki oturumdaki eski veriler bir sonraki kullanıcı için kısa süreliğine yanıp sönebilir.
Sonraki Adımlar
TanStack Query, bir durum yönetimi probleminden alınan React verilerini bildirim temelli, önbellek farkındalığına sahip bir sisteme dönüştürür. İyimser güncellemeler, önceden getirme ve SSR hidrasyonu, kullanıcılarınıza yavaş bağlantılarda bile anında hissedilen bir deneyim sunar.
ECOSIRE, tüm sunucu durumları için TanStack Query ile yalnızca istemci kullanıcı arayüzü durumu için Zustand ve veri ağırlıklı görünümler için TanStack Table ile birlikte Next.js yönetici panelleri ve müşteri portalları oluşturur. Performanslı, üretime hazır React uygulamalarını nasıl oluşturduğumuzu öğrenmek için Ön uç mühendislik hizmetlerimizi keşfedin.
Yazan
ECOSIRE Research and Development Team
ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.
İlgili Makaleler
Next.js 16 App Router: Production Patterns and Pitfalls
Production-ready Next.js 16 App Router patterns: server components, caching strategies, metadata API, error boundaries, and performance pitfalls to avoid.
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Optimizing AI Agent Costs: Token Usage and Caching
Practical strategies for reducing AI agent operational costs through token optimization, caching, model routing, and usage monitoring. Real savings from production OpenClaw deployments.
Performance & Scalability serisinden daha fazlası
k6 Load Testing: Stress-Test Your APIs Before Launch
Master k6 load testing for Node.js APIs. Covers virtual user ramp-ups, thresholds, scenarios, HTTP/2, WebSocket testing, Grafana dashboards, and CI integration patterns.
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Odoo Performance Tuning: PostgreSQL and Server Optimization
Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.
Odoo vs Acumatica: Cloud ERP for Growing Businesses
Odoo vs Acumatica compared for 2026: unique pricing models, scalability, manufacturing depth, and which cloud ERP fits your growth trajectory.
Testing and Monitoring AI Agents in Production
A complete guide to testing and monitoring AI agents in production environments. Covers evaluation frameworks, observability, drift detection, and incident response for OpenClaw deployments.
Compliance Monitoring Agents with OpenClaw
Deploy OpenClaw AI agents for continuous compliance monitoring. Automate regulatory checks, policy enforcement, audit trail generation, and compliance reporting.