Data Fetching و Server State در اکوسیستم React

در فصل چهاردهم، دریافت داده را قدم‌به‌قدم و دستی ساختیم، و در فصل شانزدهم دیدیم که Server State به ابزار تخصصی نیاز دارد. این درس نقشه آن ابزارها را می‌دهد، نشان می‌دهد هرکدام چه چیزی از دوش ما برمی‌دارند، و می‌گوید در کنار Loaderها و Server Componentها (درس ۲۴-۶) چه جایگاهی دارند.

چک‌لیستی که هر بار دستی می‌نوشتیم

برای هر درخواست داده، تا اینجا باید همه این‌ها را خودمان مدیریت می‌کردیم:

  • سه یا چهار حالت Loading، Error، Empty و Success (درس ۱۴-۲)
  • جلوگیری از Race Condition (درس ۱۴-۴)
  • لغو درخواست قدیمی (درس ۱۴-۵)
  • کش و پرهیز از درخواست تکراری (درس ۱۴-۶)
  • تشخیص کهنه شدن داده و دریافت دوباره (مفهوم Stale در درس ۱۴-۷)

کتابخانه‌های Server State همین چک‌لیست را یک‌بار و به‌صورت آزموده‌شده پیاده‌سازی کرده‌اند. علاوه بر آن، معمولاً چیزهایی را هم اضافه می‌کنند که دستی ساختنش پرزحمت است: ادغام درخواست‌های هم‌زمان با کلید یکسان، تلاش دوباره پس از خطا، دریافت دوباره هنگام برگشتن کاربر به صفحه یا وصل شدن دوباره اینترنت، و ابزار دیباگ.

ابزارهای رایج

  • TanStack Query: پرکاربردترین گزینه عمومی برای Server State. به REST و GraphQL و هر منبعی که Promise برگرداند وابسته نیست.
  • SWR: کتابخانه‌ای سبک‌تر با ایده Stale-While-Revalidate: نمایش فوری داده کش‌شده و هم‌زمان دریافت نسخه تازه.
  • RTK Query: بخشی از Redux Toolkit؛ مناسب پروژه‌هایی که از قبل Redux دارند (درس ۱۶-۵).
  • Apollo Client، urql و Relay: کلاینت‌های مخصوص GraphQL که کش را بر اساس ساختار داده GraphQL مدیریت می‌کنند.

نمونه با TanStack Query نسخه ۵

در درس ۱۴-۸ یک نمونه مفهومی دیدیم که useQuery را با آرگومان‌های جدا صدا می‌زد. آن نحو مربوط به نسخه‌های قدیمی بود؛ نسخه ۵ فقط نحو آبجکتی را می‌پذیرد. ابتدا یک QueryClient می‌سازیم و برنامه را در Provider آن می‌پیچیم (شبیه Context فصل هشتم):

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

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <ProductPage id="42" />
    </QueryClientProvider>
  );
}
jsx
import { useQuery } from "@tanstack/react-query";

function ProductPage({ id }) {
  const { data, isPending, error } = useQuery({
    queryKey: ["product", id],
    queryFn: async () => {
      const res = await fetch(`/api/products/${id}`);
      if (!res.ok) throw new Error("خطا در دریافت داده");
      return res.json();
    },
    staleTime: 60_000,
  });

  if (isPending) return <p>در حال بارگذاری...</p>;
  if (error) return <p>خطا: {error.message}</p>;
  return <h2>{data.name}</h2>;
}

به همه آنچه در فصل ۱۴ دستی نوشتیم نگاه کنید: خبری از useEffect، useState برای loading، پرچم ignore یا AbortController دستی نیست. تابع queryFn فقط می‌گوید داده چگونه گرفته شود. توجه کنید همان نکته فصل ۱۴ هنوز برقرار است: fetch برای خطای HTTP رد نمی‌شود، پس خودمان با res.ok خطا پرتاب می‌کنیم.

queryKey: هویت داده در کش

مهم‌ترین مفهوم این کتابخانه queryKey است. این آرایه، هویت یک ورودی در کش را تعیین می‌کند؛ همان ایده key و Identity در فصل‌های یازدهم و دوازدهم، فقط برای داده به‌جای کامپوننت. اگر id عوض شود، کلید دیگری ساخته می‌شود و ورودی جداگانه‌ای در کش خوانده یا ساخته می‌شود. نتیجه مستقیم آن برای Race Condition این است که پاسخ هر درخواست در ورودی مربوط به کلید خودش ذخیره می‌شود؛ پاسخ دیررس کلید قبلی، روی داده کلید فعلی نمی‌نشیند.

staleTime: تا کی داده تازه است

staleTime مشخص می‌کند داده تا چند میلی‌ثانیه پس از دریافت «تازه» حساب شود. در این بازه، نمایش دوباره کامپوننت درخواست جدیدی نمی‌فرستد. پس از آن، داده «کهنه» است: همچنان فوراً از کش نمایش داده می‌شود، ولی در پس‌زمینه دوباره دریافت می‌شود. مقدار پیش‌فرض صفر است، یعنی داده بلافاصله کهنه حساب می‌شود. این همان الگوی Stale-While-Revalidate درس ۱۴-۶ است. مقدار مناسب به نوع داده بستگی دارد: لیست قیمت لحظه‌ای را کوتاه، و فهرست کشورها را می‌توان طولانی گرفت.

