Ana içeriğe geç

TanStack Router ve Query'yi Birlikte Kullanmak

Teknik3 dk

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:

  • QueryClient router context'ine verilmeli; kök rota createRootRouteWithContext ile tip güvenli tanımlanmalı.
  • SSR'da QueryClient istek 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 setupRouterSsrQueryIntegration bağ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 verisiQuery (queryOptions + hook)
Yalnızca o rotaya özel meta (redirect, SEO, komşu kayıt)Loader + useLoaderData
Tek rotada kullanılan, paylaşılmayan veriYalnı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.


Kaynak: TanStack Router and Query - TkDodo

Arama paleti

Sayfa, yazı veya eylem ara