Blog'a Dön

TanStack React Query ile İstek Tekilleştirme ve API Önbellekleme

TanStack React Query v5'in React Native bankacılık uygulamamızda gereksiz API çağrılarını nasıl ortadan kaldırdığını, sunucu maliyetlerini %53 düşürdüğünü ve pano yükleme sürelerini 2,1 saniyeden 0,9 saniyeye indirdiğini anlatıyorum.

Umut Korkmaz

Yayın tarihi: 8 dk okuma

İstek tekilleştirmesini (request deduplication) ilk duyduğumda şöyle düşünmüştüm: "Zaten önbelleğimiz var — bu yetmez mi?" Yetmiyormuş. React Native bankacılık uygulamamızda, tekilleştirme olmadan aynı API çağrısının tek bir ekran render döngüsü içinde 4-6 kez tetiklenebildiğini keşfettim. TanStack React Query v5 bunu zarifçe çözdü, ancak finansal bir uygulama için doğru yapılandırmak dikkatli bir düşünce gerektirdi. İşte öğrendiklerim.

Sorun: Bin İstekle Ölüm

Tipik bir bankacılık panosunu düşünün. Başlık, kullanıcının adını gösterir (kullanıcı profili gerekir). Bakiye kartı, hesap bakiyesini gösterir (hesap verisi gerekir). İşlem listesi, son işlemleri gösterir (işlem verisi gerekir). Bildirim zili, okunmamış sayısını gösterir (bildirim verisi gerekir). Ve bu bileşenlerin her biri bağımsız olarak mount olur.

Ama işin püf noktası şu — işlem listesi bileşeni de hesap adlarını göstermek için hesap verisine ihtiyaç duyar. Bildirim zili, bildirimleri filtrelemek için kullanıcı profili verisine ihtiyaç duyar. React Query'den önce, useEffect tabanlı veri çekme işlemimiz yinelenen (duplicate) API çağrılarına yol açıyordu:

// Network tab without deduplication:
GET /api/user/profile      (Header component)
GET /api/user/profile      (NotificationBell component)
GET /api/accounts           (BalanceCard component)
GET /api/accounts           (TransactionList component)
GET /api/transactions       (TransactionList component)
GET /api/notifications      (NotificationBell component)
GET /api/user/profile      (Greeting component)  // 3rd call!

Yedi API çağrısı, ama yalnızca dört benzersiz uç nokta. Hücresel bağlantıdaki bir mobil uygulamada, bu üç gereksiz çağrı yalnızca israf değil — kullanıcının gördüğü bir gecikmedir.

Finansal Veri İçin TanStack Query Yapılandırması

Global yapılandırmamız finansal verinin doğasını yansıtır — taze olması gerekir, ama bunu API'yi boğma pahasına değil:

// queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,        // 5 minutes
      gcTime: 10 * 60 * 1000,           // 10 minutes (formerly cacheTime)
      retry: 2,
      retryDelay: (attemptIndex) =>
        Math.min(1000 * 2 ** attemptIndex, 10000),
      refetchOnWindowFocus: false,       // Mobile doesn't have "window focus"
      refetchOnReconnect: true,          // But it does lose connectivity
      networkMode: 'offlineFirst',
    },
    mutations: {
      retry: 0,                          // Never retry mutations in banking
    },
  },
});

5 dakikalık bayatlama süresi (stale time), bankacılık verisi için en uygun noktadır. Çoğu kullanıcı için hesap bakiyeleri her saniye değişmez ve en fazla 5 dakika eski bir bakiye göstermek kabul edilebilir (bir "son güncelleme" zaman damgası gösteririz). Ancak mutasyonları asla yeniden denemeyiz — bir transfer başarısız olursa, yanlışlıkla iki kez göndermek istemeyiz.

Tekilleştirme Aslında Nasıl Çalışır

