React Query چیست؟ آموزش کامل TanStack Query در React و TypeScript

در این آموزش TanStack Query یا React Query را از صفر یاد می‌گیرید و با useQuery، useMutation، Cache، Invalidation، Pagination، Infinite Query و Optimistic Update یک برنامه React متصل به API می‌سازید.

Share
React Query چیست؟ آموزش کامل TanStack Query در React و TypeScript

دریافت اطلاعات از API در React در نگاه اول ساده است. یک fetch داخل useEffect قرار می‌دهیم، نتیجه را در State ذخیره می‌کنیم و برای Loading و Error نیز چند State دیگر می‌سازیم.

اما با بزرگ‌تر شدن برنامه، پرسش‌های بیشتری ایجاد می‌شوند:

  • چگونه از ارسال Request تکراری جلوگیری کنیم؟
  • داده دریافت‌شده چه مدت معتبر است؟
  • پس از تغییر یک رکورد، کدام لیست باید دوباره دریافت شود؟
  • چگونه Cache را میان چند Component به اشتراک بگذاریم؟
  • چگونه درخواست قبلی را هنگام تغییر صفحه لغو کنیم؟
  • چگونه Pagination و Infinite Scroll بسازیم؟
  • چگونه وضعیت Pending، Error و Retry را مدیریت کنیم؟
  • چگونه داده را در پس‌زمینه به‌روزرسانی کنیم؟
  • چگونه پس از Mutation رابط کاربری را فوراً تغییر دهیم؟
  • چگونه وضعیت یک Job هوش مصنوعی را Poll کنیم؟

TanStack Query که بسیاری از توسعه‌دهندگان همچنان آن را با نام قبلی React Query می‌شناسند، برای مدیریت همین نوع داده‌ها ساخته شده است.

در این مقاله TanStack Query را با React و TypeScript راه‌اندازی می‌کنیم، مفاهیم Query، Mutation، Query Key، Cache، Invalidation، Pagination و Optimistic Update را می‌آموزیم و یک نمونه عملی متصل به Backend هوش مصنوعی می‌سازیم.

React Query چیست؟

React Query نام اولیه کتابخانه‌ای برای مدیریت Server State در برنامه‌های React بود. این پروژه اکنون بخشی از مجموعه TanStack است و نام رسمی آن TanStack Query محسوب می‌شود.

TanStack Query وظایف رایج مربوط به دریافت و همگام‌سازی داده‌های Server را مدیریت می‌کند:

  • دریافت داده از API
  • Cache کردن Response
  • اشتراک Cache میان Componentها
  • مدیریت Loading و Error
  • Retry خودکار
  • Refetch در پس‌زمینه
  • تشخیص قدیمی شدن داده
  • Mutation
  • Invalidation
  • Pagination
  • Infinite Query
  • Optimistic Update
  • Prefetch
  • لغو Request
  • Polling
  • هماهنگی با SSR و Hydration

TanStack Query خود یک HTTP Client نیست. برای ارسال Request همچنان می‌توانید از fetch، Axios، GraphQL Client، tRPC Client یا هر تابع Async دیگری استفاده کنید.

Server State چیست؟

برای درک کاربرد TanStack Query باید میان Client State و Server State تفاوت قائل شویم.

Client State

داده‌ای است که فقط در رابط کاربری وجود دارد:

  • باز یا بسته بودن منو
  • تب انتخاب‌شده
  • مقدار موقت Input
  • وضعیت Modal
  • تنظیم Theme
  • مرحله فعلی یک فرم

نمونه:

const [isOpen, setIsOpen] =
  useState(false);

Server State

داده‌ای است که منبع اصلی آن خارج از Frontend قرار دارد:

  • فهرست کاربران
  • موجودی محصولات
  • مکالمات
  • پیام‌ها
  • مدل‌های هوش مصنوعی
  • گزارش مصرف API
  • وضعیت Job تولید تصویر
  • اطلاعات کیف پول
  • سفارش‌ها

Server State ویژگی‌های متفاوتی دارد:

  • ممکن است توسط کاربر دیگری تغییر کند.
  • ممکن است در Cache قدیمی شود.
  • دریافت آن Async است.
  • احتمال خطای شبکه وجود دارد.
  • ممکن است نیازمند Retry باشد.
  • چند Component ممکن است هم‌زمان به آن نیاز داشته باشند.
  • باید با وضعیت Server همگام شود.

TanStack Query برای مدیریت Server State طراحی شده است.

آیا React Query جایگزین Redux است؟

به‌صورت کامل خیر. TanStack Query و Redux مسائل متفاوتی را حل می‌کنند.

ابزارکاربرد اصلی
TanStack Queryدریافت، Cache و همگام‌سازی Server State
Redux Toolkitمدیریت State عمومی و Workflowهای Client
Contextاشتراک داده ساده در بخشی از درخت Component
useStateState محلی Component
Zustandمدیریت Client State سبک
React Hook Formمدیریت State فرم

اگر بیشتر Stateهای Redux پروژه شما در واقع داده API هستند، انتقال آن بخش‌ها به TanStack Query می‌تواند کد را ساده‌تر کند. بااین‌حال Theme، State ویرایشگر، Wizard پیچیده یا داده‌های موقت UI همچنان ممکن است به ابزار دیگری نیاز داشته باشند.

تفاوت TanStack Query و fetch چیست؟

fetch فقط Request HTTP را ارسال می‌کند:

const response =
  await fetch("/api/users");

const users =
  await response.json();

اما این موارد را به‌تنهایی مدیریت نمی‌کند:

  • Cache
  • Deduplication
  • Loading State
  • Retry
  • Refetch
  • Invalidation
  • Pagination State
  • Optimistic Update

TanStack Query یک تابع Request مانند fetchUsers را دریافت می‌کند و چرخه عمر داده را مدیریت می‌کند:

const usersQuery = useQuery({
  queryKey: ["users"],
  queryFn: fetchUsers,
});

نسخه مورد استفاده در این آموزش

نمونه‌های این مقاله براساس TanStack Query نسخه ۵ نوشته شده‌اند.

اگر پروژه قدیمی از React Query نسخه ۳ یا TanStack Query نسخه ۴ استفاده می‌کند، بعضی نام‌ها و Syntaxها متفاوت خواهند بود. برای ارتقا به نسخه جدید، راهنمای رسمی مهاجرت به TanStack Query v5 را بررسی کنید.

نصب TanStack Query

در پروژه React:

npm install @tanstack/react-query

با pnpm:

pnpm add @tanstack/react-query

برای Devtools:

npm install --save-dev @tanstack/react-query-devtools

راه‌اندازی QueryClient

