mcpField
R
router-query

router-query

github.com/tanstack开发15.3k384 次安装4 天前更新
代码仓库

关于 router-query

面向熟悉 TanStack Router 与 TanStack Query 的开发者,说明如何将 Query 用作外部数据缓存并与路由加载器协作。涵盖客户端数据获取、SSR 水合与流式查询,并指出缓存协调中的关键配置。

主要功能

  1. 01协调路由加载器与 Query 缓存
  2. 02静态查询与组件订阅模式
  3. 03非阻塞后台查询与流式处理
  4. 04SSR 查询状态水合与传输
  5. 05错误处理及常见配置说明

适用场景

  1. 01在导航期间预取并缓存页面数据
  2. 02为非关键数据启用后台加载
  3. 03为 SSR 应用配置查询水合
  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 内置的预加载缓存(默认 30s)会阻止 Query 控制数据的新鲜度。

CRITICAL:对于 SSR,请在 createRouter 工厂函数内部创建 QueryClient。在模块级别创建单例会导致不同服务器请求之间的数据泄漏。

设置: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>
}

流式传输模式:触发但不等待 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. HIGH:未将 defaultPreloadStaleTime 设置为 0

Router 内置了预加载缓存(预加载的默认 staleTime 为 30s)。这会阻止 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. HIGH:在 SSR 中于 createRouter 外部创建 QueryClient

模块级单例 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. MEDIUM:在加载器中等待即发即弃的查询会阻塞渲染

用于流式传输的查询应在后台启动,并使用 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. HIGH:createRootRouteWithContext 缺少双括号

createRootRouteWithContext<Type>() 是一个工厂函数——它会返回一个函数。第二次调用时传入路由选项。

// 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 用作外部缓存时:

  • 将 defaultPreloadStaleTime: 0,防止 Router 缓存绕过 Query 的新鲜度逻辑
  • 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 设置

评论

支持 Markdown