مقالات › React

دریافت داده در React با TanStack Query؛ کش، Mutation و صفحه‌بندی

داده‌ی سرور را در React اصولی با TanStack Query بگیرید: Query، کلیدهای کوئری، کش و staleTime، Mutation، ابطال کش، به‌روزرسانی خوش‌بینانه، صفحه‌بندی و پیش‌بارگذاری.

دریافت داده با useEffect و useState ساده به نظر می‌رسد؛ تا وقتی که به وضعیت بارگذاری، مدیریت خطا، کش، دریافت مجدد هنگام برگشت به تب، حذف درخواست‌های تکراری، تلاش مجدد، صفحه‌بندی و همگام نگه داشتن داده بعد از یک تغییر نیاز پیدا کنید. TanStack Query (نام قبلی React Query) همه‌ی این‌ها را مدیریت می‌کند تا کامپوننت‌ها فقط توصیف کنند چه داده‌ای لازم دارند، نه اینکه چطور مدیریتش کنند.

راه‌اندازی

npm install @tanstack/react-query
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,          // داده تا ۱ دقیقه تازه محسوب می‌شود
      retry: 1,
      refetchOnWindowFocus: true,
    },
  },
});

createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

در زمان توسعه @tanstack/react-query-devtools را اضافه کنید تا کش را به‌صورت بصری بررسی کنید.

اولین Query

import { useQuery } from "@tanstack/react-query";

async function fetchArticles() {
  const res = await fetch("/api/articles");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

function ArticleList() {
  const { data, isPending, isError, error, isFetching } = useQuery({
    queryKey: ["articles"],
    queryFn: fetchArticles,
  });

  if (isPending) return <Spinner />;
  if (isError) return <ErrorBox message={error.message} />;

  return (
    <>
      {isFetching && <small>Refreshing…</small>}
      {data.map((a) => <ArticleCard key={a.id} article={a} />)}
    </>
  );
}
  • isPending — هنوز داده‌ای وجود ندارد (بارگذاری اول).
  • isFetching — هر درخواستی در جریان است، حتی به‌روزرسانی‌های پس‌زمینه.
  • queryFn باید در صورت شکست خطا پرتاب کند تا TanStack Query متوجه خطا شود.

کلید کوئری؛ قلب کش

queryKey داده‌ی کش‌شده را به‌طور یکتا شناسایی می‌کند. هر متغیری که کوئری به آن وابسته است را در آن بیاورید:

function useArticle(slug) {
  return useQuery({
    queryKey: ["articles", slug],
    queryFn: ({ signal }) => api.getArticle(slug, { signal }),
    enabled: Boolean(slug),       // تا slug نداریم اجرا نشود
  });
}

function useArticleSearch(filters) {
  return useQuery({
    queryKey: ["articles", "list", filters],   // { tag, page, q }
    queryFn: () => api.searchArticles(filters),
  });
}

وقتی کلید تغییر کند، کوئری جدیدی اجرا می‌شود؛ وقتی دو کامپوننت از یک کلید استفاده کنند، یک درخواست و یک ورودی کش مشترک دارند. signal اجازه می‌دهد TanStack Query درخواست‌های قدیمی را خودکار لغو کند.

نکته: کلیدها را سلسله‌مراتبی سازمان دهید (["articles"]، ["articles", "list", filters]، ["articles", slug]). آن‌وقت ابطال ["articles"] همه‌ی چیزهای مربوط به مقالات را یک‌جا تازه می‌کند.

تفاوت staleTime و gcTime

دو تنظیم کش را کنترل می‌کنند:

گزینه معنی پیش‌فرض
staleTime مدتی که داده تازه محسوب می‌شود (بدون دریافت مجدد) 0
gcTime مدتی که داده‌ی بلااستفاده در حافظه می‌ماند ۵ دقیقه

با پیش‌فرض staleTime: 0، داده‌ی کش‌شده فوراً نمایش داده می‌شود اما هنگام Mount یا برگشت به پنجره در پس‌زمینه دوباره دریافت می‌شود («Stale-While-Revalidate»). برای داده‌هایی که کم تغییر می‌کنند — فهرست کشورها، مقالات منتشرشده — staleTime را به چند دقیقه یا بیشتر برسانید.

Mutation؛ تغییر داده‌ها

import { useMutation, useQueryClient } from "@tanstack/react-query";

function useCreateComment(slug) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (text) => api.addComment(slug, text),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["comments", slug] });
    },
  });
}

function CommentForm({ slug }) {
  const createComment = useCreateComment(slug);
  const [text, setText] = useState("");

  return (
    <form onSubmit={(e) => { e.preventDefault(); createComment.mutate(text, { onSuccess: () => setText("") }); }}>
      <textarea value={text} onChange={(e) => setText(e.target.value)} />
      <button disabled={createComment.isPending}>
        {createComment.isPending ? "Sending…" : "Send"}
      </button>
      {createComment.isError && <p role="alert">{createComment.error.message}</p>}
    </form>
  );
}

