mcpField
R
router-query

router-query

github.com/tanstack開発15.3k384 回インストール4 日前に更新
リポジトリ

router-query について

TanStack Router のローダーと TanStack Query を外部データキャッシュとして連携する開発者向けスキルです。クライアントでの取得、SSR のハイドレーション、ストリーミング、設定上の注意点を扱います。

主な機能

  1. 01ローダーと Query キャッシュの連携
  2. 02静的クエリとコンポーネント購読
  3. 03非ブロッキングなクエリのストリーミング
  4. 04SSR でのクエリ状態の転送と復元
  5. 05エラー処理と設定上の注意点

活用シーン

  1. 01ナビゲーション中のページデータ取得とキャッシュ
  2. 02重要度の低いデータのバックグラウンド取得
  3. 03SSR アプリのクエリハイドレーション設定
  4. 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 セットアップ

コメント