Takip et

useQuery/Promise.all Spagettisinden Sıkıldım, Ben de Bunu İnşa Ettim 🫖🦡

Tek bir veri parçasını çekmek için useQuery kullanmak oldukça basittir. Ancak, bir sayfada birden fazla bağımsız veya bağımlı veriye ihtiyaç duy…

useQuery/Promise.all Spagettisinden Sıkıldım, Ben de Bunu İnşa Ettim 🫖🦡

Modern web uygulamaları geliştirirken, veri çekme işlemleri işimizin ayrılmaz bir parçasıdır. React ekosisteminde react-query (şimdiki adıyla TanStack Query) gibi kütüphaneler, bu süreci büyük ölçüde basitleştirerek önbellekleme, senkronizasyon ve sunucu durumu yönetimi gibi karmaşık konuları ele almamızı sağlıyor. Ancak, birden fazla API çağrısını aynı anda veya birbirine bağımlı olarak yapmamız gerektiğinde, useQuery hook’unun gücünü Promise.all ile birleştirmek, kodumuzun hızla bir “spagetti” yığınına dönüşmesine neden olabilir. Bu makalede, bu yaygın sorunu derinlemesine inceleyecek ve bu karmaşayı ortadan kaldırmak için geliştirdiğim, daha temiz ve yönetilebilir bir çözümü sizlerle paylaşacağım.

Mevcut Yaklaşımların Sınırları: Neden “Spagetti” Oluyor?

Tek bir veri parçasını çekmek için useQuery kullanmak oldukça basittir. Ancak, bir sayfada birden fazla bağımsız veya bağımlı veriye ihtiyaç duyduğumuzda işler karışmaya başlar. Gelin, bu karmaşanın nedenlerini ve yaygın senaryoları inceleyelim.

Bağımsız Çoklu Sorgular ve Promise.all

Bir bileşenin aynı anda birden fazla bağımsız veriye ihtiyacı olduğunda, genellikle her biri için ayrı bir useQuery çağrısı yaparız. Tüm bu sorguların tamamlanmasını beklemek ve ortak bir yükleme/hata durumunu yönetmek istediğimizde, Promise.all akla ilk gelen çözümdür. Ancak useQuery zaten promise tabanlı bir yapıya sahip olduğundan, bu ikisini birleştirmek beklediğimizden daha karmaşık olabilir.


import { useQuery } from '@tanstack/react-query';

function MyComponent() {
  const { data: users, isLoading: usersLoading, error: usersError } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json()),
  });

  const { data: products, isLoading: productsLoading, error: productsError } = useQuery({
    queryKey: ['products'],
    queryFn: () => fetch('/api/products').then(res => res.json()),
  });

  const isLoading = usersLoading || productsLoading;
  const isError = usersError || productsError;

  if (isLoading) return 
Yükleniyor...
; if (isError) return
Hata oluştu: {usersError?.message || productsError?.message}
; return (

Kullanıcılar

    {users?.map(user =>
  • {user.name}
  • )}

Ürünler

    {products?.map(product =>
  • {product.name}
  • )}
); }

Yukarıdaki örnekte, her ne kadar basit görünse de, sorgu sayısı arttıkça isLoading ve isError durumlarını manuel olarak birleştirmek yorucu hale gelir. Ayrıca, her bir sorgunun kendi durumu olduğundan, genel bir durum yönetimi zorlaşır.

Bağımlı Sorgular ve Zincirleme Problemleri

Bazen bir sorgunun sonucu, başka bir sorgunun parametresi olarak kullanılır. Örneğin, önce bir kullanıcı ID'sini çeker, sonra o ID ile kullanıcının detaylarını veya siparişlerini getiririz. Bu durumda, enabled seçeneği ile sorguları zincirlemek yaygın bir yöntemdir:


import { useQuery } from '@tanstack/react-query';

function UserOrders({ userId }) {
  const { data: userProfile, isLoading: profileLoading } = useQuery({
    queryKey: ['userProfile', userId],
    queryFn: () => fetch(/api/users/${userId}).then(res => res.json()),
    enabled: !!userId, // userId varsa sorguyu çalıştır
  });

  const { data: orders, isLoading: ordersLoading } = useQuery({
    queryKey: ['userOrders', userProfile?.id],
    queryFn: () => fetch(/api/users/${userProfile.id}/orders).then(res => res.json()),
    enabled: !!userProfile?.id, // userProfile varsa sorguyu çalıştır
  });

  const isLoading = profileLoading || ordersLoading;

  if (isLoading) return 
Yükleniyor...
; if (!userProfile) return
Kullanıcı bulunamadı.
; return (

{userProfile.name} Siparişleri

    {orders?.map(order =>
  • {order.productName} ({order.amount})
  • )}
); }