در ورودی برنامه، یک QueryClient بسازید و برنامه را داخل QueryClientProvider قرار دهید.

فایل src/main.tsx:

import { StrictMode } from "react";
import {
  createRoot,
} from "react-dom/client";
import {
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";
import {
  ReactQueryDevtools,
} from "@tanstack/react-query-devtools";
import App from "./App";

const queryClient =
  new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 30_000,
        retry: 2,
        refetchOnWindowFocus: true,
      },
    },
  });

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

      <ReactQueryDevtools
        initialIsOpen={false}
      />
    </QueryClientProvider>
  </StrictMode>,
);

QueryClient مرکز مدیریت Query Cache و Mutation Cache است. در یک برنامه SPA معمولاً یک Instance مشترک برای کل برنامه ساخته می‌شود.

در SSR باید مراقب باشید Cache کاربران مختلف با یکدیگر به اشتراک گذاشته نشود و برای هر Request سمت Server، QueryClient مناسب ساخته شود.

ساخت API Client

بهتر است منطق fetch را مستقیماً داخل Component ننویسیم.

فایل src/lib/api.ts:

const API_BASE_URL =
  import.meta.env
    .VITE_API_BASE_URL ??
  "http://localhost:3000";

export class ApiError extends Error {
  constructor(
    message: string,
    public status: number,
    public body?: unknown,
  ) {
    super(message);
    this.name = "ApiError";
  }
}

export async function apiFetch<T>(
  path: string,
  options?: RequestInit,
): Promise<T> {
  const response = await fetch(
    `${API_BASE_URL}${path}`,
    {
      ...options,
      headers: {
        "Content-Type":
          "application/json",
        ...options?.headers,
      },
    },
  );

  const body =
    await response
      .json()
      .catch(() => null);

  if (!response.ok) {
    throw new ApiError(
      "API request failed",
      response.status,
      body,
    );
  }

  return body as T;
}

نکته مهم این است که fetch برای پاسخ‌های HTTP مانند 404 یا 500 به‌صورت خودکار خطا پرتاب نمی‌کند. باید response.ok را بررسی و در صورت ناموفق بودن Request، خطا ایجاد کنید تا TanStack Query وارد وضعیت Error شود.

ساخت اولین Query

Type مدل:

export type Model = {
  id: string;
  name: string;
  category:
    | "text"
    | "coding"
    | "reasoning";
};

تابع دریافت مدل‌ها:

import {
  apiFetch,
} from "../lib/api";

export async function getModels() {
  return apiFetch<Model[]>(
    "/api/models",
  );
}

Component:

import {
  useQuery,
} from "@tanstack/react-query";
import { getModels } from "./api";

export function ModelList() {
  const modelsQuery = useQuery({
    queryKey: ["models"],
    queryFn: getModels,
  });

  if (modelsQuery.isPending) {
    return (
      <p>
        در حال دریافت مدل‌ها...
      </p>
    );
  }

  if (modelsQuery.isError) {
    return (
      <p>
        دریافت مدل‌ها انجام نشد.
      </p>
    );
  }

  return (
    <ul>
      {modelsQuery.data.map(
        (model) => (
          <li key={model.id}>
            {model.name}
          </li>
        ),
      )}
    </ul>
  );
}

TanStack Query نتیجه getModels را در Cache مرتبط با کلید ["models"] قرار می‌دهد.

وضعیت‌های مهم useQuery

خروجی useQuery شامل وضعیت‌ها و توابع مختلفی است:

const {
  data,
  error,
  isPending,
  isError,
  isSuccess,
  isFetching,
  refetch,
  dataUpdatedAt,
} = useQuery({
  queryKey: ["models"],
  queryFn: getModels,
});

isPending

هنوز داده‌ای برای Query وجود ندارد و اولین Request در حال اجرا است.

isFetching

Query در حال دریافت داده است؛ حتی اگر داده قبلی در Cache وجود داشته باشد.

ممکن است:

isPending === false
isFetching === true

این حالت معمولاً هنگام Background Refetch رخ می‌دهد.

isError

آخرین عملیات Query با خطا تمام شده است.

isSuccess

Query با موفقیت داده دارد.

تفاوت isPending و isFetching

برای Loading اولیه:

if (query.isPending) {
  return <FullPageLoader />;
}

برای Refetch پس‌زمینه:

return (
  <div>
    {query.isFetching && (
      <small>
        در حال به‌روزرسانی...
      </small>
    )}

    <ModelTable
      models={query.data}
    />
  </div>
);

اگر برای هر isFetching کل صفحه را با Loader جایگزین کنید، هنگام به‌روزرسانی پس‌زمینه محتوا دائماً ناپدید و ظاهر می‌شود.

Query Key چیست؟

Query Key شناسه داده داخل Cache است.

["models"]

برای یک مدل مشخص:

["models", modelId]

برای فهرست فیلترشده:

[
  "models",
  {
    category: "coding",
    page: 1,
  },
]

Query Key باید تمام متغیرهایی را که روی نتیجه Query اثر می‌گذارند شامل شود.

اشتباه:

useQuery({
  queryKey: ["models"],
  queryFn: () =>
    getModels(category),
});

اگر category تغییر کند، Query Key ثابت می‌ماند و Cache فیلترها با یکدیگر مخلوط می‌شود.

درست:

useQuery({
  queryKey: [
    "models",
    {
      category,
    },
  ],
  queryFn: () =>
    getModels(category),
});

ساخت Query Key Factory

برای جلوگیری از پراکندگی کلیدها:

export const modelKeys = {
  all: ["models"] as const,

  lists: () =>
    [...modelKeys.all, "list"]
      as const,

  list: (
    filters: {
      category?: string;
      page?: number;
    },
  ) =>
    [
      ...modelKeys.lists(),
      filters,
    ] as const,

  details: () =>
    [
      ...modelKeys.all,
      "detail",
    ] as const,

  detail: (id: string) =>
    [
      ...modelKeys.details(),
      id,
    ] as const,
};

استفاده:

useQuery({
  queryKey:
    modelKeys.detail(modelId),
  queryFn: () =>
    getModel(modelId),
});

این الگو Invalidation و Refactoring را ساده‌تر می‌کند.

Cache چگونه کار می‌کند؟

وقتی Component دارای یک Query Mount می‌شود:

  1. TanStack Query کلید را در Cache جست‌وجو می‌کند.
  2. اگر داده وجود داشته باشد، می‌تواند آن را فوراً نمایش دهد.
  3. براساس Fresh یا Stale بودن داده تصمیم می‌گیرد Refetch انجام دهد.
  4. چند Component با Query Key یکسان از یک Cache مشترک استفاده می‌کنند.
  5. وقتی Query بدون Observer شود، مدتی در Cache باقی می‌ماند.
  6. پس از پایان gcTime، Query غیرفعال می‌تواند از Cache حذف شود.

