React
قراءة في 4 دقائقReact Query: من الإعداد إلى التقنيات المتقدمة، ومتى نحتاجه فعلًا
الإعداد الأساسي، والـ mutation مع الـ invalidation، والاستعلامات المعتمدة، والتحديثات المتفائلة، والجلب المسبق على الخادم مع hydration — وقاعدة واضحة للمفاضلة بين React Query وRedux Toolkit / Zustand / Jotai.

يحل React Query (وهو TanStack Query) مشكلة مختلفة عمّا تحله Redux Toolkit أو Zustand أو Jotai. تلك الأدوات تدير حالة العميل — بيانات يملكها تطبيقك بالكامل: علم فتح نافذة منبثقة، أو مسودة نموذج، أو تبديل السمة. أما React Query فيدير حالة الخادم — بيانات تعيش في مكان آخر، وأنت تستعير نسخة منها فقط، وقد تصبح قديمة بصمت، وتحتاج إلى تخزين مؤقت وإزالة التكرار وإعادة جلب في الخلفية. واستخدام الأداة الخاطئة لأيٍّ من المهمتين هو مصدر معظم المتاعب.
الإعداد الأساسي
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // data is "fresh" for 1 minute — no refetch on remount in this window
gcTime: 5 * 60 * 1000, // unused cache entries are garbage-collected after 5 minutes
refetchOnWindowFocus: false, // noisy default for most admin/dashboard UIs — turn it off on purpose
retry: 1,
},
},
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}الشكل الأساسي
function useProject(slug: string) {
return useQuery({
queryKey: ["project", slug],
queryFn: () => fetch("/api/projects/" + slug).then((r) => r.json()),
});
}الـ mutation مع الـ invalidation
يغيّر الـ mutation البيانات؛ أما ذاكرة الاستعلامات المؤقتة فلا تعرف بهذا التغيير حتى تخبرها. إبطال (invalidate) المفاتيح المرتبطة بعد نجاح الـ mutation هو ما يُبقي الواجهة متزامنة، دون استدعاءات إعادة جلب يدوية منتشرة في كل مكان.
function useUpdateProject(slug: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: ProjectInput) =>
fetch("/api/projects/" + slug, { method: "PATCH", body: JSON.stringify(data) }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["project", slug] });
queryClient.invalidateQueries({ queryKey: ["projects"] }); // the list view too
},
});
}متقدم — الاستعلامات المعتمدة
يمكن لاستعلام أن ينتظر بيانات استعلام آخر قبل أن يُسمح له بالعمل:
const { data: user } = useQuery({ queryKey: ["user"], queryFn: getUser });
const { data: orders } = useQuery({
queryKey: ["orders", user?.id],
queryFn: () => getOrders(user!.id),
enabled: !!user?.id, // does not run until user.id exists
});متقدم — التحديثات المتفائلة
حدّث الذاكرة المؤقتة فورًا قبل تأكيد الخادم، وتراجع إن فشل الـ mutation — فتبدو الواجهة لحظية بدل أن تنتظر رحلة ذهاب وإياب.
useMutation({
mutationFn: updateProject,
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: ["project", slug] });
const previous = queryClient.getQueryData(["project", slug]);
queryClient.setQueryData(["project", slug], newData);
return { previous };
},
onError: (_err, _newData, context) => {
queryClient.setQueryData(["project", slug], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["project", slug] });
},
});متقدم — الجلب المسبق على الخادم مع hydration (App Router)
اجلب مسبقًا على الخادم، فيقرأ أول استدعاء لـ useQuery على العميل ذاكرة مؤقتة جاهزة بدل إرسال طلب جديد وإظهار حالة التحميل.
// Server Component
export default async function ProjectPage({ params }: { params: { slug: string } }) {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ["project", params.slug],
queryFn: () => getProject(params.slug),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProjectView slug={params.slug} />
</HydrationBoundary>
);
}
// Client Component (project-view.tsx) — reads the already-warm cache, no loading flash.
"use client";
function ProjectView({ slug }: { slug: string }) {
const { data } = useQuery({ queryKey: ["project", slug], queryFn: () => getProject(slug) });
return <h1>{data.title}</h1>;
}متقدم — الترقيم دون وميض التحميل
const { data } = useQuery({
queryKey: ["projects", page],
queryFn: () => getProjects(page),
placeholderData: keepPreviousData, // keep showing the previous page until the next one loads
});متى React Query ومتى Redux Toolkit / Zustand / Jotai
هذه الأدوات ليست متنافسة. معظم التطبيقات الحقيقية تنتهي باستخدام React Query لكل ما يأتي عبر الشبكة، وشريحة صغيرة من Zustand أو Jotai لما تبقى من حالة العميل المحلية — ولا تحتاج إلى Redux إلا إذا كانت حالة العميل نفسها كبيرة وعابرة للأجزاء فعلًا.