Bu yaklaşım çalışsa da, iç içe geçmiş useQuery çağrıları ve koşullu enabled ayarları, özellikle daha karmaşık bağımlılık zincirlerinde kodun okunabilirliğini ve bakımını zorlaştırır. Her bir adımın kendi yükleme ve hata durumu olduğundan, genel bir durum elde etmek yine manuel birleştirme gerektirir.

Hata Yönetimi ve Yükleme Durumunun Dağınıklığı

Yukarıdaki örneklerde de görüldüğü gibi, birden fazla useQuery kullandığımızda, her bir sorgunun kendi isLoading, isError ve data değişkeni olur. Bu durumları tek bir çatı altında toplamak için manuel olarak mantık yazmak, tekrar eden ve hataya açık bir süreçtir. Özellikle kısmi başarı veya kısmi hata senaryolarını yönetmek daha da karmaşık hale gelir.

İdeal Bir Çözümden Beklentilerimiz

Yukarıdaki "spagetti" durumundan kurtulmak için, geliştireceğimiz çözümün belirli özelliklere sahip olması gerekir. Bu özellikler, hem geliştirici deneyimini iyileştirmeli hem de uygulamanın performansını ve kararlılığını artırmalıdır.

Tek Bir Yükleme ve Hata Durumu

Tüm sorguların genel yükleme durumunu (isLoading) ve herhangi bir sorguda oluşan genel hata durumunu (isError) tek bir değişken üzerinden kolayca erişilebilir kılmalıyız. Bu, UI'da spinner göstermek veya hata mesajlarını render etmek için kod tekrarını önleyecektir.

Toplu Veri Erişimi

Çekilen tüm verilerin tek bir nesne veya dizi içinde düzenli bir şekilde erişilebilir olması, bileşen içerisinde veri kullanımını basitleştirecektir. Böylece, her bir veri parçası için ayrı ayrı değişken tanımlamaktan kurtuluruz.

Bağımlı Sorgu Desteği

Çözümümüz, bir sorgunun sonucunun başka bir sorgunun girişini oluşturduğu senaryoları doğal bir şekilde desteklemelidir. Bu, enabled zincirlerini daha temiz bir yapıyla yönetmemizi sağlamalıdır.

Esneklik ve Genişletilebilirlik

Her bir sorgu için useQuery'nin sunduğu tüm seçenekleri (staleTime, cacheTime, retry vb.) kullanabilmeliyiz. Ayrıca, gelecekte farklı ihtiyaçlara göre kolayca genişletilebilir olmalıdır.

Tip Güvenliği (TypeScript ile)

Özellikle büyük projelerde, çekilen verilerin ve döndürülen sonuçların tip güvenli olması, hataları erken yakalamak ve geliştirme sürecini hızlandırmak için kritik öneme sahiptir.

Çözümümüz: useCombinedQueries Hook'u

Bu beklentileri karşılamak ve useQuery/Promise.all spagettisine son vermek için, useCombinedQueries adını verdiğim (veya benzeri bir isimle) özel bir React hook'u geliştirdim. Bu hook, bir dizi sorguyu tek bir çağrıda yöneterek, ortak bir yükleme ve hata durumu sunar ve tüm verileri tek bir yapıda toplar.

Temel Fikir: Sorgu Tanımlarının Bir Dizisi

useCombinedQueries hook'u, useQuery'ye geçirdiğimiz seçenek nesnelerinin bir dizisini alır. Bu dizi, hook'un iç mantığı tarafından işlenerek tüm sorgular çalıştırılır ve sonuçlar birleştirilir.


import { useQueries, UseQueryOptions, QueryFunction } from '@tanstack/react-query';

interface CombinedQueryResult {
  data: TData;
  isLoading: boolean;
  isError: boolean;
  error: unknown;
  isSuccess: boolean;
  // Diğer useQuery dönüş değerleri de eklenebilir
}

function useCombinedQueries(
  queries: UseQueryOptions[]
): CombinedQueryResult {
  const results = useQueries({ queries });

  const isLoading = results.some(result => result.isLoading);
  const isError = results.some(result => result.isError);
  const error = results.find(result => result.isError)?.error;
  const isSuccess = results.every(result => result.isSuccess);
  const data = results.map(result => result.data) as TData[];

  return {
    data,
    isLoading,
    isError,
    error,
    isSuccess,
  };
}

