İçeriğe geç
react

TanStack Query (React Query) ile Veri Yönetimi

Tarık Tunç
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>
  );
}
İpucu: queryKey array'i, sorgunuzun benzersiz kimliğidir. Parametreleri key'e dahil etmek, her parametre kombinasyonu için ayrı cache oluşturur. ['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>
  );
}
Uyarı: TanStack Query, server state yönetimi içindir. Client state (modal açık/kapalı, form input değerleri) için TanStack Query kullanmayın — bunun yerine useState, Zustand veya Jotai kullanın.
Bilgi: TanStack Query DevTools, cache durumunu, aktif sorguları ve mutation geçmişini görsel olarak incelemenizi sağlar. Geliştirme sürecinde mutlaka kullanın.

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.

Kaynaklar

İlgili Yazılar