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 मार्च 202610 मिनट पढ़ें2.2k शब्द|

हमारी Performance & Scalability श्रृंखला का हिस्सा

पूरी गाइड पढ़ें

रिएक्ट क्वेरी (टैनस्टैक): डेटा फ़ेचिंग पैटर्न

रिएक्ट में डेटा लाने का सतह क्षेत्र आश्चर्यजनक रूप से बड़ा है: लोडिंग स्थिति, त्रुटि स्थिति, बैकग्राउंड रीफ़ेचिंग, म्यूटेशन के बाद कैश अमान्यकरण, आशावादी अपडेट, अनंत स्क्रॉल, प्रीफ़ेचिंग और डीडुप्लीकेशन। इस तर्क को useEffect + useState के साथ हाथ से लिखने से दौड़ की स्थिति, मेमोरी लीक और असंगत यूआई स्थिति उत्पन्न होती है। टैनस्टैक क्वेरी (पूर्व में रिएक्ट क्वेरी) v5 एक साफ़, कंपोज़ेबल एपीआई के साथ यह सब हल करता है।

यह मार्गदर्शिका उत्पादन रिएक्ट अनुप्रयोगों के लिए टैनस्टैक क्वेरी v5 पैटर्न को कवर करती है - बुनियादी प्रश्नों से लेकर जटिल उत्परिवर्तन प्रवाह, आशावादी अपडेट, अनंत स्क्रॉल और नेक्स्ट.जेएस ऐप राउटर एकीकरण तक। प्रत्येक पैटर्न में टाइपस्क्रिप्ट और त्रुटि प्रबंधन शामिल है।

मुख्य बातें

  • टैनस्टैक क्वेरी एक सर्वर स्टेट लाइब्रेरी है - इसका उपयोग केवल क्लाइंट यूआई स्थिति के लिए न करें (उसके लिए ज़स्टैंड या यूज़स्टेट का उपयोग करें)
  • क्वेरी कुंजियाँ कैश पता हैं - इसमें वे सभी वेरिएबल शामिल हैं जो क्वेरी परिणाम को प्रभावित करते हैं
  • staleTime पृष्ठभूमि रीफ़ेचिंग ट्रिगर होने पर नियंत्रण करता है; gcTime नियंत्रित करता है कि कब अप्रयुक्त डेटा कचरा एकत्र किया जाता है
  • उत्परिवर्तन को onSuccess में queryClient.invalidateQueries() के माध्यम से संबंधित प्रश्नों को अमान्य कर देना चाहिए
  • आशावादी अपडेट कथित प्रदर्शन में सुधार करते हैं लेकिन onError में रोलबैक तर्क की आवश्यकता होती है
  • पृष्ठांकित सूचियों के लिए useInfiniteQuery का उपयोग करें; useQuery + मैन्युअल पृष्ठ स्थिति को कभी भी संयोजित न करें
  • त्वरित नेविगेशन अनुभव के लिए होवर पर प्रीफ़ेच करें: queryClient.prefetchQuery()
  • नेक्स्ट.जेएस ऐप राउटर में, क्लाइंट में सर्वर से प्राप्त डेटा को डीहाइड्रेट करने के लिए 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' });
    },
  });
}

आशावादी अपडेट

आशावादी अपडेट सर्वर की पुष्टि से पहले यूआई को अपडेट करके उत्परिवर्तन को तुरंत महसूस कराते हैं:

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

नेक्स्ट.जेएस ऐप राउटर एसएसआर + टैनस्टैक क्वेरी

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

अक्सर पूछे जाने वाले प्रश्न

मुझे टैनस्टैक क्वेरी बनाम एसडब्ल्यूआर बनाम प्लेन फ़ेच का उपयोग कब करना चाहिए?

जब आपको आवश्यकता हो तो टैनस्टैक क्वेरी का उपयोग करें: म्यूटेशन, आशावादी अपडेट, अनंत स्क्रॉल, प्रीफ़ेचिंग, बैकग्राउंड रीफ़ेचिंग, या डिडुप्लीकेशन के बाद कैश अमान्यकरण। यदि आपको छोटे बंडल और केवल साधारण उपयोग के मामलों की आवश्यकता है तो एसडब्ल्यूआर का उपयोग करें। नेक्स्ट.जेएस सर्वर कंपोनेंट्स में सादे fetch का उपयोग करें जहां आप रिएक्ट की मूल डेटा कैशिंग (cache() फ़ंक्शन और revalidatePath() पैटर्न) चाहते हैं। टैनस्टैक क्वेरी क्लाइंट-साइड इंटरएक्टिविटी के लिए है जहां उपयोगकर्ता की गतिविधियां सर्वर डेटा को प्रभावित करती हैं।

staleTime और gcTime के बीच क्या अंतर है?