Yukarıdaki basit örnek, TanStack Query'nin useQueries hook'unu kullanarak birden fazla sorguyu aynı anda çalıştırmanın ve sonuçlarını birleştirmenin temel mantığını gösteriyor. Ancak, bağımlı sorgular ve daha gelişmiş senaryolar için bu yapıyı biraz daha genişletmemiz gerekecek.

useCombinedQueries Nasıl Çalışır? Derinlemesine Bir Bakış

Geliştirdiğim useCombinedQueries hook'u, useQueries'in sağladığı gücü kullanarak, her bir sorgunun durumunu ayrı ayrı yönetirken, dışarıya tek bir birleşik durum sunar. İşte ana bileşenleri ve çalışma prensibi:

1. Sorgu Tanımlarının Kabulü

Hook, useQuery'ye geçireceğimiz tüm seçenekleri içeren bir dizi nesne (UseQueryOptions) alır. Bu sayede her bir sorgunun kendi queryKey, queryFn ve diğer özel ayarları olabilir.

2. useQueries Kullanımı

TanStack Query'nin useQueries hook'u, birden fazla sorguyu aynı anda çalıştırmak ve her birinin sonucunu bir dizi olarak döndürmek için tasarlanmıştır. Bizim hook'umuz da tam olarak bunu kullanır.


// useCombinedQueries.ts
import { useQueries, UseQueryOptions, QueryFunction } from '@tanstack/react-query';

type QueryConfig = UseQueryOptions & {
  id: string; // Opsiyonel: Sorguları tanımlamak için benzersiz bir ID
};

interface CombinedQueryResult> {
  data: TDataMap;
  isLoading: boolean;
  isError: boolean;
  error: unknown;
  isSuccess: boolean;
  isFetching: boolean;
  // Diğer useQuery dönüş değerleri de eklenebilir
}

function useCombinedQueries>(
  queryConfigs: {
    [K in keyof TDataMap]: UseQueryOptions;
  }
): CombinedQueryResult {
  const queryKeys = Object.keys(queryConfigs);
  const queries = queryKeys.map(key => queryConfigs[key]);

  const results = useQueries({ queries });

  const isLoading = results.some(result => result.isLoading);
  const isError = results.some(result => result.isError);
  const error = results.find(result => result.isError)?.error;
  const isSuccess = results.every(result => result.isSuccess);
  const isFetching = results.some(result => result.isFetching);

  const data = queryKeys.reduce((acc, key, index) => {
    acc[key as keyof TDataMap] = results[index].data;
    return acc;
  }, {} as TDataMap);

  return {
    data,
    isLoading,
    isError,
    error,
    isSuccess,
    isFetching,
  };
}

Bu gelişmiş versiyon, sorgu tanımlarını bir nesne olarak alarak, döndürülen data nesnesinin de anahtarlarıyla eşleşen tiplere sahip olmasını sağlar. Bu sayede, verilere data.users veya data.products gibi tip güvenli bir şekilde erişebiliriz.

3. Birleşik Durum Yönetimi

useQueries'in döndürdüğü sonuçlar dizisini (results) kullanarak, tüm sorguların genel yükleme, hata ve başarı durumlarını hesaplarız:

  • isLoading: Herhangi bir sorgu yükleniyorsa true.
  • isError: Herhangi bir sorgu hata verdiyse true.
  • error: Hata veren ilk sorgunun hatası.
  • isSuccess: Tüm sorgular başarılıysa true.
  • isFetching: Herhangi bir sorgu veri çekiyorsa (yeniden çekme dahil) true.
4. Veri Birleştirme

Tüm sorgulardan dönen veriler, anahtarlarıyla eşleşen tek bir nesne (data) içinde toplanır. Bu, bileşen içerisinde verilere erişimi son derece kolaylaştırır.

Gerçek Dünya Senaryolarında Kullanım ve Avantajları

Şimdi, bu hook'u gerçek dünya senaryolarında nasıl kullanabileceğimizi ve bize ne gibi avantajlar sağladığını görelim.

1. Bağımsız Çoklu Sorguları Temiz Bir Şekilde Yönetme

Daha önceki kullanıcılar ve ürünler örneğini bu yeni hook ile yeniden yazalım:


import React from 'react';
import { useCombinedQueries } from './useCombinedQueries'; // Kendi hook dosyanız

interface User { id: number; name: string; email: string; }
interface Product { id: number; name: string; price: number; }

