• صفحه اصلی
  • درباره من
  • مهارت‌های ایجنت
  • پروژه‌ها
  • بلاگ
  • مشاوره
  • تماس با من
مشاورهرزومه
Naser Rasouli

نویسنده

Naser Rasouli

توسعه‌دهنده فرانت‌اند؛ اینجا تجربه‌ها و یادداشت‌های واقعی‌ام از پروژه‌ها را می‌نویسم.

GitHubLinkedIn

آخرین نوشته‌ها

Codebase Memory MCP چیست؟ حافظه ساختاری برای Coding Agentها
2026-08-08•1 دقیقه مطالعه

Codebase Memory MCP چیست؟ حافظه ساختاری برای Coding Agentها

راهنمای Codebase Memory MCP برای ساخت Knowledge Graph از پروژه و کمک به Coding Agentها در فهم سریع‌تر کدبیس‌های بزرگ فرانت‌اند.

متدولوژی BEM در CSS: نام‌گذاری استاندارد برای کدهای تمیز
2026-02-18•1 دقیقه مطالعه

متدولوژی BEM در CSS: نام‌گذاری استاندارد برای کدهای تمیز

راهنمای عملی BEM برای جلوگیری از تداخل استایل، ساختاردهی کلاس‌ها و نگهداری ساده‌تر CSS.

بررسی نحوه به‌روزرسانی state در React
2026-02-11•1 دقیقه مطالعه

بررسی نحوه به‌روزرسانی state در React

setState بلافاصله state را عوض نمی‌کند؛ همین باعث می‌شود console.log مقدار قبلی را لاگ کند. این راهنما توضیح می‌دهد چرا و چطور مقدار جدید را درست دریافت کنیم.

معماری Cache در TanStack Query برای پروژه‌های React

معماری Cache در TanStack Query برای پروژه‌های React

2026-09-07
reacttanstack-queryarchitecturecachingserver-state

معماری Cache در TanStack Query برای پروژه‌های React

مشکل اصلی در پروژه‌های بزرگ معمولاً خود TanStack Query نیست؛ مشکل این است که بدون قرارداد مشخص، هر Feature برای queryKey، staleTime، invalidation و prefetch تصمیم متفاوتی می‌گیرد. نتیجه می‌تواند refetch بی‌دلیل، cacheهای موازی برای یک داده و mutationهایی باشد که نمی‌دانند دقیقاً چه چیزی را invalidate کنند.

هدف این مقاله ساختن یک قرارداد ساده برای server state است؛ نه ایجاد یک لایه wrapper سنگین روی TanStack Query.

1. اول مرز Server State را روشن کنید

TanStack Query برای داده‌ای مناسب است که منبع حقیقت آن بیرون از مرورگر است:

API / Server
    ↓
TanStack Query Cache
    ↓
React UI

مواردی مثل وضعیت باز یا بسته بودن Modal، step فعلی یک wizard یا draft محلی فرم معمولاً server state نیستند.

اگر یک داده از API می‌آید و توسط چند بخش رابط کاربری مصرف می‌شود، قبل از بردن آن به global client store بررسی کنید که آیا همان Query Cache کافی نیست.

2. Query Key را بخشی از معماری Feature بدانید

TanStack Query cache را بر اساس queryKey مدیریت می‌کند. Key باید داده را به‌صورت یکتا توصیف کند و هر متغیری که queryFn به آن وابسته است باید در key حضور داشته باشد.

برای یک Feature سفارش:

export const orderKeys = {
  all: ["orders"] as const,
  lists: () => [...orderKeys.all, "list"] as const,
  list: (filters: OrderFilters) =>
    [...orderKeys.lists(), filters] as const,
  details: () => [...orderKeys.all, "detail"] as const,
  detail: (id: string) =>
    [...orderKeys.details(), id] as const,
}

این ساختار دو مزیت دارد:

  1. invalidation از عمومی به اختصاصی قابل پیش‌بینی می‌شود؛
  2. keyها کنار Feature مالک داده باقی می‌مانند.

لازم نیست برای هر Query یک «Query Key Factory Framework» داخلی بسازید. یک object کوچک و قابل‌خواندن معمولاً کافی است.

3. queryOptions را برای هم‌مکان‌کردن قرارداد Query استفاده کنید

در TypeScript، queryOptions راه خوبی است تا queryKey و queryFn را کنار هم نگه دارید:

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

export const orderOptions = {
  detail: (id: string) =>
    queryOptions({
      queryKey: orderKeys.detail(id),
      queryFn: () => getOrder(id),
      staleTime: 30_000,
    }),
}

سپس همان قرارداد را در UI، prefetch یا cache update استفاده کنید:

const query = useQuery(orderOptions.detail(orderId))

این الگو بهتر از wrapper عمومی مثل useAppQuery() است که API اصلی کتابخانه را پنهان کند و تعداد زیادی option را دوباره expose کند.

4. staleTime را بر اساس ماهیت داده تنظیم کنید

در TanStack Query داده cache‌شده به‌صورت پیش‌فرض stale در نظر گرفته می‌شود و queryهای stale در موقعیت‌هایی مثل mount، focus یا reconnect می‌توانند دوباره fetch شوند.

پس staleTime باید پاسخ یک سؤال باشد:

این داده برای چه مدت می‌تواند بدون درخواست جدید، تازه محسوب شود؟

مثلاً:

queryOptions({
  queryKey: ["countries"],
  queryFn: getCountries,
  staleTime: 30 * 60 * 1000,
})

