React Query (TanStack): Data Fetching Patterns

Master TanStack Query v5 data fetching patterns: queries, mutations, optimistic updates, infinite scroll, prefetching, cache invalidation, and integration with Next.js App Router.

E
ECOSIRE Research and Development Team
|19 Mart 20269 dk okuma2.0k Kelime|

Performance & Scalability serimizin bir parçası

Tam kılavuzu okuyun

React 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
  • staleTime arka planda yeniden getirmenin ne zaman tetikleneceğini kontrol eder; gcTime kullanılmayan verilerin ne zaman çöp olarak toplandığını kontrol eder
  • Mutasyonlar, onSuccess içindeki queryClient.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 useInfiniteQuery kullanı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 HydrationBoundary kullanı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.

E

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.

WhatsApp'ta Sohbet Et