function MyCombinedComponent() {
  const { data, isLoading, isError, error } = useCombinedQueries({
    users: {
      queryKey: ['users'],
      queryFn: () => fetch('/api/users').then(res => res.json()),
    },
    products: {
      queryKey: ['products'],
      queryFn: () => fetch('/api/products').then(res => res.json()),
    },
  });

  if (isLoading) return 
Tüm veriler yükleniyor...
; if (isError) return
Veri çekme hatası: {error?.message}
; const { users, products } = data; return (

Kullanıcılar

    {users?.map((user: User) =>
  • {user.name}
  • )}

Ürünler

    {products?.map((product: Product) =>
  • {product.name} - ${product.price}
  • )}
); }

Gördüğünüz gibi, kod çok daha temiz ve okunabilir hale geldi. Tek bir isLoading ve isError durumu ile tüm UI akışını yönetebiliyoruz ve verilere data.users ve data.products şeklinde tip güvenli bir şekilde erişebiliyoruz.

2. Bağımlı Sorguları Yönetme (Gelişmiş)

useCombinedQueries hook'unun temel versiyonu bağımsız sorgular için harikadır. Bağımlı sorguları yönetmek için ise, useQuery'nin enabled seçeneğini kullanarak veya hook'u daha da geliştirerek bir sorgunun sonucunu diğerine aktarabiliriz. İşte enabled ile bir örnek:


import React from 'react';
import { useCombinedQueries } from './useCombinedQueries';

interface User { id: number; name: string; }
interface Order { id: number; productId: number; amount: number; }

function UserDashboard({ userId }: { userId: number }) {
  const { data, isLoading, isError, error } = useCombinedQueries({
    userProfile: {
      queryKey: ['userProfile', userId],
      queryFn: () => fetch(/api/users/${userId}).then(res => res.json()),
      enabled: !!userId,
    },
    userOrders: {
      queryKey: ['userOrders', userId],
      queryFn: ({ queryKey }) => {
        const [, fetchedUserId] = queryKey;
        // Burada data.userProfile.id'yi kullanmak yerine,
        // doğrudan userId'yi veya queryKey'den gelen değeri kullanabiliriz.
        // Daha karmaşık bağımlılıklar için useCombinedQueries'i geliştirmek gerekir.
        return fetch(/api/users/${fetchedUserId}/orders).then(res => res.json());
      },
      enabled: !!userId, // userProfile'ın yüklenmesini beklemek yerine, userId'nin varlığına bağladık
      // Eğer userProfile'dan gelen bir ID'ye bağımlıysak,
      // hook'un iç mantığını bu bağımlılığı yönetmek üzere genişletmeliyiz.
      // Şimdilik, her iki sorgunun da doğrudan userId'ye bağımlı olduğunu varsayalım.
    },
  });

  if (isLoading) return 
Kullanıcı paneli yükleniyor...
; if (isError) return
Hata: {error?.message}
; const { userProfile, userOrders } = data; if (!userProfile) return
Kullanıcı bulunamadı.
; return (

{userProfile.name} Paneli

Email: {userProfile.email}

Siparişleri:

    {userOrders?.map((order: Order) =>
  • Sipariş #{order.id} - Miktar: {order.amount}
  • )}
); }

Yukarıdaki örnekte, her iki sorgu da doğrudan userId'ye bağımlı olduğu için enabled: !!userId yeterli oldu. Ancak, bir sorgunun verisinin diğerinin parametresi olduğu gerçek bir zincirleme senaryosu için useCombinedQueries'i biraz daha akıllı hale getirmemiz gerekir. Bu, genellikle queryFn içinde önceki sorguların sonuçlarına erişim sağlayarak veya hook'u daha karmaşık bir bağımlılık grafiği yönetecek şekilde tasarlayarak yapılabilir. TanStack Query'nin useQueries'i, bu tür doğrudan bağımlılıkları yönetmek için enabled seçeneğini kullanmayı teşvik eder.

Avantajları Özetle:
  • Kod Okunabilirliği: Tek bir yerden tüm sorguları tanımlama ve yönetme.
  • Basit Durum Yönetimi: Tek bir isLoading, isError ve data nesnesi ile karmaşıklığı azaltma.
  • Tip Güvenliği: TypeScript ile mükemmel entegrasyon sayesinde verilerin doğru tiplerde olduğundan emin olma.
  • Geliştirici Deneyimi: Daha az tekrar eden kod, daha hızlı geliştirme.
  • Performans: useQueries sayesinde sorguların paralel çalıştırılması ve TanStack Query'nin önbellekleme mekanizmalarından tam olarak faydalanma.

Sonuç: Daha Temiz, Daha Yönetilebilir Veri Çekme