TanStack Query, sorgu anahtarına (query key) göre tekilleştirir. Birden fazla bileşen aynı anahtarı kullandığında, yalnızca tek bir ağ isteği tetiklenir:

// hooks/useAccountData.ts
export function useAccountData(accountId: string) {
  return useQuery({
    queryKey: ['account', accountId],
    queryFn: () => accountApi.getAccount(accountId),
    staleTime: 5 * 60 * 1000,
  });
}

// Used in BalanceCard.tsx
const { data: account } = useAccountData('ACC-001');

// Used in TransactionList.tsx — SAME query key, NO duplicate request
const { data: account } = useAccountData('ACC-001');

Kaputun altında, BalanceCard mount olup sorguyu tetiklediğinde, TanStack Query uçuştaki promise'i saklar. TransactionList milisaniyeler sonra aynı anahtarla mount olduğunda, aynı promise'i alır. Tek istek, iki abone.

Sorgu Anahtarı Mimarisi

Sorgu anahtarlarını doğru yapmak, tekilleştirme için kritiktir. Bir fabrika (factory) kalıbı kullanıyoruz:

// queryKeys.ts
export const queryKeys = {
  user: {
    all: ['user'] as const,
    profile: () => [...queryKeys.user.all, 'profile'] as const,
    preferences: () => [...queryKeys.user.all, 'preferences'] as const,
  },
  accounts: {
    all: ['accounts'] as const,
    list: () => [...queryKeys.accounts.all, 'list'] as const,
    detail: (id: string) => [...queryKeys.accounts.all, 'detail', id] as const,
    balance: (id: string) => [...queryKeys.accounts.all, 'balance', id] as const,
  },
  transactions: {
    all: ['transactions'] as const,
    list: (accountId: string, filters?: TransactionFilters) =>
      [...queryKeys.transactions.all, 'list', accountId, filters] as const,
    detail: (id: string) =>
      [...queryKeys.transactions.all, 'detail', id] as const,
  },
  notifications: {
    all: ['notifications'] as const,
    unread: () => [...queryKeys.notifications.all, 'unread'] as const,
  },
};

Bu yapı, cerrahi hassasiyette geçersiz kılmayı (invalidation) mümkün kılar. Bir transferden sonra, her hesabı yeniden çekmeden yalnızca ilgili hesabın bakiyesini geçersiz kılarız:

// After a successful transfer
await queryClient.invalidateQueries({
  queryKey: queryKeys.accounts.balance(fromAccountId),
});
await queryClient.invalidateQueries({
  queryKey: queryKeys.accounts.balance(toAccountId),
});
await queryClient.invalidateQueries({
  queryKey: queryKeys.transactions.list(fromAccountId),
});

Daha İyi Kullanıcı Deneyimi İçin İyimser Güncellemeler

Kritik olmayan güncellemeler için (bildirimleri okundu olarak işaretleme, tercihleri güncelleme), algılanan gecikmeyi ortadan kaldırmak adına iyimser güncellemeler (optimistic updates) kullanırız:

export function useMarkNotificationRead() {
  return useMutation({
    mutationFn: (notificationId: string) =>
      notificationApi.markRead(notificationId),

    onMutate: async (notificationId) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({
        queryKey: queryKeys.notifications.all,
      });

      // Snapshot previous value
      const previousNotifications = queryClient.getQueryData(
        queryKeys.notifications.unread()
      );

      // Optimistically update
      queryClient.setQueryData(
        queryKeys.notifications.unread(),
        (old: Notification[]) =>
          old?.filter((n) => n.id !== notificationId) ?? []
      );

      return { previousNotifications };
    },

    onError: (_err, _id, context) => {
      // Rollback on error
      queryClient.setQueryData(
        queryKeys.notifications.unread(),
        context?.previousNotifications
      );
    },

    onSettled: () => {
      queryClient.invalidateQueries({
        queryKey: queryKeys.notifications.all,
      });
    },
  });
}