staleTime چیست؟

staleTime مشخص می‌کند داده برای چه مدت Fresh محسوب شود.

useQuery({
  queryKey: ["models"],
  queryFn: getModels,
  staleTime: 60_000,
});

در این مثال داده یک دقیقه Fresh است.

در این مدت TanStack Query معمولاً برای Triggerهای عادی مانند Mount مجدد، Request جدید غیرضروری ارسال نمی‌کند؛ مگر اینکه Query به‌صورت دستی Invalid شود یا Refetch صریح انجام شود.

نمونه‌ها:

staleTime: 0

داده بلافاصله Stale محسوب می‌شود.

staleTime: 5 * 60 * 1000

داده پنج دقیقه Fresh می‌ماند.

staleTime: Infinity

داده تا زمان Invalidation دستی Fresh در نظر گرفته می‌شود.

طبق راهنمای Important Defaults، Queryها در حالت پیش‌فرض داده Cache‌شده را Stale در نظر می‌گیرند؛ بنابراین شناخت staleTime برای جلوگیری از Refetchهای غیرمنتظره اهمیت دارد.

gcTime چیست؟

gcTime تعیین می‌کند Query غیرفعال چه مدت در Cache باقی بماند.

useQuery({
  queryKey: ["models"],
  queryFn: getModels,
  gcTime: 10 * 60 * 1000,
});

اگر هیچ Component فعالی از Query استفاده نکند، داده تا ده دقیقه قابل نگهداری است و سپس می‌تواند Garbage Collect شود.

تفاوت مهم:

  • staleTime درباره تازگی داده است.
  • gcTime درباره مدت نگهداری Query غیرفعال در Cache است.

Fresh بودن به معنی ماندگاری همیشگی در Cache نیست و Stale بودن نیز به معنی حذف فوری داده نیست.

تنظیمات پیش‌فرض مهم TanStack Query

پیش‌فرض‌ها ممکن است برای تازه‌کاران غیرمنتظره باشند:

  • داده Query معمولاً بلافاصله Stale محسوب می‌شود.
  • Queryهای Stale ممکن است هنگام Mount مجدد Refetch شوند.
  • Queryهای Stale می‌توانند هنگام Focus شدن دوباره پنجره Refetch شوند.
  • Queryهای ناموفق در Client چند بار Retry می‌شوند.
  • داده غیرفعال مدتی در Cache باقی می‌ماند.
  • Objectهای JSON-Compatible تا حد امکان Structural Sharing دارند.

این رفتارها اشتباه نیستند؛ هدف آن‌ها تازه نگه داشتن Server State است. تنظیمات را براساس نوع داده تغییر دهید، نه اینکه همه پیش‌فرض‌ها را بدون تحلیل غیرفعال کنید.

Query دارای پارامتر

تابع:

export async function getModel(
  modelId: string,
  signal?: AbortSignal,
) {
  return apiFetch<Model>(
    `/api/models/${modelId}`,
    {
      signal,
    },
  );
}

Hook:

function ModelDetails({
  modelId,
}: {
  modelId: string;
}) {
  const query = useQuery({
    queryKey:
      modelKeys.detail(modelId),
    queryFn: ({ signal }) =>
      getModel(
        modelId,
        signal,
      ),
  });

  if (query.isPending) {
    return <p>در حال دریافت...</p>;
  }

  if (query.isError) {
    return <p>خطا در دریافت مدل</p>;
  }

  return (
    <article>
      <h2>
        {query.data.name}
      </h2>
      <p>
        {query.data.category}
      </p>
    </article>
  );
}

لغو Request با AbortSignal

TanStack Query یک AbortSignal در اختیار queryFn قرار می‌دهد:

useQuery({
  queryKey: ["models", search],
  queryFn: ({ signal }) =>
    fetch(
      `/api/models?search=${encodeURIComponent(
        search,
      )}`,
      {
        signal,
      },
    ).then((response) => {
      if (!response.ok) {
        throw new Error(
          "Request failed",
        );
      }

      return response.json();
    }),
});

اگر Query قدیمی یا غیرضروری شود، Signal می‌تواند Abort شود و fetch درخواست را لغو کند. جزئیات در مستندات Query Cancellation آمده است.

این قابلیت برای Search، تغییر سریع Filter و جابه‌جایی صفحات مفید است.

Query وابسته با enabled

فرض کنید ابتدا باید User دریافت شود و سپس مکالمات او بارگذاری شوند:

const userQuery = useQuery({
  queryKey: ["user", "me"],
  queryFn: getCurrentUser,
});

const conversationsQuery =
  useQuery({
    queryKey: [
      "conversations",
      userQuery.data?.id,
    ],
    queryFn: () =>
      getConversations(
        userQuery.data!.id,
      ),
    enabled:
      Boolean(
        userQuery.data?.id,
      ),
  });

تا زمانی که userId وجود نداشته باشد، Query دوم اجرا نمی‌شود.

از enabled برای وابستگی واقعی استفاده کنید. اگر Queryها مستقل هستند، بهتر است آن‌ها را موازی اجرا کنید.

اجرای Queryهای موازی

const modelsQuery = useQuery({
  queryKey: ["models"],
  queryFn: getModels,
});

const profileQuery = useQuery({
  queryKey: ["profile"],
  queryFn: getProfile,
});

React هر دو Hook را در یک Render اجرا می‌کند و Requestها می‌توانند موازی شروع شوند.

برای تعداد پویا می‌توان از useQueries استفاده کرد:

const modelQueries = useQueries({
  queries: modelIds.map(
    (modelId) => ({
      queryKey:
        modelKeys.detail(modelId),
      queryFn: ({ signal }) =>
        getModel(
          modelId,
          signal,
        ),
    }),
  ),
});

تبدیل یا انتخاب بخشی از داده با select

const modelNamesQuery = useQuery({
  queryKey: ["models"],
  queryFn: getModels,
  select: (models) =>
    models.map(
      (model) => model.name,
    ),
});

Type data در این Component برابر string[] خواهد بود.

select برای انتخاب یا تبدیل نمایشی داده مفید است، اما تابع‌های سنگین و ناپایدار می‌توانند محاسبات اضافی ایجاد کنند. برای پردازش پیچیده از تابع ثابت یا Memoization استفاده کنید.

initialData و placeholderData

initialData

داده اولیه معتبر را داخل Cache قرار می‌دهد:

useQuery({
  queryKey: ["models"],
  queryFn: getModels,
  initialData: initialModels,
});

placeholderData

تا زمان رسیدن داده اصلی، یک مقدار موقت برای همان Observer نمایش می‌دهد:

