بازگشت به وبلاگ

React Query: از کانفیگ تا تکنیک‌های پیشرفته، و کِی واقعاً استفاده‌ش کنیم

۲ مرداد ۱۴۰۵4 دقیقه مطالعه
React Query: از کانفیگ تا تکنیک‌های پیشرفته، و کِی واقعاً استفاده‌ش کنیم

React Query (TanStack Query) یه مسئله‌ی متفاوت از Redux Toolkit، Zustand یا Jotai حل می‌کنه. اون‌ها client state رو مدیریت می‌کنن — داده‌ای که اپت کاملاً مالکشه: پرچم باز/بسته‌ی یه مودال، پیش‌نویس یه فرم، toggle تم. React Query سرو‌ر استیت رو مدیریت می‌کنه — داده‌ای که یه‌جای دیگه زندگی می‌کنه، فقط یه کپی ازش قرض گرفتی، ممکنه بی‌سروصدا بیات بشه و به کش‌شدن، دی‌داپلیکیت و refetch پس‌زمینه نیاز داره. سراغ ابزار غلط رفتن برای هرکدوم از این دوتا کار، منشأ بیشتر دردسرهاست.

کانفیگ پایه

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // داده ۱ دقیقه "تازه"ست — توی این بازه موقع remount دوباره fetch نمی‌شه
      gcTime: 5 * 60 * 1000, // ورودی‌های کش استفاده‌نشده بعد ۵ دقیقه garbage-collect می‌شن
      refetchOnWindowFocus: false, // پیش‌فرض شلوغی برای بیشتر UIهای ادمین/داشبورد — عمداً خاموشش کن
      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"] }); // نمای لیست هم
    },
  });
}

پیشرفته — کوئری‌های وابسته

یه کوئری می‌تونه قبل از اجازه‌ی اجرا، منتظر داده‌ی یه کوئری دیگه بمونه:

const { data: user } = useQuery({ queryKey: ["user"], queryFn: getUser });

const { data: orders } = useQuery({
  queryKey: ["orders", user?.id],
  queryFn: () => getOrders(user!.id),
  enabled: !!user?.id, // تا وقتی user.id وجود نداشته باشه اجرا نمی‌شه
});

پیشرفته — آپدیت 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 — کش از قبل گرم رو می‌خونه، بدون فلاش لودینگ.
("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, // تا لود شدن صفحه‌ی جدید، صفحه‌ی قبلی رو نشون بده
});

کِی React Query و کِی Redux Toolkit / Zustand / Jotai

سوال

جواب

این داده از یه fetch اومده و ممکنه بی‌سروصدا بیات بشه؟

React Query — دقیقاً کار خودشه: کش‌کردن، دی‌داپلیکیت، refetch پس‌زمینه، retry.

فقط client state محلیه که کاملاً مالکشی (باز بودن مودال، پیش‌نویس فرم، تب فعال)؟

Zustand / Jotai — نه شبکه‌ای در کاره، نه به یه سیاست invalidation کش نیاز داره.

به client state سنگین و cross-cutting توی یه اپ بزرگ، با آپدیت‌های سخت‌گیرانه و قابل‌ردیابی نیاز داری؟

Redux Toolkit — هنوز برای دقیقاً همین شکل از مسئله انتخاب درستیه.

داری همزمان React Query و Redux رو برای همون داده‌ی سرور استفاده می‌کنی؟

اون slice رداکس رو بردار — الان داره با کش React Query می‌جنگه، نه اینکه مکملش باشه.

این‌ها رقیب هم نیستن. بیشتر اپ‌های واقعی آخرش React Query رو برای هرچی از شبکه اومده استفاده می‌کنن، و یه تیکه‌ی کوچیک Zustand یا Jotai برای client state محلی باقی‌مونده — و اصلاً به Redux نیاز پیدا نمی‌کنن مگر اینکه خودِ client state سمت کلاینت بزرگ و واقعاً cross-cutting باشه.