تغییر داده: Mutation و ابطال کش

برای ارسال تغییر به سرور (POST، PUT، DELETE)، همین کتابخانه useMutation دارد. بعد از موفقیت، باید به کش بگوییم کدام داده‌ها دیگر معتبر نیستند تا دوباره دریافت شوند:

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

function AddProduct() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (name) => {
      const res = await fetch("/api/products", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name }),
      });
      if (!res.ok) throw new Error("ذخیره نشد");
      return res.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["products"] });
    },
  });

  return (
    <button
      onClick={() => mutation.mutate("کیبورد")}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? "در حال ذخیره..." : "افزودن"}
    </button>
  );
}

این الگو، همان ایده Actionهای فصل ۲۳ را در سطح کتابخانه دارد: یک عملیات غیرهمزمان با وضعیت isPending. تفاوت در این است که Action یک ابزار خود React است و ابطال کش را نمی‌شناسد، در حالی که invalidateQueries دقیقاً کار کش را انجام می‌دهد. این دو رقیب نیستند؛ می‌توان از Action برای فرم و از کتابخانه برای هماهنگی کش استفاده کرد.

یک اشتباه رایج: کپی کردن داده سرور در useState

jsx
// نادرست: داده کش‌شده دوباره در State کپی شده است
const { data } = useQuery({ queryKey: ["user"], queryFn: fetchUser });
const [user, setUser] = useState(null);

useEffect(() => {
  if (data) setUser(data);
}, [data]);

// درست: همان data را مستقیم استفاده کنید
const { data: user } = useQuery({ queryKey: ["user"], queryFn: fetchUser });

این دقیقاً همان Derived State فصل سوم است: وقتی مقداری از جای دیگری قابل خواندن است، یک State جداگانه برایش ساختن دو منبع حقیقت می‌سازد و باید همیشه آن‌ها را هماهنگ نگه داریم. کش این کتابخانه خودش منبع حقیقت Server State است.

جایگاه این ابزارها کنار Framework و Server Components

همان‌طور که در درس ۲۴-۶ دیدیم، داده اولیه صفحه را می‌توان در Server Component یا Loader خواند، و آنجا نیازی به کتابخانه کلاینتی نیست. این ابزارها همچنان جا دارند وقتی داده با تعامل مداوم مرورگر سروکار دارد: جست‌وجوی هم‌زمان با تایپ، Polling، صفحه‌بندی یا اسکرول بی‌نهایت، و به‌روزرسانی داده پس از Mutation. بسیاری از Frameworkها راه یکپارچه‌سازی دارند، مثلاً دریافت داده روی سرور و تحویل آن به کش کلاینت؛ جزئیات به Framework و نسخه آن بستگی دارد.

آیا به کتابخانه نیاز دارید؟

  • برنامه کوچک با چند درخواست ساده: الگوی دستی فصل ۱۴ یا Loader/Server Component Framework معمولاً کافی است.
  • صفحه‌های زیاد با داده مشترک و تعامل زیاد: کتابخانه Server State معمولاً ارزش هزینه‌اش را دارد، چون چک‌لیست بالا را یک‌جا حل می‌کند.
  • پروژه‌ای که از قبل Redux یا GraphQL دارد: RTK Query یا کلاینت GraphQL همان تجربه را در اکوسیستم موجود می‌دهد.

هزینه‌های اضافه کردن ابزار جدید، همان Trade-offهای درس ۱۶-۴ هستند: یادگیری نحو جدید، وابستگی بلندمدت و پیچیدگی بیشتر.

چرا این درس مثال قابل اجرا ندارد

این کتابخانه‌ها بسته‌های بیرونی هستند و باید با npm نصب شوند، که در ویرایشگر درون‌صفحه‌ای امکان ندارد. کدها را در یک پروژه واقعی (مثلاً ساخته‌شده با Vite) نصب و اجرا کنید.

جمع‌بندی

کتابخانه‌های Server State (مثل TanStack Query، SWR، RTK Query و کلاینت‌های GraphQL) چک‌لیست فصل ۱۴ را یک‌جا حل می‌کنند: حالت‌های Loading و Error، Race Condition، کش، تشخیص کهنه شدن داده و دریافت دوباره. در TanStack Query نسخه ۵، queryKey هویت داده در کش است و staleTime مدت تازه ماندن آن را تعیین می‌کند؛ تغییر داده با useMutation و invalidateQueries انجام می‌شود. کپی کردن داده این کتابخانه در useState اشتباه رایجی است. این ابزارها در کنار Loader و Server Component، برای داده تعاملی و پرتغییر مفیدند. در درس بعدی، به Styling و کتابخانه‌های UI می‌پردازیم.