useQuery({
  queryKey: ["model", modelId],
  queryFn: () =>
    getModel(modelId),
  placeholderData: {
    id: modelId,
    name: "در حال بارگذاری",
    category: "text",
  },
});

Placeholder لزوماً داده معتبر Server نیست و نباید با داده واقعی اشتباه گرفته شود.

ساخت Mutation

Query معمولاً برای خواندن داده است. Mutation برای عملیات دارای Side Effect استفاده می‌شود:

  • ساخت رکورد
  • ویرایش
  • حذف
  • ارسال فرم
  • ایجاد مکالمه
  • ارسال پیام
  • شروع Job هوش مصنوعی

طبق مستندات Mutations، Hook اصلی برای این عملیات useMutation است.

تابع API:

type CreateConversationInput = {
  title: string;
  modelId: string;
};

type Conversation = {
  id: string;
  title: string;
  modelId: string;
  createdAt: string;
};

export async function createConversation(
  input: CreateConversationInput,
) {
  return apiFetch<Conversation>(
    "/api/conversations",
    {
      method: "POST",
      body: JSON.stringify(input),
    },
  );
}

Component:

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

function CreateConversation() {
  const mutation = useMutation({
    mutationFn:
      createConversation,
  });

  function handleCreate() {
    mutation.mutate({
      title: "مکالمه جدید",
      modelId:
        "YOUR_MODEL_ID",
    });
  }

  return (
    <div>
      <button
        onClick={handleCreate}
        disabled={
          mutation.isPending
        }
      >
        {mutation.isPending
          ? "در حال ساخت..."
          : "ساخت مکالمه"}
      </button>

      {mutation.isError && (
        <p>
          ساخت مکالمه انجام نشد.
        </p>
      )}

      {mutation.isSuccess && (
        <p>
          مکالمه ساخته شد.
        </p>
      )}
    </div>
  );
}

mutate و mutateAsync

mutate

Callbackها را از طریق Optionها مدیریت می‌کند:

mutation.mutate(input, {
  onSuccess(data) {
    console.log(data);
  },
});

mutateAsync

Promise برمی‌گرداند:

try {
  const result =
    await mutation.mutateAsync(
      input,
    );

  navigate(
    `/conversations/${result.id}`,
  );
} catch (error) {
  console.error(error);
}

اگر از mutateAsync استفاده می‌کنید، خطا را مدیریت کنید.

Query Invalidation

پس از ساخت یک مکالمه، Cache فهرست مکالمات ممکن است قدیمی شود. با Invalidation به TanStack Query می‌گوییم این داده دیگر معتبر نیست.

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

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

  const mutation = useMutation({
    mutationFn:
      createConversation,

    onSuccess: async () => {
      await queryClient
        .invalidateQueries({
          queryKey: [
            "conversations",
          ],
        });
    },
  });

  // ...
}

invalidateQueries Queryهای مطابق را Stale می‌کند و Queryهای فعال معمولاً دوباره دریافت می‌شوند.

براساس مستندات Query Invalidation، این روش به‌جای مدیریت دستی Normalized Cache، Queryهای مرتبط را هدف می‌گیرد و برای Refetch علامت‌گذاری می‌کند.

Invalidation دقیق یا کلی

تمام Queryهای مدل:

queryClient.invalidateQueries({
  queryKey: ["models"],
});

فقط جزئیات یک مدل:

queryClient.invalidateQueries({
  queryKey:
    modelKeys.detail(modelId),
});

فقط Match دقیق:

queryClient.invalidateQueries({
  queryKey: ["models"],
  exact: true,
});

Invalidation بسیار گسترده می‌تواند Requestهای اضافی ایجاد کند. Query Key Factory برای هدف‌گیری دقیق مفید است.

به‌روزرسانی مستقیم Cache

اگر Mutation نسخه کامل و جدید رکورد را برمی‌گرداند، می‌توان Cache همان رکورد را مستقیم به‌روزرسانی کرد:

const mutation = useMutation({
  mutationFn: updateModel,

  onSuccess: (updatedModel) => {
    queryClient.setQueryData(
      modelKeys.detail(
        updatedModel.id,
      ),
      updatedModel,
    );
  },
});

برای به‌روزرسانی لیست:

queryClient.setQueryData<Model[]>(
  modelKeys.lists(),
  (oldModels) => {
    if (!oldModels) {
      return oldModels;
    }

    return oldModels.map(
      (model) =>
        model.id ===
        updatedModel.id
          ? updatedModel
          : model,
    );
  },
);

داده Cache را Mutation نکنید:

oldModels[0].name =
  "New Name";

Object یا Array جدید برگردانید.

Optimistic Update چیست؟

در Optimistic Update رابط کاربری پیش از دریافت پاسخ Server به‌روزرسانی می‌شود. اگر Request شکست بخورد، تغییر Rollback خواهد شد.

مثال تغییر عنوان مکالمه:

const mutation = useMutation({
  mutationFn:
    updateConversationTitle,

  onMutate: async (
    updatedConversation,
  ) => {
    await queryClient
      .cancelQueries({
        queryKey: [
          "conversations",
        ],
      });

    const previous =
      queryClient.getQueryData<
        Conversation[]
      >(["conversations"]);

    queryClient.setQueryData<
      Conversation[]
    >(
      ["conversations"],
      (old) =>
        old?.map(
          (conversation) =>
            conversation.id ===
            updatedConversation.id
              ? {
                  ...conversation,
                  title:
                    updatedConversation.title,
                }
              : conversation,
        ) ?? [],
    );

    return {
      previous,
    };
  },

  onError: (
    _error,
    _variables,
    context,
  ) => {
    if (context?.previous) {
      queryClient.setQueryData(
        ["conversations"],
        context.previous,
      );
    }
  },

  onSettled: async () => {
    await queryClient
      .invalidateQueries({
        queryKey: [
          "conversations",
        ],
      });
  },
});

گردش عملیات:

  1. Queryهای فعال لغو می‌شوند.
  2. Cache قبلی ذخیره می‌شود.
  3. UI خوش‌بینانه تغییر می‌کند.
  4. اگر Request شکست خورد، Cache قبلی برمی‌گردد.
  5. در پایان داده Server دوباره دریافت می‌شود.

Optimistic Update برای عملیات سریع و قابل‌برگشت مناسب است. برای عملیات حساس یا غیرقابل‌برگشت بهتر است ابتدا موفقیت Server تأیید شود.

راهنمای کامل در مستندات Optimistic Updates قرار دارد.

Pagination با TanStack Query

تابع API:

type PaginatedModels = {
  items: Model[];
  page: number;
  totalPages: number;
  total: number;
};

