جزء من سلسلة Performance & Scalability
اقرأ الدليل الكاملReact Query (TanStack): أنماط جلب البيانات
يمتلك جلب البيانات في React مساحة سطحية كبيرة بشكل مدهش: حالات التحميل، وحالات الخطأ، وإعادة جلب الخلفية، وإبطال ذاكرة التخزين المؤقت بعد الطفرات، والتحديثات المتفائلة، والتمرير اللانهائي، والجلب المسبق، وإلغاء البيانات المكررة. تؤدي كتابة هذا المنطق يدويًا باستخدام useEffect + useState إلى حالات سباق وتسرب للذاكرة وحالات واجهة مستخدم غير متناسقة. يعمل الإصدار 5 من TanStack Query (المعروف سابقًا باسم React Query) على حل كل هذه المشكلات باستخدام واجهة برمجة تطبيقات نظيفة وقابلة للتركيب.
يغطي هذا الدليل أنماط TanStack Query v5 لتطبيقات React الإنتاجية - بدءًا من الاستعلامات الأساسية وحتى تدفقات الطفرة المعقدة والتحديثات المتفائلة والتمرير اللانهائي وتكامل Next.js App Router. يتضمن كل نمط TypeScript ومعالجة الأخطاء.
الوجبات الرئيسية
- استعلام TanStack عبارة عن مكتبة حالة خادم - لا تستخدمها لحالة واجهة المستخدم الخاصة بالعميل فقط (استخدم Zustand أو useState لذلك)
- مفاتيح الاستعلام هي عنوان ذاكرة التخزين المؤقت - تتضمن كافة المتغيرات التي تؤثر على نتيجة الاستعلام
- عناصر التحكم
staleTimeعند تشغيل إعادة جلب الخلفية؛ يتحكمgcTimeفي تجميع البيانات غير المستخدمة- يجب أن تؤدي الطفرات إلى إبطال الاستعلامات ذات الصلة عبر
queryClient.invalidateQueries()فيonSuccess- تعمل التحديثات المتفائلة على تحسين الأداء الملحوظ ولكنها تتطلب منطق التراجع في
onError- استخدم
useInfiniteQueryللقوائم المرقّمة؛ لا تجمع مطلقًا بينuseQuery+ حالة الصفحة اليدوية- الجلب المسبق عند التمرير للتمتع بالتنقل الفوري:
queryClient.prefetchQuery()- في Next.js App Router، استخدم
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>
);
}
Next.js App Router 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 إذا كنت بحاجة إلى حزمة أصغر وحالات استخدام بسيطة فقط. استخدم fetch العادي في مكونات خادم Next.js حيث تريد تخزين بيانات React الأصلية مؤقتًا (الدالة cache() ونمط revalidatePath()). TanStack Query مخصص للتفاعل من جانب العميل حيث تؤثر إجراءات المستخدم على بيانات الخادم.
ما الفرق بين staleTime و gcTime؟
staleTime هو المدة التي تعتبر فيها البيانات حديثة - خلال هذا الوقت، لا تحدث عملية إعادة سحب للخلفية ويتم إرجاع البيانات المخزنة مؤقتًا على الفور. بعد انتهاء صلاحية staleTime، تصبح البيانات "قديمة" وسيتم إعادة جلبها في الخلفية عند الاستخدام التالي. gcTime (سابقًا cacheTime) هو المدة التي تبقى فيها البيانات غير المستخدمة (لا يوجد مشتركون نشطون) في الذاكرة قبل تجميع البيانات المهملة. قم بتعيين staleTime بناءً على مدى سرعة تغير بياناتك؛ قم بتعيين gcTime بناءً على المدة التي تريد التنقل فيها بعيدًا والعودة لتشعر باللحظة.
كيف أتعامل مع أخطاء المصادقة (401) عالميًا؟
أضف معالج الأخطاء العام في خيارات QueryClient الافتراضية. في 401، قم بإعادة التوجيه لتسجيل الدخول ومسح ذاكرة التخزين المؤقت للاستعلام. في استعلام TanStack v5: defaultOptions: { queries: { throwOnError: (error) => { if (error.status === 401) { router.push('/auth/login'); queryClient.clear(); return false; } return true; } } }. بدلاً من ذلك، تعامل مع 401 في مساعد apiFetch الخاص بك عن طريق استدعاء دالة logout().
كيف يمكنني مشاركة حالة الاستعلام بين المكونات الشقيقة دون حفر الدعامات؟
أي مكون يستدعي useQuery بنفس مفتاح الاستعلام يشارك نفس البيانات المخزنة مؤقتًا - يقوم TanStack Query بإلغاء تكرار الطلبات تلقائيًا. سيتم قراءة مكونين ContactsTable وشارة ContactCount التي تستدعي useContacts({ page: 1, limit: 20 }) بنفس عوامل التصفية من طلب واحد مخبأ. لا حاجة لحفر الدعامة، ولا حاجة إلى سياق.
هل يجب أن تتضمن مفاتيح الاستعلام معرف جلسة المستخدم أو معرف المؤسسة؟
نعم — إذا تم تحديد نطاق البيانات لمستخدم أو مؤسسة، فقم بتضمين معرف النطاق في مفتاح الاستعلام. يؤدي هذا إلى منع ظهور بيانات مستخدم لآخر (وهو أمر بالغ الأهمية للتطبيقات متعددة المستأجرين). امسح أيضًا ذاكرة التخزين المؤقت للاستعلام بالكامل عند تسجيل الخروج: queryClient.clear() أو queryClient.removeQueries() - وإلا فقد تومض البيانات القديمة من الجلسة السابقة لفترة وجيزة للمستخدم التالي.
الخطوات التالية
يقوم TanStack Query بتحويل جلب بيانات React من مشكلة إدارة الحالة إلى نظام تعريفي مدرك لذاكرة التخزين المؤقت. تمنح التحديثات المتفائلة والجلب المسبق وترطيب SSR للمستخدمين تجربة تبدو فورية - حتى عبر الاتصالات البطيئة.
تقوم ECOSIRE بإنشاء لوحات إدارة Next.js وبوابات العملاء باستخدام TanStack Query لجميع حالات الخادم، بالإضافة إلى Zustand لحالة واجهة المستخدم الخاصة بالعميل فقط وTanStack Table لطرق العرض كثيفة البيانات. استكشف خدماتنا الهندسية للواجهة الأمامية لتتعرف على كيفية إنشاء تطبيقات React عالية الأداء وجاهزة للإنتاج.
بقلم
ECOSIRE Research and Development Team
بناء منتجات رقمية بمستوى المؤسسات في ECOSIRE. مشاركة رؤى حول تكاملات Odoo وأتمتة التجارة الإلكترونية وحلول الأعمال المدعومة بالذكاء الاصطناعي.
مقالات ذات صلة
Next.js 16 App Router: Production Patterns and Pitfalls
Production-ready Next.js 16 App Router patterns: server components, caching strategies, metadata API, error boundaries, and performance pitfalls to avoid.
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Optimizing AI Agent Costs: Token Usage and Caching
Practical strategies for reducing AI agent operational costs through token optimization, caching, model routing, and usage monitoring. Real savings from production OpenClaw deployments.
المزيد من Performance & Scalability
k6 Load Testing: Stress-Test Your APIs Before Launch
Master k6 load testing for Node.js APIs. Covers virtual user ramp-ups, thresholds, scenarios, HTTP/2, WebSocket testing, Grafana dashboards, and CI integration patterns.
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Odoo Performance Tuning: PostgreSQL and Server Optimization
Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.
Odoo vs Acumatica: Cloud ERP for Growing Businesses
Odoo vs Acumatica compared for 2026: unique pricing models, scalability, manufacturing depth, and which cloud ERP fits your growth trajectory.
Testing and Monitoring AI Agents in Production
A complete guide to testing and monitoring AI agents in production environments. Covers evaluation frameworks, observability, drift detection, and incident response for OpenClaw deployments.
Compliance Monitoring Agents with OpenClaw
Deploy OpenClaw AI agents for continuous compliance monitoring. Automate regulatory checks, policy enforcement, audit trail generation, and compliance reporting.