Finansal işlemler için asla iyimser güncellemeler kullanmayız. Birkaç saniyeliğine bile yanlış bir bakiye gösteren başarısız bir iyimser transfer güncellemesi, müşterinin yanlış bilgiye dayanarak karar vermesine neden olabilir.

Algılanan Performans İçin Ön Yükleme (Prefetching)

Kullanıcının bir sonraki adımda muhtemelen ihtiyaç duyacağı verileri önceden yükleriz:

// In the account list screen
function AccountListItem({ account }: { account: Account }) {
  const queryClient = useQueryClient();

  const handlePressIn = () => {
    // Prefetch when the user starts pressing, not when they navigate
    queryClient.prefetchQuery({
      queryKey: queryKeys.transactions.list(account.id),
      queryFn: () => transactionApi.getRecent(account.id),
      staleTime: 5 * 60 * 1000,
    });
  };

  return (
    <Pressable onPressIn={handlePressIn} onPress={() => navigate(account.id)}>
      <AccountCard account={account} />
    </Pressable>
  );
}

onPress (dokunma sonu) yerine onPressIn'de (dokunma başlangıcı) ön yükleme yaparak ~200ms ağ süresi kazanırız. İyi bir bağlantıda, veri navigasyon animasyonu tamamlanmadan hazır olur.

Etkiyi Ölçmek

Uygun tekilleştirme ile TanStack Query'yi devreye aldıktan sonra:

MetrikÖnceSonra
Pano yüklemesi başına API çağrısı74
Ortalama pano yükleme süresi2,1s0,9s
Aylık API çağrısı (toplam)12,4M5,8M
Sunucu altyapı maliyeti$X~0,47X

API çağrısındaki azalma tek başına geçişi haklı çıkardı. Ama asıl kazanç kullanıcı deneyimiydi — veriler önbelleğe alınıp tekilleştirildiğinde ekranlar anında geliyor. 5 dakikalık bayatlama süresi, bir ekrana geri dönmenin önbelleğe alınmış veriyi anında göstermesi, arka plandaki bir yeniden çekme işleminin (refetch) ise tazeliği güvence altına alması anlamına gelir.

TanStack React Query, onsuz uygulamaları nasıl geliştirdiğinizi sorgulatan kütüphanelerden biri. Tekilleştirme ve önbellekleme tek başına buna değerdi, ancak iyimser güncellemeler, ön yükleme ve sorgu anahtarı fabrika kalıbıyla birleştiğinde, duyarlı (responsive) bir finansal uygulamanın bel kemiği hâline geliyor.

Yazar hakkında

Umut Korkmaz

Lead Yazılım Mühendisi

İstanbul'da kıdemli yazılım mühendisi olarak çalışıyorum. Burada, bizzat canlıya aldığım araçları ve sürümleri, sahada öğrendiklerimle birlikte yazıyorum.

Ortak konulara göre seçildi.

  • 8 dk okuma

    Claude ile Prompt Önbellekleme: Üretimde Asıl Parayı Ne Kazandırır

    Anthropic API'sinde prompt önbellekleme, iş yükünüz onun şekliyle örtüştüğünde getiri sağlar: kararlı sistem prompt'ları, belirleyici getirme bağlamları ve gruplanmış değerlendirme — varsayılan değil, ölçülmüş.

    • Yapay Zekâ
    • Claude API
    • Performans
  • 4 dk okuma

    Next.js 16.3: Ücretsiz Gelenler ve Taşıma Gerektirenler

    Next.js 16.3'ün getirdikleri ikiye ayrılıyor: sadece sürümü yükselterek elde ettiğiniz kazanımlar ve isteğe bağlı Instant Navigations geçişi. İkisini nasıl planlardım, yeni AGENTS.md bloğu ekipler için ne anlama geliyor, bu yazıda anlatıyorum.

    • Next.js
    • React
    • Performans