TanStack React Query ile İstek Tekilleştirme ve API Önbellekleme
İ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:
typescript// 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:
typescript// 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:
typescript// 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:
typescript// 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:
typescriptexport 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:
typescript// 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 | Önce | Sonra | |--------|--------|-------| | Pano yüklemesi başına API çağrısı | 7 | 4 | | Ortalama pano yükleme süresi | 2,1s | 0,9s | | Aylık API çağrısı (toplam) | 12,4M | 5,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.