router-query

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Router + TanStack Query Integration

TanStack Router + TanStack Query 集成

This skill requires familiarity with both TanStack Router and TanStack Query. Read router-core and react-router first.
This skill covers coordinating TanStack Query as an external data cache with TanStack Router's loader system. The router acts as a coordinator — it triggers data fetching during navigation, while Query manages caching, background refetching, and data lifecycle.
CRITICAL: Set
defaultPreloadStaleTime: 0
when using TanStack Query. Without this, Router's built-in preload cache (30s default) prevents Query from controlling data freshness.
CRITICAL: For SSR, create
QueryClient
inside the
createRouter
factory function. A module-level singleton leaks data between server requests.
本 skill 要求熟悉 TanStack Router 和 TanStack Query。请先阅读 router-core 和 react-router。
本 skill 介绍如何将 TanStack Query 作为外部数据缓存,与 TanStack Router 的 loader 系统协同工作。Router 充当一个 协调器——它会在导航期间触发数据获取,而 Query 负责管理缓存、后台重新获取以及数据生命周期。
关键:使用 TanStack Query 时,请将
defaultPreloadStaleTime: 0
设置为 0。否则,Router 内置的预加载缓存(默认 30s)会阻止 Query 控制数据新鲜度。
关键:对于 SSR,请在
createRouter
工厂函数内部创建
QueryClient
。模块级单例会导致数据在服务器请求之间泄漏。

Setup: QueryClient in Router Context

设置:在 Router Context 中使用 QueryClient

Basic (Client-Only)

基础用法(仅客户端)

tsx
// 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} />
}
tsx
// 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} />
}

Root Route with Context

包含 Context 的根路由

tsx
// 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 />,
})
tsx
// 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-Safe Setup

SSR 安全设置

tsx
// 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>
  }
}
tsx
// 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>
  }
}

SSR with
setupRouterSsrQueryIntegration

使用
setupRouterSsrQueryIntegration
实现 SSR

For automatic SSR dehydration/hydration and streaming:
bash
npm install @tanstack/react-router-ssr-query
tsx
// 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
}
The integration:
  • Dehydrates query state on the server and hydrates on the client automatically
  • Streams queries that resolve during server render to the client
  • Handles
    redirect()
    thrown from queries/mutations
如需自动进行 SSR 脱水/水合和流式传输:
bash
npm install @tanstack/react-router-ssr-query
tsx
// 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
}
该集成能够:
  • 在服务器端自动脱水 query 状态,并在客户端自动水合
  • 将服务器渲染期间完成的 query 流式传输到客户端
  • 处理 query/mutation 中抛出的
    redirect()

Manual SSR Dehydration/Hydration (Without SSR Query Package)

手动 SSR 脱水/水合(不使用 SSR Query 包)

tsx
// 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>
    ),
  })
}
tsx
// 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>
    ),
  })
}

Core Pattern:
ensureQueryData
in Loader +
useSuspenseQuery
in Component

核心模式:在 Loader 中使用
ensureQueryData
,在组件中使用
useSuspenseQuery

This is the recommended pattern. The loader ensures data is in the cache before render (no loading flash). The component subscribes to the cache for updates.
tsx
// 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 }) => {
    // ensureQueryData returns cached data if available, fetches if not in cache
    // To also refetch stale data, pass revalidateIfStale: true
    return context.queryClient.ensureQueryData(postsQueryOptions)
  },
  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>
  )
}
这是推荐的模式。loader 确保数据在渲染前已进入缓存,从而避免加载闪烁;组件则订阅缓存以获取更新。
tsx
// 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 }) => {
    // ensureQueryData returns cached data if available, fetches if not in cache
    // To also refetch stale data, pass revalidateIfStale: true
    return context.queryClient.ensureQueryData(postsQueryOptions)
  },
  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>
  )
}

With Dynamic Params

使用动态参数

tsx
// 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.ensureQueryData(postQueryOptions(params.postId))
  },
  component: PostPage,
})

function PostPage() {
  const { postId } = Route.useParams()
  const { data: post } = useSuspenseQuery(postQueryOptions(postId))

  return <article>{post.title}</article>
}
tsx
// 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.ensureQueryData(postQueryOptions(params.postId))
  },
  component: PostPage,
})

function PostPage() {
  const { postId } = Route.useParams()
  const { data: post } = useSuspenseQuery(postQueryOptions(postId))

  return <article>{post.title}</article>
}

Streaming Pattern:
prefetchQuery
(Not Awaited)

流式模式:使用
prefetchQuery
(无需 await)

For non-critical data, start the fetch without blocking navigation:
tsx
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'