staleTime यह है कि कितने समय तक डेटा को ताज़ा माना जाता है - इस दौरान, कोई बैकग्राउंड रीफ़ेच नहीं होता है और कैश्ड डेटा तुरंत वापस आ जाता है। staleTime समाप्त होने के बाद, डेटा "बासी" हो जाएगा और अगले उपयोग पर पृष्ठभूमि में पुनः प्राप्त हो जाएगा। gcTime (पहले cacheTime) यह है कि अप्रयुक्त (कोई सक्रिय ग्राहक नहीं) डेटा कचरा संग्रहण से पहले मेमोरी में कितने समय तक रहता है। आपका डेटा कितनी तेजी से बदलता है, इसके आधार पर staleTime सेट करें; आप तुरंत महसूस करने के लिए कितनी देर तक दूर जाना और वापस आना चाहते हैं, इसके आधार पर gcTime सेट करें।

मैं वैश्विक स्तर पर प्रमाणीकरण त्रुटियों (401) को कैसे संभालूं?

अपने QueryClient डिफ़ॉल्ट विकल्पों में एक वैश्विक त्रुटि हैंडलर जोड़ें। 401 पर, लॉगिन करने के लिए रीडायरेक्ट करें और क्वेरी कैश साफ़ करें। टैनस्टैक क्वेरी v5 में: defaultOptions: { queries: { throwOnError: (error) => { if (error.status === 401) { router.push('/auth/login'); queryClient.clear(); return false; } return true; } } }। वैकल्पिक रूप से, logout() फ़ंक्शन को कॉल करके अपने apiFetch हेल्पर में 401 को संभालें।

मैं प्रोप ड्रिलिंग के बिना सहोदर घटकों के बीच क्वेरी स्थिति कैसे साझा करूं?

कोई भी घटक जो समान क्वेरी कुंजी के साथ useQuery को कॉल करता है, वही कैश्ड डेटा साझा करता है - टैनस्टैक क्वेरी स्वचालित रूप से अनुरोधों को काट देती है। दो ContactsTable घटक और एक ContactCount बैज, जो समान फ़िल्टर के साथ useContacts({ page: 1, limit: 20 }) को कॉल करते हैं, सभी एक ही कैश्ड अनुरोध से पढ़े जाएंगे। कोई प्रोप ड्रिलिंग नहीं, किसी संदर्भ की आवश्यकता नहीं।

क्या क्वेरी कुंजियों में उपयोगकर्ता का सत्र या संगठन आईडी शामिल होना चाहिए?

हाँ - यदि डेटा का दायरा किसी उपयोगकर्ता या संगठन तक है, तो क्वेरी कुंजी में स्कोपिंग पहचानकर्ता शामिल करें। यह एक उपयोगकर्ता के डेटा को दूसरे के लिए प्रदर्शित होने से रोकता है (बहु-किरायेदार ऐप्स के लिए महत्वपूर्ण)। लॉगआउट पर संपूर्ण क्वेरी कैश भी साफ़ करें: queryClient.clear() या queryClient.removeQueries() - अन्यथा पिछले सत्र का पुराना डेटा अगले उपयोगकर्ता के लिए संक्षेप में फ़्लैश हो सकता है।


अगले चरण

टैनस्टैक क्वेरी राज्य प्रबंधन समस्या से प्राप्त होने वाले रिएक्ट डेटा को एक घोषणात्मक, कैश-जागरूक प्रणाली में बदल देती है। आशावादी अपडेट, प्रीफ़ेचिंग और एसएसआर हाइड्रेशन आपके उपयोगकर्ताओं को एक ऐसा अनुभव प्रदान करते हैं जो धीमे कनेक्शन पर भी तुरंत महसूस होता है।

ECOSIRE सभी सर्वर स्थिति के लिए टैनस्टैक क्वेरी के साथ नेक्स्ट.जेएस एडमिन पैनल और ग्राहक पोर्टल बनाता है, जो क्लाइंट-केवल यूआई स्थिति के लिए ज़स्टैंड और डेटा-हैवी व्यू के लिए टैनस्टैक टेबल के साथ संयुक्त है। हमारी फ्रंटएंड इंजीनियरिंग सेवाओं का अन्वेषण करें यह जानने के लिए कि हम परफॉर्मेंट, उत्पादन के लिए तैयार रिएक्ट एप्लिकेशन कैसे बनाते हैं।

शेयर करें:
E

लेखक

ECOSIRE Research and Development Team

ECOSIRE में एंटरप्राइज़-ग्रेड डिजिटल उत्पाद बना रहे हैं। Odoo एकीकरण, ई-कॉमर्स ऑटोमेशन, और AI-संचालित व्यावसायिक समाधानों पर अंतर्दृष्टि साझा कर रहे हैं।

WhatsApp पर चैट करें