export async function getModelsPage(
  page: number,
  signal?: AbortSignal,
) {
  return apiFetch<PaginatedModels>(
    `/api/models?page=${page}&limit=20`,
    {
      signal,
    },
  );
}

Component:

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

export function PaginatedModels() {
  const [page, setPage] =
    useState(1);

  const query = useQuery({
    queryKey: [
      "models",
      "page",
      page,
    ],
    queryFn: ({ signal }) =>
      getModelsPage(
        page,
        signal,
      ),
    placeholderData:
      keepPreviousData,
  });

  if (
    query.isPending &&
    !query.data
  ) {
    return <p>در حال دریافت...</p>;
  }

  if (query.isError) {
    return <p>خطا در دریافت داده</p>;
  }

  return (
    <section>
      {query.isPlaceholderData && (
        <small>
          در حال دریافت صفحه جدید...
        </small>
      )}

      <ul>
        {query.data?.items.map(
          (model) => (
            <li key={model.id}>
              {model.name}
            </li>
          ),
        )}
      </ul>

      <button
        onClick={() =>
          setPage((value) =>
            Math.max(
              1,
              value - 1,
            ),
          )
        }
        disabled={page === 1}
      >
        صفحه قبل
      </button>

      <span>
        صفحه {page} از{" "}
        {query.data?.totalPages}
      </span>

      <button
        onClick={() =>
          setPage(
            (value) =>
              value + 1,
          )
        }
        disabled={
          query.isPlaceholderData ||
          page >=
            (query.data
              ?.totalPages ?? 1)
        }
      >
        صفحه بعد
      </button>
    </section>
  );
}

keepPreviousData باعث می‌شود هنگام تغییر صفحه، داده صفحه قبلی تا رسیدن صفحه جدید نمایش داده شود و UI خالی نشود.

Prefetch صفحه بعد

وقتی صفحه فعلی دریافت شد، می‌توان صفحه بعد را از پیش دریافت کرد:

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

const queryClient =
  useQueryClient();

useEffect(() => {
  const totalPages =
    query.data?.totalPages;

  if (
    totalPages &&
    page < totalPages
  ) {
    queryClient.prefetchQuery({
      queryKey: [
        "models",
        "page",
        page + 1,
      ],
      queryFn: ({ signal }) =>
        getModelsPage(
          page + 1,
          signal,
        ),
    });
  }
}, [
  page,
  query.data?.totalPages,
  queryClient,
]);

اگر کاربر وارد صفحه بعد شود، داده احتمالاً از قبل در Cache قرار دارد.

Infinite Query

برای Infinite Scroll یا دکمه «نمایش بیشتر» از useInfiniteQuery استفاده می‌شود.

پاسخ API:

type ConversationPage = {
  items: Conversation[];
  nextCursor:
    | string
    | null;
};

تابع:

export async function getConversations(
  cursor: string | null,
  signal?: AbortSignal,
) {
  const params =
    new URLSearchParams();

  params.set("limit", "20");

  if (cursor) {
    params.set("cursor", cursor);
  }

  return apiFetch<ConversationPage>(
    `/api/conversations?${params}`,
    {
      signal,
    },
  );
}

Hook:

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

const conversationsQuery =
  useInfiniteQuery({
    queryKey: [
      "conversations",
      "infinite",
    ],

    queryFn: ({
      pageParam,
      signal,
    }) =>
      getConversations(
        pageParam,
        signal,
      ),

    initialPageParam:
      null as string | null,

    getNextPageParam:
      (lastPage) =>
        lastPage.nextCursor,
  });

نمایش داده‌ها:

const conversations =
  conversationsQuery.data
    ?.pages.flatMap(
      (page) => page.items,
    ) ?? [];

return (
  <div>
    {conversations.map(
      (conversation) => (
        <article
          key={conversation.id}
        >
          {conversation.title}
        </article>
      ),
    )}

    <button
      onClick={() =>
        conversationsQuery
          .fetchNextPage()
      }
      disabled={
        !conversationsQuery
          .hasNextPage ||
        conversationsQuery
          .isFetchingNextPage
      }
    >
      {conversationsQuery
        .isFetchingNextPage
        ? "در حال دریافت..."
        : conversationsQuery
            .hasNextPage
          ? "نمایش بیشتر"
          : "پایان فهرست"}
    </button>
  </div>
);

در Infinite Query هر صفحه داخل pages نگهداری می‌شود. برای فهرست‌های بزرگ می‌توان maxPages را براساس تجربه کاربری و مصرف حافظه تنظیم کرد.

رفتار Refetch صفحات و Cursorها در مستندات Infinite Queries توضیح داده شده است.

Polling وضعیت Job هوش مصنوعی

تولید تصویر، ویدئو یا صوت ممکن است Async باشد. Backend ابتدا یک Job می‌سازد:

{
  "id": "job_123",
  "status": "pending"
}

و سپس Frontend وضعیت را Poll می‌کند.

Type:

type GenerationJob = {
  id: string;
  status:
    | "pending"
    | "processing"
    | "completed"
    | "failed";
  progress: number;
  outputUrl?: string;
  error?: string;
};

Query:

const jobQuery = useQuery({
  queryKey: [
    "generation-job",
    jobId,
  ],

  queryFn: ({ signal }) =>
    apiFetch<GenerationJob>(
      `/api/jobs/${jobId}`,
      {
        signal,
      },
    ),

  enabled: Boolean(jobId),

  refetchInterval: (query) => {
    const status =
      query.state.data?.status;

    if (
      status === "completed" ||
      status === "failed"
    ) {
      return false;
    }

    return 2_000;
  },
});

در این مثال:

  • هر دو ثانیه وضعیت Job بررسی می‌شود.
  • پس از completed یا failed، Polling متوقف می‌شود.
  • اگر jobId وجود نداشته باشد، Query اجرا نمی‌شود.

فاصله Polling باید با نوع سرویس، بار Server و تجربه کاربری متناسب باشد.

ساخت Mutation برای شروع Job

type StartGenerationInput = {
  prompt: string;
  modelId: string;
};

export async function startGeneration(
  input: StartGenerationInput,
) {
  return apiFetch<GenerationJob>(
    "/api/jobs",
    {
      method: "POST",
      body: JSON.stringify(input),
    },
  );
}

Component:

function GenerationForm() {
  const [jobId, setJobId] =
    useState<string | null>(
      null,
    );

  const startMutation =
    useMutation({
      mutationFn:
        startGeneration,

      onSuccess: (job) => {
        setJobId(job.id);
      },
    });

  // jobQuery براساس jobId
}

این معماری برای عملیات طولانی بهتر از باز نگه داشتن یک Request HTTP معمولی برای چند دقیقه است.

