ترکیب TanStack Router و TanStack Query
خلاصهٔ کاملتر
TanStack Router و TanStack Query هر دو از یه اکوسیستم میان، پس طبیعیه که کنار هم خوب کار کنن. اما یه سوال مهم وجود داره: Router که کش داخلی داره، چرا باید Query رو هم بیاریم؟
کش داخلی Router فقط در سطح همون مسیر (route) کار میکنه. یعنی اگه دادهای مثل اطلاعات کاربر رو توی /dashboard لود کنی، مسیر دیگهای مثل /settings بهش دسترسی نداره و باید دوباره fetch کنه. Query Cache اما سراسریه — از طریق queryKey در هر جایی از اپ قابل دسترسه. همین یه دلیل کافیه که خیلیها در اپهای سمت کلاینت به Query وابسته باشن.
پس چرا اصلاً از loader استفاده کنیم؟ وقتی از Query داریم استفاده میکنیم، loader ها یه نقش مهم دارن: شروع زودهنگام fetch. لودرها قبل از رندر کامپوننت اجرا میشن، حتی ممکنه قبل از دانلود JS bundle مربوط به اون کامپوننت. با ویژگی prefetch: 'intent' روتر، وقتی کاربر موس رو روی یه لینک میبره، loader اون مسیر شروع میکنه — یعنی prefetch-on-hover رایگان.
دو تا تنظیم مهم هم باید انجام بدی. اول، QueryClient رو به context روتر اضافه کن تا توی لودرها بهش دسترسی داشته باشی. دوم، کش داخلی روتر رو غیرفعال کن چون وقتی Query مدیریت کش رو برعهده داره، نمیخوای دو تا بازیکن همزمان این کار رو انجام بدن — کافیه defaultPreloadStaleTime رو روی 0 بذاری.
یه تصمیم دیگه اینه که آیا داخل loader باید await بنویسیم یا نه. نویسنده پیشنهاد میکنه اصلاً await نکنیم و این تصمیم رو به کامپوننت بدیم:
export const Route = createFileRoute('/dashboard')({
loader: ({ context: { queryClient } }) => {
queryClient.prefetchQuery(dashboardQueryOptions())
queryClient.prefetchQuery(widgetCountQueryOptions())
},
})با این مدل، لودر نه asyncه، نه چیزی return میکنه. فقط fetch رو شروع میکنه. بعد توی کامپوننت، اگه از useSuspenseQuery استفاده کنی، داده بلاکینگه و تضمین میشه undefined نیست. اگه از useQuery استفاده کنی، داده non-blocking و ممکنه undefined باشه — میتونی یه skeleton loader نشون بدی.
یه نکته مهم: هیچوقت از useLoaderData برای خوندن دادهی Query استفاده نکن. Query برای ردیابی کوئریهای فعال به Observer نیاز داره که با useQuery ساخته میشه. بدون اون، کوئری «غیرفعال» حساب میشه و refetch خودکار، invalidation و جلوگیری از garbage collection همه از کار میافتن.
برای SSR با TanStack Start، اوضاع تقریباً همونه. لودرها isomorphic هستن — روی سرور هم اجرا میشن. دادهای که سرور fetch کرده، dehydrate شده و به کلاینت stream میشه تا QueryCache رو seed کنه. یه نکته: اگه از useQuery استفاده میکنی، باید توی loader آن رو await کنی وگرنه HTML اولیهی سرور شامل markup کامپوننت نمیشه. useSuspenseQuery این محدودیت رو نداره چون با streaming SSR کار میکنه.
نکات کلیدی:
- کش داخلی Router فقط per-route کار میکنه، ولی Query Cache سراسریه
- لودرها رو بهعنوان event handler ببین که فقط کش رو پر میکنن
- کش داخلی روتر رو غیرفعال کن (defaultPreloadStaleTime: 0)
- همیشه از useQuery یا useSuspenseQuery برای خوندن داده استفاده کن، نه useLoaderData
- useSuspenseQuery هم برای blocking data و هم برای SSR گزینه بهتریه
- با TanStack Start، کد لودر و کامپوننت تغییری نمیکنه — فقط یه integration سراسری اضافه میشه




