React
۴ دقیقه مطالعهReact Query: از کانفیگ تا تکنیکهای پیشرفته، و کِی واقعاً استفادهش کنیم
کانفیگ پایه، mutation + invalidation، کوئریهای وابسته، آپدیت optimistic، prefetch سمت سرور با hydration — و یه قانون کلی روشن برای React Query در برابر Redux Toolkit / Zustand / Jotai.

React Query (TanStack Query) یه مسئلهی متفاوت از Redux Toolkit، Zustand یا Jotai حل میکنه. اونها client state رو مدیریت میکنن — دادهای که اپت کاملاً مالکشه: پرچم باز/بستهی یه مودال، پیشنویس یه فرم، toggle تم. React Query server state رو مدیریت میکنه — دادهای که یهجای دیگه زندگی میکنه، فقط یه کپی ازش قرض گرفتی، ممکنه بیسروصدا بیات بشه و به کششدن، دیداپلیکیت و refetch پسزمینه نیاز داره. سراغ ابزار غلط رفتن برای هرکدوم از این دوتا کار، منشأ بیشتر دردسرهاست.
کانفیگ پایه
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، چیزیه که UI رو sync نگه میداره بدون یه فراخوانی refetch دستی که همهجا پخش شده باشه.
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
});پیشرفته — آپدیت optimistic
کش رو فوری آپدیت کن، قبل از تایید سرور، و اگه mutation fail شد برگردون عقب — UI فوری حس میشه بهجای اینکه منتظر یه رفتوبرگشت بمونه.
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] });
},
});پیشرفته — prefetch سمت سرور + hydration (App Router)
روی سرور prefetch کن تا اولین صدازدن 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 برای client state محلی باقیمونده — و اصلاً به Redux نیاز پیدا نمیکنن مگر اینکه خودِ client state سمت کلاینت بزرگ و واقعاً cross-cutting باشه.