پروژه عملی: چت هوش مصنوعی با TanStack Query

در یک چت ساده غیرجریانی، ارسال پیام یک Mutation است و تاریخچه مکالمه یک Query.

Typeها

type ChatMessage = {
  id: string;
  role: "user" | "assistant";
  content: string;
  createdAt: string;
};

type SendMessageInput = {
  conversationId: string;
  modelId: string;
  content: string;
};

type SendMessageResult = {
  userMessage: ChatMessage;
  assistantMessage: ChatMessage;
};

دریافت تاریخچه

export async function getMessages(
  conversationId: string,
  signal?: AbortSignal,
) {
  return apiFetch<ChatMessage[]>(
    `/api/conversations/${conversationId}/messages`,
    {
      signal,
    },
  );
}

Hook:

const messagesQuery = useQuery({
  queryKey: [
    "conversations",
    conversationId,
    "messages",
  ],

  queryFn: ({ signal }) =>
    getMessages(
      conversationId,
      signal,
    ),

  enabled:
    Boolean(conversationId),
});

ارسال پیام

export async function sendMessage(
  input: SendMessageInput,
) {
  return apiFetch<SendMessageResult>(
    `/api/conversations/${input.conversationId}/messages`,
    {
      method: "POST",
      body: JSON.stringify({
        modelId: input.modelId,
        content: input.content,
      }),
    },
  );
}

Mutation و به‌روزرسانی Cache

const queryClient =
  useQueryClient();

const messageKey = [
  "conversations",
  conversationId,
  "messages",
] as const;

const sendMutation =
  useMutation({
    mutationFn: sendMessage,

    onSuccess: (result) => {
      queryClient
        .setQueryData<
          ChatMessage[]
        >(
          messageKey,
          (oldMessages = []) => [
            ...oldMessages,
            result.userMessage,
            result.assistantMessage,
          ],
        );
    },
  });

ارسال:

sendMutation.mutate({
  conversationId,
  modelId:
    "YOUR_MODEL_ID",
  content: prompt,
});

اتصال Backend به درواره

Frontend نباید مستقیماً API Key درواره را داشته باشد. مسیر صحیح:

React
  ↓
Backend برنامه
  ↓
API درواره
  ↓
مدل هوش مصنوعی

نمونه ساده Backend:

app.post(
  "/api/conversations/:id/messages",
  async (req, res) => {
    const {
      content,
      modelId,
    } = req.body;

    const response = await fetch(
      "https://api.darvareh.ir/v1/chat/completions",
      {
        method: "POST",
        headers: {
          Authorization:
            `Bearer ${process.env.DARVAREH_API_KEY}`,
          "Content-Type":
            "application/json",
        },
        body: JSON.stringify({
          model: modelId,
          messages: [
            {
              role: "user",
              content,
            },
          ],
        }),
      },
    );

    if (!response.ok) {
      return res.status(502).json({
        error:
          "AI_REQUEST_FAILED",
      });
    }

    const completion =
      await response.json();

    const answer =
      completion.choices?.[0]
        ?.message?.content;

    if (
      typeof answer !== "string"
    ) {
      return res.status(502).json({
        error:
          "INVALID_AI_RESPONSE",
      });
    }

    return res.json({
      userMessage: {
        id: crypto.randomUUID(),
        role: "user",
        content,
        createdAt:
          new Date().toISOString(),
      },
      assistantMessage: {
        id: crypto.randomUUID(),
        role: "assistant",
        content: answer,
        createdAt:
          new Date().toISOString(),
      },
    });
  },
);

مقادیر محیطی:

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

کلید API را داخل React، فایل JavaScript عمومی، Local Storage یا مخزن Git قرار ندهید.

برای دریافت API Key می‌توانید در درواره ثبت‌نام کنید. فهرست شناسه مدل‌ها، قابلیت‌ها و قیمت‌های به‌روز در صفحه مدل‌های درواره قرار دارد.

آیا TanStack Query برای Streaming مناسب است؟

TanStack Query برای Promiseهایی که در نهایت یک نتیجه مشخص تولید می‌کنند طراحی بسیار خوبی دارد. Streaming Tokenهای پاسخ هوش مصنوعی رفتار متفاوتی دارد؛ زیرا داده به‌تدریج و در چند Chunk دریافت می‌شود.

برای Streaming می‌توان از این روش‌ها استفاده کرد:

  • fetch و خواندن ReadableStream
  • Server-Sent Events
  • WebSocket
  • کتابخانه اختصاصی Streaming
  • ذخیره نتیجه نهایی در Query Cache پس از اتمام Stream

الگوی پیشنهادی:

  1. Streaming را با ابزار مناسب مدیریت کنید.
  2. متن موقت در حال تولید را در State محلی نگه دارید.
  3. پس از پایان پاسخ، پیام نهایی را در Backend ذخیره کنید.
  4. Cache پیام‌ها را با setQueryData به‌روزرسانی یا Invalid کنید.

قرار دادن هر Token به‌عنوان یک Query مجزا طراحی مناسبی نیست.

Retry در TanStack Query

تنظیم Retry:

useQuery({
  queryKey: ["models"],
  queryFn: getModels,
  retry: 2,
});

Retry شرطی:

useQuery({
  queryKey: ["models"],
  queryFn: getModels,

  retry: (
    failureCount,
    error,
  ) => {
    if (
      error instanceof
        ApiError &&
      error.status >= 400 &&
      error.status < 500 &&
      error.status !== 429
    ) {
      return false;
    }

    return failureCount < 2;
  },
});

همه خطاها نباید Retry شوند:

  • 400 معمولاً ورودی نامعتبر است.
  • 401 نیازمند ورود یا تمدید Session است.
  • 403 مشکل مجوز است.
  • 404 معمولاً با Retry حل نمی‌شود.
  • 429 ممکن است با تأخیر و سیاست مناسب دوباره امتحان شود.
  • 500، 502 و خطای شبکه می‌توانند موقت باشند.

Retry کورکورانه ممکن است بار Server و هزینه API هوش مصنوعی را افزایش دهد. Mutationهای دارای Side Effect نیز باید با احتیاط Retry شوند.

Refetch دستی

const query = useQuery({
  queryKey: ["models"],
  queryFn: getModels,
});

<button
  onClick={() =>
    query.refetch()
  }
>
  به‌روزرسانی
</button>

اگر هدف اعلام قدیمی شدن داده در سطح برنامه است، invalidateQueries معمولاً مناسب‌تر از دسترسی مستقیم به refetch یک Component است.

Refetch هنگام Focus

useQuery({
  queryKey: ["wallet"],
  queryFn: getWallet,
  refetchOnWindowFocus: true,
});

