React Query چیست؟ آموزش کامل TanStack Query در React و TypeScript
در این آموزش TanStack Query یا React Query را از صفر یاد میگیرید و با useQuery، useMutation، Cache، Invalidation، Pagination، Infinite Query و Optimistic Update یک برنامه React متصل به API میسازید.
دریافت اطلاعات از 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 |
| useState | State محلی 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 میشود:
- TanStack Query کلید را در Cache جستوجو میکند.
- اگر داده وجود داشته باشد، میتواند آن را فوراً نمایش دهد.
- براساس Fresh یا Stale بودن داده تصمیم میگیرد Refetch انجام دهد.
- چند Component با Query Key یکسان از یک Cache مشترک استفاده میکنند.
- وقتی Query بدون Observer شود، مدتی در Cache باقی میماند.
- پس از پایان
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",
],
});
},
});
گردش عملیات:
- Queryهای فعال لغو میشوند.
- Cache قبلی ذخیره میشود.
- UI خوشبینانه تغییر میکند.
- اگر Request شکست خورد، Cache قبلی برمیگردد.
- در پایان داده 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
الگوی پیشنهادی:
- Streaming را با ابزار مناسب مدیریت کنید.
- متن موقت در حال تولید را در State محلی نگه دارید.
- پس از پایان پاسخ، پیام نهایی را در Backend ذخیره کنید.
- 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 وارد مرورگر شوند.
منابع تکمیلی
- مستندات رسمی TanStack Query
- راهنمای تنظیمات پیشفرض مهم
- راهنمای Query Invalidation
- Invalidation پس از Mutation
- راهنمای Mutationها
- راهنمای Optimistic Update
- راهنمای Infinite Query
- لغو Query با AbortSignal
- مهاجرت به TanStack Query v5
مقالات مرتبط
- React چیست؟ آموزش کامل ریاکت از صفر با پروژه عملی
- ساخت چتبات هوش مصنوعی با Next.js، React و API درواره
- هوش مصنوعی با Node.js؛ ساخت اپلیکیشن AI با Express و API
- ساخت Job Queue، Worker و Webhook برای API هوش مصنوعی
- Streaming API چیست؟ آموزش دریافت پاسخ جریانی
- آموزش کامل REST API و طراحی RESTful
- آموزش کامل JSON در Python، JavaScript و API هوش مصنوعی
- ساخت API هوش مصنوعی آماده Production
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.