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 مارچ، 202611 منٹ پڑھیں2.3k الفاظ|

ہماری Performance & Scalability سیریز کا حصہ

مکمل گائیڈ پڑھیں

React Query (TanStack): ڈیٹا لانے والے پیٹرنز

React میں ڈیٹا کی بازیافت کا سطحی رقبہ حیرت انگیز طور پر بڑا ہوتا ہے: لوڈنگ اسٹیٹس، ایرر اسٹیٹس، بیک گراؤنڈ ری فیچنگ، میوٹیشنز کے بعد کیشے کی غلط کاری، پرامید اپ ڈیٹس، لامحدود اسکرول، پری فیچنگ، اور ڈپلیکیشن۔ اس منطق کو ہاتھ سے useEffect + useState کے ساتھ لکھنے سے ریس کے حالات، میموری لیک ہونے اور UI کی متضاد حالتیں پیدا ہوتی ہیں۔ TanStack Query (پہلے React Query) v5 صاف، کمپوز ایبل API کے ساتھ ان سب کو حل کرتا ہے۔

یہ گائیڈ پروڈکشن ری ایکٹ ایپلی کیشنز کے لیے TanStack Query v5 پیٹرنز کا احاطہ کرتی ہے — پیچیدہ تبدیلی کے بہاؤ، پرامید اپ ڈیٹس، لامحدود اسکرول، اور Next.js App Router انٹیگریشن کے ذریعے بنیادی سوالات سے۔ ہر پیٹرن میں TypeScript اور ایرر ہینڈلنگ شامل ہوتی ہے۔

اہم ٹیک ویز

  • TanStack Query ایک سرور اسٹیٹ لائبریری ہے — اسے کلائنٹ کے لیے صرف UI اسٹیٹ کے لیے استعمال نہ کریں (اس کے لیے Zustand یا UseState استعمال کریں)
  • استفسار کی کلیدیں کیش ایڈریس ہیں - ان تمام متغیرات کو شامل کریں جو استفسار کے نتیجے کو متاثر کرتے ہیں۔
  • staleTime کنٹرول کرتا ہے جب بیک گراؤنڈ ری فیچنگ ٹرگر کرتا ہے۔ gcTime کنٹرول کرتا ہے جب غیر استعمال شدہ ڈیٹا کوڑا کرکٹ جمع کیا جاتا ہے۔
  • تغیرات کو onSuccess میں queryClient.invalidateQueries() کے ذریعے متعلقہ سوالات کو باطل کرنا چاہئے
  • پرامید اپ ڈیٹس سمجھی جانے والی کارکردگی کو بہتر بناتی ہیں لیکن onError میں رول بیک منطق کی ضرورت ہوتی ہے۔
  • صفحہ بندی کی فہرستوں کے لیے useInfiniteQuery استعمال کریں؛ کبھی بھی useQuery + دستی صفحہ کی حالت کو یکجا نہ کریں۔
  • فوری نیویگیشن کے احساس کے لیے ہوور پر پری فیچ کریں: queryClient.prefetchQuery()
  • Next.js ایپ راؤٹر میں، کلائنٹ میں سرور سے حاصل کردہ ڈیٹا کو ڈی ہائیڈریٹ کرنے کے لیے HydrationBoundary استعمال کریں

سیٹ اپ اور کنفیگریشن

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>
  );
}

سوال کی چابیاں - فاؤنڈیشن

استفسار کی کلیدیں آپ کے ڈیٹا کے لیے کیشے کا پتہ ہیں۔ ہر متغیر جو استفسار کے نتیجے کو متاثر کرتا ہے کلید میں ہونا چاہیے۔

// 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

بنیادی سوالات

// 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>
  );
}

کیشے کی غلط کاری کے ساتھ تغیرات

// 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' });
    },
  });
}

پر امید اپڈیٹس

پرامید اپ ڈیٹس سرور کی تصدیق کرنے سے پہلے UI کو اپ ڈیٹ کر کے تغیرات کو فوری محسوس کرتے ہیں:

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) });
    },
  });
}

لامحدود اسکرول / صفحہ بندی

// 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>
  );
}

فوری نیویگیشن کے لیے پیشگی بازیافت

// 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 ایپ راؤٹر SSR + TanStack استفسار

// 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 ?? []} />;
}

اکثر پوچھے گئے سوالات

مجھے TanStack Query بمقابلہ SWR بمقابلہ سادہ بازیافت کب استعمال کرنا چاہئے؟

جب آپ کو ضرورت ہو تو TanStack استفسار کا استعمال کریں: اتپریورتنوں کے بعد کیشے کی غلط کاری، پرامید اپ ڈیٹس، لامحدود اسکرول، پری فیچنگ، بیک گراؤنڈ ری فیچنگ، یا ڈپلیکیشن کی درخواست کریں۔ اگر آپ کو چھوٹے بنڈل کی ضرورت ہو اور صرف سادہ استعمال کے معاملات کی ضرورت ہو تو SWR استعمال کریں۔ Next.js سرور کے اجزاء میں سادہ fetch استعمال کریں جہاں آپ React کا مقامی ڈیٹا کیشنگ چاہتے ہیں (cache() فنکشن اور revalidatePath() پیٹرن)۔ TanStack Query کلائنٹ سائیڈ انٹرایکٹیویٹی کے لیے ہے جہاں صارف کے اعمال سرور کے ڈیٹا کو متاثر کرتے ہیں۔

