TanStack Router ve Query'yi Birlikte Kullanmak
TanStack Router veriyi loader üzerinden cache'leyebiliyor; TanStack Query de aynı işi yapıyor gibi görünüyor. İkisi bir arada kullanıldığında sınır nerede çizilmeli?
İki Cache, Farklı Kapsam
Router cache'i rota bazlıdır: veri yalnızca o rotada (ve alt rotalarında) yaşar. Tek bir sayfaya özgü veri için yeterli. Query cache'i ise queryKey ile globaldir - aynı veri birden fazla rotadan okunabiliyorsa paylaşılır. TkDodo'nun yazısında bu ayrım, iki aracın neden birlikte kullanıldığının temelini oluşturuyor.
Loader ve Query'yi Birleştirmek
Önerilen desen: fetch'i loader'da başlat, veriyi component'te Query hook'u ile oku. Rota loader'ları component render'ından önce çalışır; Router'ın prefetch: 'intent' ayarıyla link üzerine gelindiğinde de tetiklenir (prefetch-on-hover). Loader'da context.queryClient.ensureQueryData(...) cache'i erken doldurur; component'te aynı queryOptions ile useSuspenseQuery kullanıldığında tek istek atılır, veri cache'ten gelir.
Blog listesinde desen şöyle görünüyor:
export const Route = createFileRoute("/blog/")({
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(blogPostsQueryOptions())
},
component: BlogIndexPage,
})
function BlogIndexPage() {
const { data: posts } = useGetBlogPosts()
return <BlogPage posts={posts} />
}export function useGetBlogPosts() {
return useSuspenseQuery(blogPostsQueryOptions())
}Loader yalnızca Query cache'ini dolduruyor; liste verisi useLoaderData ile taşınmıyor. Aynı blogPostsQueryOptions() ana sayfa loader'ında da prefetch edildiği için liste verisi rotalar arasında paylaşılıyor.
Kurulumda dört nokta kritik:
QueryClientrouter context'ine verilmeli; kök rotacreateRootRouteWithContextile tip güvenli tanımlanmalı.- SSR'da
QueryClientistek başına oluşturulmalı; modül seviyesi singleton istekler arası veri sızdırır. - Query kullanırken Router'ın preload stale cache'ini devre dışı bırak:
defaultPreloadStaleTime: 0(tazeliği Query yönetir, preload davranışı sürer). - TanStack Start ile SSR (Server-Side Rendering) için
setupRouterSsrQueryIntegrationbağlanmalı; sunucuda çekilen veri istemci cache'ine aktarılır.
const queryClient = createQueryClient()
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreloadStaleTime: 0
})
setupRouterSsrQueryIntegration({
router,
queryClient,
wrapQueryClient: true
})Ne Nereye Konur?
Loader'ı event handler gibi düşünmek işe yarıyor: öncelikle cache'i doldurur, Query verisini useLoaderData ile component'e taşımak önerilmez. Query observer'ı (useQuery veya useSuspenseQuery) olmadan veri "pasif" kalır; window focus refetch'i, invalidation ve gc zaman aşımı devreye girmez, cache'ten silinebilir.
| Veri türü | Nereye? |
|---|---|
| Birden fazla rotada paylaşılan sunucu verisi | Query (queryOptions + hook) |
| Yalnızca o rotaya özel meta (redirect, SEO, komşu kayıt) | Loader + useLoaderData |
| Tek rotada kullanılan, paylaşılmayan veri | Yalnızca loader yeterli |
useSuspenseQuery ve SSR
Loader'da await etmek blocking veri için geçerli bir yol. Query entegrasyonunda useSuspenseQuery, router'ın errorComponent ve Suspense sınırlarıyla uyumlu çalışır; component yalnızca başarılı duruma odaklanır. SSR'da useSuspenseQuery, streaming ile daha doğal örtüşür - TkDodo bu noktada özellikle Suspense tarafını öne çıkarıyor.
Sitemde Nasıl Uyguladım?
Kişisel sitem TanStack Start ve Supabase üzerinde. Blog detayında slug redirect, notFound, komşu yazılar ve SEO meta verisi loader'da kalıyor; yazı içeriği Query'den geliyor:
loader: async ({ context, params }) => {
const slug = routeParamToBlogSlug(params.slug)
const [post, neighbours] = await Promise.all([
context.queryClient.ensureQueryData(blogPostDetailQueryOptions(slug)),
getBlogPostNeighboursFn({ data: { slug } })
])
if (!post) {
throw notFound()
}
return {
slug,
previous: neighbours.previous,
next: neighbours.next,
seo: {
title: post.title,
description: post.description
}
}
},
function BlogPostRoutePage() {
const { slug, previous, next } = Route.useLoaderData()
const { data: post } = useGetBlogPostDetail(slug)
return (
<BlogPostDetailPage post={post} previous={previous} next={next} />
)
}ensureQueryData yazı içeriğini Query cache'ine yazar; useGetBlogPostDetail aynı queryOptions ile observer oluşturur. Komşu yazılar ve SEO meta verisi yalnızca bu rotada kullanıldığından loader'dan dönüyor. /about rotasında ise GitHub katkı verisi yalnızca o sayfada kullanıldığından Query'siz loader yeterli.