این رفتار برای داده‌هایی مانند موجودی کیف پول مفید است، اما برای داده‌های تقریباً ثابت می‌توان آن را غیرفعال یا staleTime را افزایش داد.

useQuery({
  queryKey: ["static-config"],
  queryFn: getConfig,
  staleTime:
    30 * 60 * 1000,
  refetchOnWindowFocus: false,
});

مدیریت خطای عمومی

TanStack Query v5 برای Queryها Callbackهای onError در سطح useQuery را مانند الگوهای قدیمی توصیه نمی‌کند. برای خطای عمومی می‌توان Mutation Cache، Query Cache یا Error Boundary متناسب تعریف کرد.

نمونه Query Cache:

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

const queryClient =
  new QueryClient({
    queryCache:
      new QueryCache({
        onError: (error) => {
          console.error(
            "Query failed",
            error,
          );
        },
      }),
  });

نمایش Toast عمومی نباید برای هر Observer تکرار شود. همچنین پیام فنی خام همیشه برای کاربر مناسب نیست.

Prefetch

پیش از ورود کاربر به صفحه جزئیات:

async function handleModelHover(
  modelId: string,
) {
  await queryClient
    .prefetchQuery({
      queryKey:
        modelKeys.detail(
          modelId,
        ),
      queryFn: ({ signal }) =>
        getModel(
          modelId,
          signal,
        ),
      staleTime: 60_000,
    });
}

در Link:

<Link
  to={`/models/${model.id}`}
  onMouseEnter={() =>
    handleModelHover(
      model.id,
    )
  }
>
  {model.name}
</Link>

Prefetch بیش از حد می‌تواند Request و مصرف پهنای باند را افزایش دهد. فقط مسیرهایی را Prefetch کنید که احتمال استفاده آن‌ها بالاست.

Query Options مشترک

برای استفاده مجدد از تنظیم Query:

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

export function modelOptions(
  modelId: string,
) {
  return queryOptions({
    queryKey:
      modelKeys.detail(modelId),

    queryFn: ({ signal }) =>
      getModel(
        modelId,
        signal,
      ),

    staleTime: 60_000,
  });
}

در Component:

const query = useQuery(
  modelOptions(modelId),
);

در Prefetch:

queryClient.prefetchQuery(
  modelOptions(modelId),
);

این روش Query Key، Query Function و تنظیمات را هماهنگ نگه می‌دارد.

SSR و Hydration

در Frameworkهایی مانند Next.js می‌توان داده را روی Server Prefetch، Dehydrate و در Browser Hydrate کرد.

نکات مهم:

  • برای هر Request سمت Server یک QueryClient جداگانه بسازید.
  • Cache کاربران مختلف نباید مشترک باشد.
  • Query Key باید در Server و Client یکسان باشد.
  • staleTime مناسب مانع Refetch فوری پس از Hydration می‌شود.
  • داده حساس نباید ناخواسته داخل HTML سریال شود.
  • QueryClient مرورگر نباید در هر Render دوباره ساخته شود.

پیاده‌سازی دقیق به Router و معماری Framework بستگی دارد و باید با مستندات نسخه فعلی TanStack Query و Framework هماهنگ شود.

تست Componentهای دارای Query

یک Wrapper برای تست بسازید:

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

export function createTestWrapper() {
  const queryClient =
    new QueryClient({
      defaultOptions: {
        queries: {
          retry: false,
        },
        mutations: {
          retry: false,
        },
      },
    });

  return function Wrapper({
    children,
  }: {
    children: ReactNode;
  }) {
    return (
      <QueryClientProvider
        client={queryClient}
      >
        {children}
      </QueryClientProvider>
    );
  };
}

در تست‌ها Retry را غیرفعال کنید تا شکست‌ها باعث تأخیر غیرضروری نشوند.

برای هر تست QueryClient جدید بسازید تا Cache یک تست روی تست دیگر اثر نگذارد.

TanStack Query Devtools

Devtools اطلاعات مفیدی نمایش می‌دهد:

  • Query Keyها
  • Fresh یا Stale بودن داده
  • وضعیت Fetch
  • تعداد Observerها
  • محتوای Cache
  • زمان آخرین به‌روزرسانی
  • Mutationهای فعال

استفاده:

<ReactQueryDevtools
  initialIsOpen={false}
/>

Devtools برای Development مناسب است. نحوه وارد کردن آن را طوری تنظیم کنید که Bundle Production را بی‌دلیل افزایش ندهد.

اشتباهات رایج در React Query

استفاده از Query Key ثابت برای Filterهای مختلف

اشتباه:

queryKey: ["models"]

درحالی‌که نتیجه به category وابسته است.

درست:

queryKey: [
  "models",
  {
    category,
  },
]

نداشتن خطا در Query Function

اشتباه:

return fetch(url).then(
  (response) =>
    response.json(),
);

درست:

const response =
  await fetch(url);

if (!response.ok) {
  throw new Error(
    "Request failed",
  );
}

return response.json();

ساخت QueryClient داخل Component

اشتباه:

function App() {
  const queryClient =
    new QueryClient();

  // ...
}

در هر Render ممکن است Client جدید ساخته و Cache از دست برود.

قرار دادن Server State در useState

اگر داده از API می‌آید و باید Cache و Refetch شود، بهتر است TanStack Query مالک آن باشد.

کپی کردن data به State محلی

useEffect(() => {
  setModels(query.data);
}, [query.data]);

این کار دو منبع حقیقت ایجاد می‌کند. اگر فقط نیاز به نمایش دارید، مستقیماً از query.data استفاده کنید.

استفاده از useQuery برای POST دارای Side Effect

برای عملیات تغییر‌دهنده Server از useMutation استفاده کنید.

Invalidation بیش از حد گسترده

پس از هر Mutation تمام Queryها را Invalid نکنید. Queryهای واقعاً مرتبط را هدف قرار دهید.

استفاده نادرست از staleTime

staleTime مدت Fresh بودن داده است، نه مدت نگهداری آن در Cache.

Retry عملیات هزینه‌دار بدون کنترل

Retry خودکار یک درخواست تولید تصویر یا پاسخ هوش مصنوعی ممکن است هزینه را چند برابر کند. Idempotency و سیاست Retry باید در Backend نیز در نظر گرفته شوند.

نمایش Loader کامل هنگام Background Fetch

برای Refetch پس‌زمینه از نشانگر کوچک استفاده کنید و داده قبلی را نگه دارید.

استفاده از Query برای Streaming Token

Streaming را با ابزار مناسب مدیریت و نتیجه نهایی را با Cache همگام کنید.

قرار دادن API Key در Frontend