useQuery ve Promise.all kombinasyonunun getirdiği "spagetti" kod, özellikle büyük ve karmaşık uygulamalarda geliştirici verimliliğini düşüren ve hatalara davetiye çıkaran bir sorundur. Geliştirdiğimiz useCombinedQueries hook'u, bu soruna zarif ve etkili bir çözüm sunarak, birden fazla veri çekme işlemini tek bir, okunabilir ve tip güvenli bir arayüz altında birleştirir. Bu sayede, React bileşenlerinizdeki veri çekme mantığını basitleştirebilir, daha az tekrar eden kod yazabilir ve uygulamanızın bakımını kolaylaştırabilirsiniz. Artık "useQuery/Promise.all spagettisinden" kurtulup, daha düzenli ve keyifli bir geliştirme deneyimine sahip olabilirsiniz. 🫖🦡

Sıkça Sorulan Sorular (SSS)

1. useCombinedQueries hook'u Promise.all'dan ne kadar farklı?

useCombinedQueries hook'u, temelinde TanStack Query'nin useQueries hook'unu kullanır, bu da aslında arka planda promise'ları (ve dolayısıyla Promise.all benzeri bir mekanizmayı) yönetir. Ancak fark şudur: useCombinedQueries, useQuery'nin sunduğu tüm gelişmiş özellikleri (önbellekleme, tekrar deneme, odaklanma üzerine yenileme vb.) korurken, Promise.all'ın sadece asenkron işlemleri paralel çalıştırma yeteneğini değil, aynı zamanda bu sorguların React yaşam döngüsü ile entegrasyonunu, yükleme/hata durumlarının merkezi yönetimini ve tip güvenliğini de sağlar. Kısacası, Promise.all sadece promise'ları çözerken, useCombinedQueries React Query'nin tüm ekosistemini kullanarak daha kapsamlı bir çözüm sunar.

2. Bağımlı sorguları gerçekten bu hook ile yönetebilir miyim?

Evet, ancak doğrudan bir sorgunun sonucunu diğerine parametre olarak geçirme şeklinde değil, TanStack Query'nin enabled seçeneğini kullanarak. Örneğin, ilk sorgunun verisi gelene kadar ikinci sorguyu çalıştırmamak için enabled: !!data.ilkSorguSonucu gibi bir yapı kullanabilirsiniz. Daha karmaşık bağımlılıklar için, hook'u daha gelişmiş bir bağımlılık grafiği yönetecek şekilde özelleştirmek gerekebilir, ancak çoğu senaryoda enabled yeterli olacaktır.

3. Bu hook'u kullanmak performans avantajı sağlar mı?

Evet. useCombinedQueries, TanStack Query'nin useQueries mekanizmasını kullandığı için, tüm sorgular paralel olarak başlatılır. Bu, her bir sorgunun ayrı ayrı tamamlanmasını beklemek yerine, hepsinin aynı anda ağ isteklerini yapabileceği anlamına gelir. Ayrıca, TanStack Query'nin güçlü önbellekleme ve veri senkronizasyon yeteneklerinden de tam olarak faydalanılır, bu da gereksiz ağ isteklerini önleyerek performansı artırır.

4. Hata yönetimi nasıl çalışır? Bir sorgu hata verirse ne olur?

Eğer useCombinedQueries içindeki sorgulardan herhangi biri hata verirse, hook'un döndürdüğü isError durumu true olur ve error değişkeni, hata veren ilk sorgunun hatasını içerir. Bu sayede, tek bir merkezi noktadan tüm hata durumunu yönetebilir ve kullanıcıya uygun geri bildirim sağlayabilirsiniz. TanStack Query'nin tekrar deneme (retry) mekanizmaları da varsayılan olarak devrede olacaktır.

5. Bu hook'u her zaman kullanmalı mıyım, yoksa bazı durumlarda useQuery daha mı iyi?

Eğer sadece tek bir veri parçasına ihtiyacınız varsa veya sorgularınız arasında hiçbir ilişki ve ortak bir yükleme/hata yönetimi ihtiyacı yoksa, doğrudan useQuery kullanmak daha basit ve yeterli olabilir. useCombinedQueries, özellikle birden fazla bağımsız veya bağımlı sorguyu tek bir bileşen içinde yönetmeniz gerektiğinde ve kod okunabilirliği, durum yönetimi ve tip güvenliğini artırmak istediğinizde parlar. Temel kural, kodunuz "spagetti" gibi görünmeye başladığında bu tür bir soyutlamayı düşünmektir.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.