router-query について
TanStack Router のローダーと TanStack Query を外部データキャッシュとして連携する開発者向けスキルです。クライアントでの取得、SSR のハイドレーション、ストリーミング、設定上の注意点を扱います。
主な機能
- 01ローダーと Query キャッシュの連携
- 02静的クエリとコンポーネント購読
- 03非ブロッキングなクエリのストリーミング
- 04SSR でのクエリ状態の転送と復元
- 05エラー処理と設定上の注意点
活用シーン
- 01ナビゲーション中のページデータ取得とキャッシュ
- 02重要度の低いデータのバックグラウンド取得
- 03SSR アプリのクエリハイドレーション設定
- 04ルーターキャッシュによる鮮度制御の阻害防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tanstack/ router --skill router-queryインストール後、Claude Code で/router-queryを入力して読み込まれたか確認します。
詳細
TanStack Router + TanStack Query の統合
このスキルを利用するには、TanStack Router と TanStack Query の両方に精通している必要があります。まず @tanstack/router-core#router-core と react-router をお読みください。
このスキルでは、TanStack Query を外部データキャッシュとして TanStack Router のローダーシステムと連携させる方法を説明します。ルーターはコーディネーターとして機能し、ナビゲーション中にデータ取得を開始します。一方、Query はキャッシュ、バックグラウンドでの再取得、データのライフサイクルを管理します。
CRITICAL:TanStack Query を使用する場合は、
defaultPreloadStaleTime: 0を設定してください。これを設定しないと、Router に組み込まれたプリロードキャッシュ(デフォルトは 30 秒)により、Query がデータの鮮度を制御できなくなります。
CRITICAL:SSR では、
QueryClientをcreateRouterファクトリー関数内で作成してください。モジュールレベルのシングルトンを使うと、サーバーリクエスト間でデータが漏洩します。
セットアップ:Router コンテキスト内の QueryClient
基本(クライアントのみ)
// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
RouterProvider,
createRouter,
createRootRouteWithContext,
} from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
// Root route declares that router context includes queryClient
// (root route file creates it with createRootRouteWithContext — see below)
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0, // Let Query manage caching
context: { queryClient },
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
function App() {
return <RouterProvider router={router} />
}
コンテキストを持つルート
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
// Double parentheses: factory pattern
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
component: () => <Outlet />,
})
SSR 対応のセットアップ
// src/router.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function createAppRouter() {
// Fresh QueryClient per request — prevents data leaking between SSR requests
const queryClient = new QueryClient()
return createRouter({
routeTree,
defaultPreloadStaleTime: 0,
context: { queryClient },
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
})
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createAppRouter>
}
}
setupRouterSsrQueryIntegration を使用した SSR
SSR のデハイドレーション/ハイドレーションとストリーミングを自動で行うには:
npm install @tanstack/react-router-ssr-query
// src/router.tsx
import { QueryClient } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { routeTree } from './routeTree.gen'
export function createAppRouter() {
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
context: { queryClient },
})
setupRouterSsrQueryIntegration({
router,
queryClient,
// wrapQueryClient: true (default — wraps with QueryClientProvider)
// handleRedirects: true (default — handles redirect() from queries)
})
return router
}
この統合機能では、次のことが行われます。
- サーバー上でクエリの状態をデハイドレーションし、クライアント上で自動的にハイドレーションします
- サーバーのレンダリング中に解決されたクエリをクライアントにストリーミングします
- クエリ/ミューテーションからスローされた
redirect()を処理します
手動での SSR デハイドレーション/ハイドレーション(SSR Query パッケージを使用しない場合)
// src/router.tsx
import { QueryClient, dehydrate, hydrate } from '@tanstack/react-query'
import { QueryClientProvider } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function createAppRouter() {
const queryClient = new QueryClient()
return createRouter({
routeTree,
defaultPreloadStaleTime: 0,
context: { queryClient },
dehydrate: () => ({
queryClientState: dehydrate(queryClient),
}),
hydrate: (dehydrated) => {
hydrate(queryClient, dehydrated.queryClientState)
},
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
})
}
基本パターン:ローダー内の静的な query +コンポーネント内の useSuspenseQuery
これは推奨されるパターンです。ローダーがレンダリング前にデータをクエリし(読み込み中のちらつきがありません)、staleTime: 'static' を使って、キャッシュ済みデータを古くなったデータとして再取得せずに再利用します。コンポーネントはキャッシュをサブスクライブして更新を受け取ります。
// src/routes/posts.tsx
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
interface Post {
id: string
title: string
}
const postsQueryOptions = queryOptions({
queryKey: ['posts'],
queryFn: (): Promise<Array<Post>> =>
fetch('/api/posts').then((r) => r.json()),
})
export const Route = createFileRoute('/posts')({
loader: ({ context }) => {
// query returns cached data when available and fetches when it is missing
// Keep this query static so a stale refetch does not block navigation
return context.queryClient.query({
...postsQueryOptions,
staleTime: 'static',
})
},
component: PostsPage,
})
function PostsPage() {
// useSuspenseQuery subscribes to cache — gets background updates
const { data: posts } = useSuspenseQuery(postsQueryOptions)
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
動的パラメーターを使用する場合
// src/routes/posts/$postId.tsx
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
interface Post {
id: string
title: string
content: string
}
const postQueryOptions = (postId: string) =>
queryOptions({
queryKey: ['posts', postId],
queryFn: () => fetch(`/api/posts/${postId}`).then((r) => r.json()),
})
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context, params }) => {
return context.queryClient.query({
...postQueryOptions(params.postId),
staleTime: 'static',
})
},
component: PostPage,
})
function PostPage() {
const { postId } = Route.useParams()
const { data: post } = useSuspenseQuery(postQueryOptions(postId))
return <article>{post.title}</article>
}
ストリーミングパターン:待機しない fire-and-forget の query
重要度の低いデータについては、ナビゲーションをブロックせずにクエリを開始します。バックグラウンドでの失敗が未処理の拒否にならないよう、noop で拒否を処理してください。
import { noop, useQuery, useSuspenseQuery } from '@tanstack/react-query'
export const Route = createFileRoute('/dashboard')({
loader: ({ context }) => {
// Await critical data
const user = context.queryClient.query({
...userQueryOptions,
staleTime: 'static',
})
// Start non-critical query without awaiting — streams during SSR
void context.queryClient.query({ ...analyticsQueryOptions }).catch(noop)
return user
},
component: Dashboard,
})
function Dashboard() {
// Critical: suspense (data ready immediately)
const { data: user } = useSuspenseQuery(userQueryOptions)
// Non-critical: regular query (shows loading state)
const { data: analytics, isLoading } = useQuery(analyticsQueryOptions)
return (
<div>
<h1>Welcome {user.name}</h1>
{isLoading ? <Skeleton /> : <AnalyticsChart data={analytics} />}
</div>
)
}
useQueryErrorResetBoundary によるエラー処理
import { useEffect } from 'react'
import { useQueryErrorResetBoundary } from '@tanstack/react-query'
import { useRouter } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: ({ context }) =>
context.queryClient.query({
...postsQueryOptions,
staleTime: 'static',
}),
errorComponent: PostsErrorComponent,
component: PostsPage,
})
function PostsErrorComponent({
error,
reset,
}: {
error: unknown
reset: () => void
}) {
const router = useRouter()
const queryErrorResetBoundary = useQueryErrorResetBoundary()
useEffect(() => {
queryErrorResetBoundary.reset()
}, [queryErrorResetBoundary])
return (
<div>
<p>{error instanceof Error ? error.message : String(error)}</p>
<button onClick={() => router.invalidate()}>Retry</button>
</div>
)
}
よくある間違い
1. 高:defaultPreloadStaleTime を 0 に設定していない
Router には組み込みのプリロードキャッシュがあります(プリロードのデフォルトの staleTime は 30 秒です)。このため、プリロード中に Query がデータの鮮度を制御できません。
// WRONG — Router's preload cache serves stale data, Query never refetches
const router = createRouter({ routeTree })
// CORRECT — disable Router's preload cache, let Query manage freshness
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
2. 高:SSR 用の QueryClient を createRouter の外で作成する
モジュールレベルのシングルトン QueryClient はすべてのサーバーリクエストで共有されるため、リクエスト間でユーザーデータが漏洩します。
// WRONG — shared across SSR requests
const queryClient = new QueryClient()
export function createAppRouter() {
return createRouter({
routeTree,
context: { queryClient },
})
}
// CORRECT — new QueryClient per createAppRouter call
export function createAppRouter() {
const queryClient = new QueryClient()
return createRouter({
routeTree,
context: { queryClient },
})
}
3. 中:ローダー内で fire-and-forget のクエリを待機すると、レンダリングがブロックされる
ストリーミングに使うクエリはバックグラウンドで開始し、catch(noop) でエラーを処理する必要があります。クエリを待機すると、データが解決するまでナビゲーションの遷移がブロックされ、ストリーミングの目的が損なわれます。
import { noop } from '@tanstack/react-query'
// WRONG — blocks navigation, no streaming benefit
loader: async ({ context }) => {
await context.queryClient.query({ ...analyticsQueryOptions }).catch(noop)
}
// CORRECT — fire and forget for streaming, with handled rejection
loader: ({ context }) => {
void context.queryClient.query({ ...analyticsQueryOptions }).catch(noop)
}
// If you need to block (critical data), use a static query instead:
loader: ({ context }) => {
return context.queryClient.query({
...criticalQueryOptions,
staleTime: 'static',
})
}
4. 高:createRootRouteWithContext で二重の括弧を付け忘れる
createRootRouteWithContext<Type>() はファクトリーであり、関数を返します。2 回目の呼び出しでルートのオプションを渡します。
// WRONG — passing options to the factory, not the returned function
const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>({
component: RootComponent,
})
// CORRECT — double call: factory()({options})
const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({
component: RootComponent,
})
注意点:組み込みの SWR キャッシュと外部キャッシュ
TanStack Router には独自の SWR キャッシュ(staleTime、gcTime、defaultPreloadStaleTime)があります。Query を外部キャッシュとして使用する場合:
- Router のキャッシュが Query の鮮度判定を先回りして処理しないように、
defaultPreloadStaleTime: 0を設定します - Router の
staleTime/gcTimeはローダーの戻り値にも適用されます。Query のみを使うパターンでは、ローダーから何も返さず(副作用として静的なqueryのみを実行し)、データはuseSuspenseQueryだけから読み取ります router.invalidate()はローダーを再実行します(ローダー内でstaleTime: 'static'を指定したqueryが呼び出されます)。そのため、キャッシュ済みの Query データが引き続き鮮度の基準となります
関連項目
@tanstack/router-core#router-core/data-loading— 組み込みローダーキャッシュの基本@tanstack/router-core#router-core/ssr— デハイドレーション/ハイドレーションのための SSR セットアップ