TanStack Query ابزار امن نگهداری Secret نیست. درخواست هوش مصنوعی باید از Backend ارسال شود.

چک‌لیست TanStack Query برای Production

  • یک QueryClient پایدار برای برنامه ساخته شده است.
  • Query Keyها تمام پارامترهای مؤثر را شامل می‌شوند.
  • Query Key Factory برای Domainهای بزرگ وجود دارد.
  • Query Function در پاسخ ناموفق خطا ایجاد می‌کند.
  • Requestهای قابل لغو از AbortSignal استفاده می‌کنند.
  • staleTime براساس ماهیت داده تنظیم شده است.
  • تفاوت staleTime و gcTime درک شده است.
  • Mutationهای موفق Queryهای مرتبط را Invalid یا Update می‌کنند.
  • Optimistic Update دارای Rollback است.
  • لیست‌های بزرگ Pagination یا Infinite Query دارند.
  • Polling پس از پایان Job متوقف می‌شود.
  • Retry برای خطاهای دائمی غیرفعال است.
  • Mutationهای هزینه‌دار بدون تحلیل Retry نمی‌شوند.
  • API Key فقط در Backend قرار دارد.
  • QueryClient کاربران در SSR مشترک نیست.
  • Cache تست‌ها از یکدیگر جدا است.
  • خطاهای فنی خام به کاربر نمایش داده نمی‌شوند.
  • داده حساس بدون نیاز در Cache مرورگر قرار نمی‌گیرد.
  • Devtools برای محیط توسعه استفاده می‌شود.
  • عملکرد Requestها و Cache در مرورگر بررسی شده است.

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

React Query چیست؟

React Query نام قبلی TanStack Query است؛ کتابخانه‌ای برای دریافت، Cache، همگام‌سازی و به‌روزرسانی Server State در برنامه‌های React.

تفاوت React Query و TanStack Query چیست؟

پروژه React Query به مجموعه TanStack منتقل و با نام TanStack Query توسعه داده شد. Package فعلی React آن @tanstack/react-query است.

آیا TanStack Query جایگزین Axios است؟

خیر. Axios و fetch Request HTTP را ارسال می‌کنند. TanStack Query چرخه عمر، Cache، Retry، Refetch و همگام‌سازی داده را مدیریت می‌کند.

آیا React Query جایگزین Redux است؟

برای Server State می‌تواند بخش بزرگی از نیاز به Redux را کاهش دهد، اما مدیریت Client State پیچیده همچنان ممکن است به Redux Toolkit یا ابزار دیگری نیاز داشته باشد.

useQuery چه کاری انجام می‌دهد؟

useQuery یک تابع Async را اجرا می‌کند، نتیجه را Cache می‌کند و وضعیت‌هایی مانند Pending، Error، Success و Fetching را در اختیار Component قرار می‌دهد.

useMutation چه تفاوتی با useQuery دارد؟

useQuery معمولاً برای خواندن داده استفاده می‌شود. useMutation برای عملیات دارای Side Effect مانند ایجاد، ویرایش، حذف یا ارسال پیام مناسب است.

Query Key چیست؟

شناسه یک Query در Cache است. تمام پارامترهایی که روی نتیجه اثر می‌گذارند باید در Query Key قرار بگیرند.

staleTime چیست؟

مدتی است که داده Fresh در نظر گرفته می‌شود. تا پیش از پایان این زمان، Refetchهای خودکار معمول کاهش پیدا می‌کنند.

gcTime چیست؟

مدتی است که Query غیرفعال می‌تواند در Cache باقی بماند، پیش از آنکه Garbage Collect شود.

چگونه بعد از Mutation داده را به‌روزرسانی کنیم؟

می‌توان Query مرتبط را با invalidateQueries قدیمی اعلام کرد یا نتیجه Mutation را با setQueryData مستقیماً در Cache قرار داد.

Optimistic Update چیست؟

تغییر رابط کاربری پیش از دریافت پاسخ Server است. در صورت شکست عملیات، داده قبلی باید Rollback شود.

آیا TanStack Query برای Pagination مناسب است؟

بله. Queryهای صفحه‌بندی‌شده، Cursor Pagination، Prefetch صفحه بعد و Infinite Query را پشتیبانی می‌کند.

آیا می‌توان وضعیت Job هوش مصنوعی را با React Query بررسی کرد؟

بله. با refetchInterval می‌توان وضعیت Job را Poll کرد و پس از تکمیل یا شکست، Polling را متوقف کرد.

آیا TanStack Query برای Streaming هوش مصنوعی مناسب است؟

برای نتیجه نهایی مناسب است، اما دریافت تدریجی Tokenها معمولاً باید با fetch Streaming، SSE یا WebSocket مدیریت شود و سپس Cache به‌روزرسانی شود.

آیا می‌توان API درواره را مستقیم از React فراخوانی کرد؟

نباید API Key واقعی را در Frontend قرار دهید. React باید Backend برنامه را فراخوانی کند و Backend با کلید محفوظ به API درواره متصل شود.

جمع‌بندی

TanStack Query یا React Query یکی از کاربردی‌ترین ابزارها برای مدیریت Server State در برنامه‌های React است. این کتابخانه منطق تکراری مربوط به Loading، Error، Cache، Retry، Refetch، Pagination و Mutation را از Componentها خارج می‌کند و یک الگوی منظم برای همگام‌سازی Frontend با Backend ارائه می‌دهد.

برای استفاده صحیح از TanStack Query باید چند مفهوم اصلی را به‌خوبی درک کنید:

  • Query Key هویت داده در Cache است.
  • Query Function باید در خطاهای HTTP، Error ایجاد کند.
  • staleTime تازگی داده را کنترل می‌کند.
  • gcTime مدت نگهداری Query غیرفعال را مشخص می‌کند.
  • Query برای خواندن و Mutation برای تغییر داده است.
  • پس از Mutation باید Cache مرتبط Update یا Invalid شود.
  • Pagination، Infinite Query و Polling باید محدود و قابل توقف باشند.
  • Streaming پاسخ هوش مصنوعی به مدیریت جداگانه نیاز دارد.

در یک برنامه هوش مصنوعی، TanStack Query می‌تواند فهرست مدل‌ها، مکالمات، پیام‌ها، گزارش مصرف و وضعیت Jobها را مدیریت کند. Backend نیز مسئول نگهداری API Key، ارتباط با درواره، اعتبارسنجی پاسخ و ثبت اطلاعات باقی می‌ماند.

این جداسازی باعث می‌شود رابط React سریع‌تر، قابل پیش‌بینی‌تر و ساده‌تر نگهداری شود، بدون اینکه Secretها یا منطق حساس Backend وارد مرورگر شوند.

منابع تکمیلی

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.