export const Route = createFileRoute('/dashboard')({
  loader: ({ context }) => {
    // Await critical data
    const user = context.queryClient.ensureQueryData(userQueryOptions)

    // Start non-critical fetch without awaiting — streams during SSR
    context.queryClient.prefetchQuery(analyticsQueryOptions)

    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>
  )
}
对于非关键数据,可以启动请求而不阻塞导航:
tsx
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'

export const Route = createFileRoute('/dashboard')({
  loader: ({ context }) => {
    // Await critical data
    const user = context.queryClient.ensureQueryData(userQueryOptions)

    // Start non-critical fetch without awaiting — streams during SSR
    context.queryClient.prefetchQuery(analyticsQueryOptions)

    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>
  )
}

Error Handling with
useQueryErrorResetBoundary

使用
useQueryErrorResetBoundary
处理错误

tsx
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.ensureQueryData(postsQueryOptions),
  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>
  )
}
tsx
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.ensureQueryData(postsQueryOptions),
  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>
  )
}

Common Mistakes

常见错误

1. HIGH: Not setting
defaultPreloadStaleTime
to 0

1. 高:未将
defaultPreloadStaleTime
设置为 0

Router has a built-in preload cache (default
staleTime
for preloads is 30s). This prevents Query from controlling data freshness during preloading.
tsx
// 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,
})
Router 内置了预加载缓存(预加载的默认
staleTime
为 30s)。这会阻止 Query 在预加载期间控制数据新鲜度。
tsx
// 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. HIGH: Creating QueryClient outside
createRouter
for SSR

2. 高:在 SSR 的
createRouter
外部创建 QueryClient

A module-level singleton
QueryClient
is shared across all server requests, leaking user data between requests.
tsx
// 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 },
  })
}
模块级单例
QueryClient
会被所有服务器请求共享,从而导致用户数据在请求之间泄漏。
tsx
// 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. MEDIUM: Awaiting
prefetchQuery
in loader blocks rendering

3. 中:在 loader 中 await
prefetchQuery
会阻塞渲染

prefetchQuery
is designed to fire-and-forget. Awaiting it blocks the navigation transition until the data resolves, defeating the purpose of streaming.
tsx
// WRONG — blocks navigation, no streaming benefit
loader: async ({ context }) => {
  await context.queryClient.prefetchQuery(analyticsQueryOptions)
}

// CORRECT — fire and forget for streaming
loader: ({ context }) => {
  context.queryClient.prefetchQuery(analyticsQueryOptions)
}

// If you need to block (critical data), use ensureQueryData instead:
loader: ({ context }) => {
  return context.queryClient.ensureQueryData(criticalQueryOptions)
}
prefetchQuery
的设计初衷是触发后不等待。对它执行 await 会阻塞导航过渡,直到数据解析完成,从而失去流式传输的优势。
tsx
// WRONG — blocks navigation, no streaming benefit
loader: async ({ context }) => {
  await context.queryClient.prefetchQuery(analyticsQueryOptions)
}

// CORRECT — fire and forget for streaming
loader: ({ context }) => {
  context.queryClient.prefetchQuery(analyticsQueryOptions)
}

// If you need to block (critical data), use ensureQueryData instead:
loader: ({ context }) => {
  return context.queryClient.ensureQueryData(criticalQueryOptions)
}

4. HIGH: Missing double parentheses on
createRootRouteWithContext

4. 高:
createRootRouteWithContext
缺少双层括号

createRootRouteWithContext<Type>()
is a factory — it returns a function. The second call passes route options.
tsx
// 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,
})
createRootRouteWithContext<Type>()
是一个工厂函数——它会返回一个函数。第二次调用用于传入路由选项。
tsx
// 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,
})

Tension: Built-In SWR Cache vs External Cache

冲突:内置 SWR 缓存与外部缓存

TanStack Router has its own SWR cache (
staleTime
,
gcTime
,
defaultPreloadStaleTime
). When using Query as an external cache:
  • Set
    defaultPreloadStaleTime: 0
    to prevent Router's cache from short-circuiting Query's freshness logic
  • Router's
    staleTime
    /
    gcTime
    still apply to the loader return value. For pure Query patterns, return nothing from the loader (just
    ensureQueryData
    for the side effect) and read data exclusively from
    useSuspenseQuery
  • router.invalidate()
    re-runs loaders (which call
    ensureQueryData
    ), but Query decides whether to actually refetch based on its own
    staleTime
TanStack Router 拥有自己的 SWR 缓存(
staleTime
、
gcTime
、
defaultPreloadStaleTime
)。将 Query 作为外部缓存使用时:
  • 将
    defaultPreloadStaleTime
    设置为 0,防止 Router 缓存绕过 Query 的数据新鲜度逻辑
  • Router 的
    staleTime
    /
    gcTime
    仍会应用于 loader 的返回值。对于纯 Query 模式,loader 不应返回数据(仅调用
    ensureQueryData
    产生副作用),而应 exclusively 通过
    useSuspenseQuery
    读取数据
  • router.invalidate()
    会重新运行 loader(loader 会调用
    ensureQueryData
    ),但是否实际重新获取数据仍由 Query 根据自身的
    staleTime
    决定

Cross-References

交叉参考

  • router-core/data-loading — built-in loader caching fundamentals
  • router-core/ssr — SSR setup for dehydration/hydration
  • router-core/data-loading——内置 loader 缓存的基础知识
  • router-core/ssr——用于脱水/水合的 SSR 设置