اما برای وضعیت سفارش در حال پردازش:

queryOptions({
  queryKey: ["orders", orderId],
  queryFn: () => getOrder(orderId),
  staleTime: 10_000,
})

یک staleTime واحد برای کل پروژه معمولاً تصمیم خوبی نیست.

5. بعد از Mutation دقیق invalidate کنید

بعد از mutation، ساده‌ترین راه اغلب invalidation هدفمند است:

const mutation = useMutation({
  mutationFn: updateOrder,
  onSuccess: async (_, variables) => {
    await queryClient.invalidateQueries({
      queryKey: orderKeys.detail(variables.id),
    })

    await queryClient.invalidateQueries({
      queryKey: orderKeys.lists(),
    })
  },
})

invalidateQueries queryهای match‌شده را stale می‌کند و queryهای active می‌توانند در پس‌زمینه refetch شوند.

اشتباه رایج:

queryClient.invalidateQueries()

این کار گاهی برای ابزارهای داخلی یا reset کلی قابل قبول است، اما اگر برای هر mutation انجام شود dependency واقعی بین mutation و cache را پنهان می‌کند.

6. همیشه لازم نیست بعد از Mutation refetch کنید

اگر پاسخ mutation همان entity به‌روز‌شده را برمی‌گرداند، می‌توانید cache مشخص را مستقیم update کنید:

onSuccess: (updatedOrder) => {
  queryClient.setQueryData(
    orderKeys.detail(updatedOrder.id),
    updatedOrder,
  )
}

برای listهایی که sorting، pagination یا filter پیچیده دارند، invalidation معمولاً از patch دستی امن‌تر است.

قاعده عملی:

Entity response is authoritative
→ setQueryData

Related collections may have changed
→ invalidateQueries

7. Prefetch را نزدیک Navigation Intent قرار دهید

Prefetch زمانی ارزش دارد که احتمال استفاده از داده بالا باشد.

مثلاً هنگام hover یا قبل از navigation:

await queryClient.prefetchQuery(
  orderOptions.detail(orderId),
)

اما prefetch همه چیز در startup باعث می‌شود cache به یک سیستم eager-fetch تبدیل شود.

بهتر است prefetch را به intent کاربر، route loader یا مسیرهای پرتکرار وصل کنید.

8. Query Cache را با Global Store کپی نکنید

این anti-pattern رایج است:

API
 ↓
TanStack Query
 ↓
Redux/Zustand
 ↓
Component

اگر Redux/Zustand فقط یک کپی از پاسخ Query نگه می‌دارد، دو source of truth ایجاد می‌شود.

Global client state را برای state واقعاً client-owned نگه دارید؛ server state را تا زمانی که نیاز مشخصی وجود ندارد در Query Cache نگه دارید.

9. Query را نزدیک Feature مالک داده قرار دهید

ساختار ساده:

features/orders/
├── api/
│   ├── get-order.ts
│   └── update-order.ts
├── queries/
│   ├── order-keys.ts
│   └── order-options.ts
└── components/
    └── order-details.tsx

این ساختار یعنی Feature مالک قرارداد cache خودش است، ولی QueryClient و provider همچنان infrastructure سراسری برنامه هستند.

10. چند anti-pattern که باید در Review پیدا کنید

Keyهای رشته‌ای پراکنده

["order", id]
["orders", id]
["order-detail", id]

سه convention برای یک entity یعنی invalidation سخت‌تر.

staleTime: Infinity بدون دلیل

این مقدار ممکن است مناسب داده‌ای باشد که فقط با invalidation دستی تغییر می‌کند، اما نباید راه فرار از فهمیدن lifecycle داده باشد.

wrapper عمومی برای همه Queryها

اگر wrapper فقط options کتابخانه را دوباره تعریف می‌کند، coupling و migration cost اضافه می‌کند.

invalidation خیلی گسترده

هر mutation باید dependency cache خودش را مشخص کند.

mirror کردن server state در store دیگر

این کار synchronization را به مسئله جدیدی تبدیل می‌کند.

یک قرارداد عملی برای تیم

برای هر Feature این سؤال‌ها را پاسخ دهید:

1. Owner داده کدام Feature است؟
2. Query key hierarchy چیست؟
3. متغیرهای queryFn داخل key هستند؟
4. staleTime با ماهیت داده سازگار است؟
5. mutation کدام keyها را تغییر می‌دهد؟
6. setQueryData بهتر است یا invalidation؟
7. آیا prefetch واقعاً با navigation intent مرتبط است؟
8. آیا داده بدون دلیل در Zustand/Redux کپی شده؟

اگر این قرارداد روشن باشد، بیشتر مشکلات cache قبل از تبدیل‌شدن به bug حذف می‌شوند.

جمع‌بندی

معماری خوب TanStack Query یعنی استفاده بیشتر از abstraction نیست. برعکس، معمولاً یعنی:

Feature ownership
      ↓
Stable query keys
      ↓
Co-located query options
      ↓
Intentional staleTime
      ↓
Targeted invalidation
      ↓
No duplicated server state

وقتی این مرزها روشن باشند، Query Cache به بخشی قابل‌پیش‌بینی از معماری React تبدیل می‌شود، نه مجموعه‌ای از fetchهای پراکنده.

منابع

  • https://tanstack.com/query/latest/docs/framework/react/guides/query-keys
  • https://tanstack.com/query/latest/docs/framework/react/guides/query-options
  • https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults
  • https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation
  • https://tanstack.com/query/latest/docs/framework/react/guides/prefetching