ابطال (Invalidation) کوئری‌ها را کهنه علامت می‌زند و کوئری‌های فعال را دوباره دریافت می‌کند تا رابط کاربری بعد از هر تغییر، داده‌ی تازه‌ی سرور را نشان دهد.

به‌روزرسانی خوش‌بینانه (Optimistic Update)

برای بازخورد فوری (لایک، روشن/خاموش کردن، تغییر ترتیب)، کش را قبل از پاسخ سرور به‌روز کنید و در صورت خطا برگردانید:

useMutation({
  mutationFn: (articleId) => api.like(articleId),
  onMutate: async (articleId) => {
    await queryClient.cancelQueries({ queryKey: ["articles", articleId] });
    const previous = queryClient.getQueryData(["articles", articleId]);
    queryClient.setQueryData(["articles", articleId], (old) => ({ ...old, likes: old.likes + 1, liked: true }));
    return { previous };
  },
  onError: (_err, articleId, context) => {
    queryClient.setQueryData(["articles", articleId], context.previous);
  },
  onSettled: (_data, _err, articleId) => {
    queryClient.invalidateQueries({ queryKey: ["articles", articleId] });
  },
});

صفحه‌بندی بدون پرش

import { keepPreviousData } from "@tanstack/react-query";

function ArticlePages() {
  const [page, setPage] = useState(1);
  const { data, isPlaceholderData } = useQuery({
    queryKey: ["articles", "list", { page }],
    queryFn: () => api.getArticles({ page }),
    placeholderData: keepPreviousData,   // تا صفحه‌ی بعد بیاید، صفحه‌ی قبلی نمایش داده شود
  });

  return (
    <>
      <List items={data?.items ?? []} />
      <button onClick={() => setPage((p) => p - 1)} disabled={page === 1}>Previous</button>
      <button onClick={() => setPage((p) => p + 1)} disabled={isPlaceholderData || !data?.hasMore}>Next</button>
    </>
  );
}

برای فیدهای «بارگذاری بیشتر»، useInfiniteQuery صفحه‌ها و Cursorها را برایتان مدیریت می‌کند.

پیش‌بارگذاری برای ناوبری آنی

function ArticleLink({ article }) {
  const queryClient = useQueryClient();
  return (
    <Link
      to={`/articles/${article.slug}`}
      onMouseEnter={() =>
        queryClient.prefetchQuery({
          queryKey: ["articles", article.slug],
          queryFn: () => api.getArticle(article.slug),
        })
      }
    >
      {article.title}
    </Link>
  );
}

تا کاربر کلیک کند، مقاله اغلب از قبل در کش است.

کوئری‌ها را در هوک‌های سفارشی سازمان دهید

کلیدها و توابع دریافت را در یک جا نگه دارید تا کامپوننت‌ها اعلانی بمانند:

export const articleKeys = {
  all: ["articles"],
  list: (filters) => ["articles", "list", filters],
  detail: (slug) => ["articles", slug],
};

export const useArticles = (filters) =>
  useQuery({ queryKey: articleKeys.list(filters), queryFn: () => api.searchArticles(filters) });

اشتباهات رایج

  1. کپی کردن داده‌ی کوئری در useState — کپی کهنه می‌شود. مستقیم از داده‌ی کوئری استفاده یا از آن محاسبه کنید.
  2. جا انداختن متغیرها در کلید — فیلترهای مختلف کش همدیگر را بازنویسی می‌کنند.
  3. پرتاب نکردن خطا برای خطاهای HTTP — fetch در 404/500 هم resolve می‌شود؛ res.ok را بررسی کنید.
  4. استفاده برای State کلاینت — ورودی فرم و کلیدهای رابط کاربری جایشان در useState است، نه کش کوئری.

جمع‌بندی

TanStack Query داده‌ی سرور را به‌عنوان یک کش با قوانین روشن مدیریت می‌کند: کلیدها داده را شناسایی می‌کنند، staleTime تازگی را کنترل می‌کند، Mutationها داده را تغییر می‌دهند و ابطال همه‌چیز را همگام نگه می‌دارد. به‌روزرسانی خوش‌بینانه، صفحه‌بندی با Placeholder و پیش‌بارگذاری را اضافه کنید تا اپلیکیشن React شما آنی به نظر برسد و کامپوننت‌ها کوچک و اعلانی بمانند.

پرسش‌های متداول

با TanStack Query هنوز به Redux یا Zustand نیاز دارم؟

برای داده‌ی سرور اغلب نه. ممکن است هنوز برای State پیچیده‌ی مخصوص کلاینت یک Store کوچک بخواهید.

تفاوتش با دریافت داده در useEffect چیست؟

همان درخواست شبکه است، اما TanStack Query کش، حذف تکرار، تلاش مجدد، دریافت پس‌زمینه، لغو و DevTools را اضافه می‌کند؛ کدی که در غیر این صورت باید خودتان بنویسید و نگه دارید.

آیا با Axios یا GraphQL کار می‌کند؟

بله. queryFn می‌تواند هر تابعی باشد که یک Promise برمی‌گرداند.