staleTime اور gcTime میں کیا فرق ہے؟

staleTime یہ ہے کہ کتنے عرصے تک ڈیٹا کو تازہ سمجھا جاتا ہے — اس وقت کے دوران، کوئی بیک گراؤنڈ ری فیچ نہیں ہوتا ہے اور کیشڈ ڈیٹا فوری طور پر واپس کر دیا جاتا ہے۔ staleTime کی میعاد ختم ہونے کے بعد، ڈیٹا "باسی" ہے اور اگلے استعمال پر پس منظر میں دوبارہ حاصل کیا جائے گا۔ gcTime (پہلے cacheTime) یہ ہے کہ کب تک غیر استعمال شدہ (کوئی فعال سبسکرائبر نہیں) ڈیٹا کوڑا کرکٹ جمع کرنے سے پہلے میموری میں رہتا ہے۔ staleTime اس بنیاد پر سیٹ کریں کہ آپ کا ڈیٹا کتنی تیزی سے تبدیل ہوتا ہے۔ gcTime اس بنیاد پر سیٹ کریں کہ آپ کتنی دیر تک فوری طور پر محسوس کرنے کے لیے دور اور واپس جانا چاہتے ہیں۔

میں عالمی سطح پر تصدیق کی غلطیوں (401) کو کیسے ہینڈل کروں؟

اپنے QueryClient ڈیفالٹ اختیارات میں ایک عالمی ایرر ہینڈلر شامل کریں۔ 401 پر، لاگ ان کرنے کے لیے ری ڈائریکٹ کریں اور استفسار کیش کو صاف کریں۔ TanStack Query v5 میں: defaultOptions: { queries: { throwOnError: (error) => { if (error.status === 401) { router.push('/auth/login'); queryClient.clear(); return false; } return true; } } }۔ متبادل طور پر، logout() فنکشن کو کال کرکے اپنے apiFetch مددگار میں 401 کو ہینڈل کریں۔

میں پروپ ڈرلنگ کے بغیر بہن بھائیوں کے درمیان استفسار کی حالت کا اشتراک کیسے کروں؟

کوئی بھی جزو جو ایک ہی سوال کلید کے ساتھ useQuery کو کال کرتا ہے وہی کیشڈ ڈیٹا شیئر کرتا ہے — TanStack Query درخواستوں کو خود بخود ڈپلیکیٹ کرتا ہے۔ دو ContactsTable اجزاء اور ایک ContactCount بیج کالنگ useContacts({ page: 1, limit: 20 }) ایک ہی فلٹرز کے ساتھ سبھی ایک ہی کیش شدہ درخواست سے پڑھیں گے۔ کوئی سہارا ڈرلنگ، کوئی سیاق و سباق کی ضرورت نہیں.

کیا استفسار کی کلیدوں میں صارف کا سیشن یا تنظیم کا ID شامل ہونا چاہیے؟

ہاں — اگر ڈیٹا کا دائرہ کسی صارف یا تنظیم کے لیے ہے، تو استفسار کلید میں اسکوپنگ شناخت کنندہ کو شامل کریں۔ یہ ایک صارف کا ڈیٹا دوسرے کے لیے ظاہر ہونے سے روکتا ہے (ملٹی کرایہ دار ایپس کے لیے اہم)۔ لاگ آؤٹ پر پوری استفسار کیش کو بھی صاف کریں: queryClient.clear() یا queryClient.removeQueries() — بصورت دیگر پچھلے سیشن کا باسی ڈیٹا اگلے صارف کے لیے مختصر طور پر چمک سکتا ہے۔


اگلے اقدامات

TanStack Query ریاستی انتظامی مسئلے سے React ڈیٹا کی بازیافت کو اعلانیہ، کیشے سے آگاہی والے نظام میں تبدیل کرتا ہے۔ پرامید اپ ڈیٹس، پری فیچنگ، اور SSR ہائیڈریشن آپ کے صارفین کو ایک ایسا تجربہ فراہم کرتے ہیں جو فوری محسوس ہوتا ہے — یہاں تک کہ سست روابط پر بھی۔

ECOSIRE تمام سرور اسٹیٹ کے لیے TanStack Query کے ساتھ Next.js ایڈمن پینلز اور کسٹمر پورٹلز بناتا ہے، جو صرف کلائنٹ کے لیے UI اسٹیٹ کے لیے Zustand اور ڈیٹا ہیوی ویوز کے لیے TanStack ٹیبل کے ساتھ مل کر بناتا ہے۔ ہماری فرنٹ اینڈ انجینئرنگ سروسز کو دریافت کریں یہ جاننے کے لیے کہ ہم پرفارمنس، پروڈکشن کے لیے تیار ری ایکٹ ایپلی کیشنز کیسے بناتے ہیں۔

E

تحریر

ECOSIRE Research and Development Team

ECOSIRE میں انٹرپرائز گریڈ ڈیجیٹل مصنوعات بنانا۔ Odoo انٹیگریشنز، ای کامرس آٹومیشن، اور AI سے چلنے والے کاروباری حل پر بصیرت شیئر کرنا۔

Chat on WhatsApp