data-loading

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Data Loading

数据加载

Setup

基础使用

Basic loader returning data, consumed via
useLoaderData
:
tsx
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  component: PostsComponent,
})

function PostsComponent() {
  const posts = Route.useLoaderData()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
In code-split components, use
getRouteApi
instead of importing Route:
tsx
import { getRouteApi } from '@tanstack/react-router'

const routeApi = getRouteApi('/posts')

function PostsComponent() {
  const posts = routeApi.useLoaderData()
  return <ul>{/* ... */}</ul>
}
基础loader返回数据,通过
useLoaderData
消费:
tsx
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  component: PostsComponent,
})

function PostsComponent() {
  const posts = Route.useLoaderData()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
在代码分割组件中,使用
getRouteApi
替代直接导入Route:
tsx
import { getRouteApi } from '@tanstack/react-router'

const routeApi = getRouteApi('/posts')

function PostsComponent() {
  const posts = routeApi.useLoaderData()
  return <ul>{/* ... */}</ul>
}

Route Loading Lifecycle

路由加载生命周期

The router executes this sequence on every URL/history update:
  1. Route Matching (top-down)
    • route.params.parse
    • route.validateSearch
  2. Route Pre-Loading (serial)
    • route.beforeLoad
    • route.onError
      →
      route.errorComponent
  3. Route Loading (parallel)
    • route.component.preload?
    • route.loader
      • route.pendingComponent
        (optional)
      • route.component
    • route.onError
      →
      route.errorComponent
Key:
beforeLoad
runs before
loader
.
beforeLoad
for a parent runs before its children's
beforeLoad
. Throwing in
beforeLoad
prevents all children from loading.
每次URL/历史记录更新时,路由都会按以下顺序执行:
  1. 路由匹配(自上而下)
    • route.params.parse
    • route.validateSearch
  2. 路由预加载(串行执行)
    • route.beforeLoad
    • route.onError
      →
      route.errorComponent
  3. 路由加载(并行执行)
    • route.component.preload?
    • route.loader
      • route.pendingComponent
        (optional)
      • route.component
    • route.onError
      →
      route.errorComponent
核心规则:
beforeLoad
在
loader
之前执行。父路由的
beforeLoad
先于子路由的
beforeLoad
执行。在
beforeLoad
中抛出错误会阻止所有子路由加载。

Core Patterns

核心模式

loaderDeps for Search-Param-Driven Cache Keys

使用loaderDeps实现基于搜索参数的缓存键

Loaders don't receive search params directly. Use
loaderDeps
to declare which search params affect the cache key:
tsx
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  validateSearch: (search) => ({
    offset: Number(search.offset) || 0,
    limit: Number(search.limit) || 10,
  }),
  loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
  loader: ({ deps: { offset, limit } }) => fetchPosts({ offset, limit }),
})
When deps change, the route reloads regardless of
staleTime
.
Loader不会直接接收搜索参数。使用
loaderDeps
声明哪些搜索参数会影响缓存键:
tsx
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  validateSearch: (search) => ({
    offset: Number(search.offset) || 0,
    limit: Number(search.limit) || 10,
  }),
  loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
  loader: ({ deps: { offset, limit } }) => fetchPosts({ offset, limit }),
})
当依赖项变化时,无论
staleTime
设置如何,路由都会重新加载。

SWR Caching Configuration

SWR缓存配置

TanStack Router has built-in Stale-While-Revalidate caching keyed on the route's parsed pathname +
loaderDeps
.
Defaults:
  • staleTime
    : 0
    — data is always considered stale, reloads in background on re-match
  • preloadStaleTime
    : 30 seconds
    — preloaded data won't be refetched for 30s
  • gcTime
    : 30 minutes
    — unused cache entries garbage collected after 30min
tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  staleTime: 10_000, // 10s: data considered fresh for 10 seconds
  gcTime: 5 * 60 * 1000, // 5min: garbage collect after 5 minutes
})
Disable SWR caching entirely:
tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  staleTime: Infinity,
})
Globally:
tsx
const router = createRouter({
  routeTree,
  defaultStaleTime: Infinity,
})
TanStack Router内置了Stale-While-Revalidate缓存,缓存键由路由解析后的路径名 +
loaderDeps
组成。
默认值:
  • staleTime
    : 0
    — 数据始终被视为已过期,路由重新匹配时会在后台重新加载
  • preloadStaleTime
    : 30秒
    — 预加载数据在30秒内不会被重新请求
  • gcTime
    : 30分钟
    — 未使用的缓存条目会在30分钟后被垃圾回收
tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  staleTime: 10_000, // 10s: data considered fresh for 10 seconds
  gcTime: 5 * 60 * 1000, // 5min: garbage collect after 5 minutes
})
完全禁用SWR缓存:
tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  staleTime: Infinity,
})
全局配置:
tsx
const router = createRouter({
  routeTree,
  defaultStaleTime: Infinity,
})

Pending States (pendingComponent / pendingMs / pendingMinMs)

待加载状态(pendingComponent / pendingMs / pendingMinMs)

By default, a pending component shows after 1 second (
pendingMs: 1000
) and stays for at least 500ms (
pendingMinMs: 500
) to avoid flash.
tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  pendingMs: 500,
  pendingMinMs: 300,
  pendingComponent: () => <div>Loading posts...</div>,
  component: PostsComponent,
})
默认情况下,待加载组件会在1秒后显示(
pendingMs: 1000
),且至少显示500毫秒(
pendingMinMs: 500
)以避免闪烁。
tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  pendingMs: 500,
  pendingMinMs: 300,
  pendingComponent: () => <div>Loading posts...</div>,
  component: PostsComponent,
})

Router Context with createRootRouteWithContext (Factory Pattern)

使用createRootRouteWithContext实现路由上下文(工厂模式)

createRootRouteWithContext
is a factory that returns a function. You must call it twice — the first call passes the generic type, the second passes route options:
tsx
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'

interface MyRouterContext {
  auth: { userId: string }
  fetchPosts: () => Promise<Post[]>
}

// NOTE: double call — createRootRouteWithContext<Type>()({...})
export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: () => <Outlet />,
})
Supply the context when creating the router:
tsx
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

const router = createRouter({
  routeTree,
  context: {
    auth: { userId: '123' },
    fetchPosts,
  },
})
Consume in loaders and beforeLoad:
tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
To pass React hook values into the router context, call the hook above
RouterProvider
and inject via the
context
prop:
tsx
import { RouterProvider } from '@tanstack/react-router'

function InnerApp() {
  const auth = useAuth()
  return <RouterProvider router={router} context={{ auth }} />
}

function App() {
  return (
    <AuthProvider>
      <InnerApp />
    </AuthProvider>
  )
}
Route-level context via
beforeLoad
:
tsx
export const Route = createFileRoute('/posts')({
  beforeLoad: ({ context }) => ({
    fetchPosts: context.fetchPosts,
  }),
  loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
Keep the implementation SSR-safe when the router is used by TanStack Start. A relative
fetch('/api/posts')
works in a browser event handler, but Node and many server runtimes require an absolute URL during SSR. For app-internal data in Start, call a server function from the loader:
tsx
import { createServerFn } from '@tanstack/react-start'

const getPosts = createServerFn({ method: 'GET' }).handler(() => {
  return db.posts.findMany()
})

export const Route = createFileRoute('/posts')({
  loader: () => getPosts(),
})
Use a server route plus an origin-derived absolute URL only when the HTTP boundary itself is required. Do not hard-code the production origin.
createRootRouteWithContext
是一个返回函数的工厂函数。你必须调用两次 —— 第一次传入泛型类型,第二次传入路由配置:
tsx
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'

interface MyRouterContext {
  auth: { userId: string }
  fetchPosts: () => Promise<Post[]>
}

// NOTE: double call — createRootRouteWithContext<Type>()({...})
export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: () => <Outlet />,
})
创建路由时提供上下文:
tsx
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

const router = createRouter({
  routeTree,
  context: {
    auth: { userId: '123' },
    fetchPosts,
  },
})
在loader和beforeLoad中使用上下文:
tsx
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
要将React Hook的值传入路由上下文,在
RouterProvider
的上层调用Hook并通过
context
属性注入:
tsx
import { RouterProvider } from '@tanstack/react-router'

function InnerApp() {
  const auth = useAuth()
  return <RouterProvider router={router} context={{ auth }} />
}

function App() {
  return (
    <AuthProvider>
      <InnerApp />
    </AuthProvider>
  )
}
通过
beforeLoad
设置路由级上下文:
tsx
export const Route = createFileRoute('/posts')({
  beforeLoad: ({ context }) => ({
    fetchPosts: context.fetchPosts,
  }),
  loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
在TanStack Start中使用路由时,需确保实现符合SSR安全要求。相对路径的
fetch('/api/posts')
在浏览器事件处理器中可以正常工作,但Node及许多服务端运行时在SSR期间要求使用绝对URL。对于Start中的应用内部数据,在loader中调用服务端函数:
tsx
import { createServerFn } from '@tanstack/react-start'

const getPosts = createServerFn({ method: 'GET' }).handler(() => {
  return db.posts.findMany()
})

export const Route = createFileRoute('/posts')({
  loader: () => getPosts(),
})
仅当确实需要HTTP边界时,才使用服务端路由加基于origin的绝对URL。不要硬编码生产环境的origin。

Deferred Data Loading

延迟数据加载

Return unawaited promises from the loader for non-critical data. Use the
Await
component to render them:
tsx
import { createFileRoute, Await } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params: { postId } }) => {
    // Slow data — do NOT await
    const slowDataPromise = fetchComments(postId)
    // Fast data — await
    const post = await fetchPost(postId)

    return { post, deferredComments: slowDataPromise }
  },
  component: PostComponent,
})

function PostComponent() {
  const { post, deferredComments } = Route.useLoaderData()

  return (
    <div>
      <h1>{post.title}</h1>
      <Await
        promise={deferredComments}
        fallback={<div>Loading comments...</div>}
      >
        {(comments) => (
          <ul>
            {comments.map((c) => (
              <li key={c.id}>{c.body}</li>
            ))}
          </ul>
        )}
      </Await>
    </div>
  )
}
对于非关键数据,在loader中返回未等待的Promise。使用
Await
组件来渲染它们:
tsx
import { createFileRoute, Await } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params: { postId } }) => {
    // Slow data — do NOT await
    const slowDataPromise = fetchComments(postId)
    // Fast data — await
    const post = await fetchPost(postId)

    return { post, deferredComments: slowDataPromise }
  },
  component: PostComponent,
})

function PostComponent() {
  const { post, deferredComments } = Route.useLoaderData()

  return (
    <div>
      <h1>{post.title}</h1>
      <Await
        promise={deferredComments}
        fallback={<div>Loading comments...</div>}
      >
        {(comments) => (
          <ul>
            {comments.map((c) => (
              <li key={c.id}>{c.body}</li>
            ))}
          </ul>
        )}
      </Await>
    </div>
  )
}

Invalidation After Mutations

变更后的缓存失效

router.invalidate()
forces all active route loaders to re-run and marks all cached data as stale:
tsx
import { useRouter } from '@tanstack/react-router'

function AddPostButton() {
  const router = useRouter()

  const handleAdd = async () => {
    await createPost({ title: 'New post' })
    await router.invalidate({ sync: true })
  }

  return <button onClick={handleAdd}>Add Post</button>
}
Use
await router.invalidate({ sync: true })
when the next step requires refreshed loader data.
Treat the mutation and invalidation as one workflow. The mutation must persist before invalidation starts, and the loader must read from the same authoritative store. Verify create, update, and delete through the rendered route, including a fresh reload; local component state can hide a stale loader or non-persistent write.
router.invalidate()
会强制所有活跃路由的loader重新执行,并将所有缓存数据标记为已过期:
tsx
import { useRouter } from '@tanstack/react-router'

function AddPostButton() {
  const router = useRouter()

  const handleAdd = async () => {
    await createPost({ title: 'New post' })
    await router.invalidate({ sync: true })
  }

  return <button onClick={handleAdd}>Add Post</button>
}
当后续步骤需要使用刷新后的loader数据时,使用
await router.invalidate({ sync: true })
。
将变更和缓存失效视为一个工作流。变更必须在缓存失效开始前完成持久化,且loader必须从同一个权威数据源读取数据。通过渲染后的路由验证创建、更新和删除操作,包括一次全新的重载;本地组件状态可能会掩盖loader数据过期或写入未持久化的问题。

Error Handling

错误处理

tsx
import {
  createFileRoute,
  ErrorComponent,
  useRouter,
} from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error, reset }) => {
    const router = useRouter()

    if (error instanceof CustomError) {
      return <div>{error.message}</div>
    }

    return (
      <div>
        <ErrorComponent error={error} />
        <button
          onClick={() => {
            // For loader errors, invalidate to re-run loader + reset boundary
            router.invalidate()
          }}
        >
          Retry
        </button>
      </div>
    )
  },
})
tsx
import {
  createFileRoute,
  ErrorComponent,
  useRouter,
} from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error, reset }) => {
    const router = useRouter()

    if (error instanceof CustomError) {
      return <div>{error.message}</div>
    }

    return (
      <div>
        <ErrorComponent error={error} />
        <button
          onClick={() => {
            // For loader errors, invalidate to re-run loader + reset boundary
            router.invalidate()
          }}
        >
          Retry
        </button>
      </div>
    )
  },
})

Loader Parameters

Loader参数

The
loader
function receives:
  • params
    — parsed path params
  • deps
    — object from
    loaderDeps
  • context
    — merged parent + beforeLoad context
  • abortController
    — cancelled when route unloads or becomes stale
  • cause
    —
    'enter'
    ,
    'stay'
    , or
    'preload'
  • preload
    —
    true
    during preloading
  • location
    — current location object
  • parentMatchPromise
    — promise of parent route match
  • route
    — the route object itself
tsx
export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params: { postId }, abortController }) =>
    fetchPost(postId, { signal: abortController.signal }),
})
loader
函数接收以下参数:
  • params
    — 解析后的路径参数
  • deps
    — 来自
    loaderDeps
    的对象
  • context
    — 父路由与beforeLoad合并后的上下文
  • abortController
    — 路由卸载或过期时会取消请求
  • cause
    — 取值为
    'enter'
    、
    'stay'
    或
    'preload'
    ,表示加载原因
  • preload
    — 预加载时为
    true
  • location
    — 当前location对象
  • parentMatchPromise
    — 父路由匹配的Promise
  • route
    — 路由对象本身
tsx
export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params: { postId }, abortController }) =>
    fetchPost(postId, { signal: abortController.signal }),
})

Common Mistakes

常见错误

CRITICAL: Assuming loaders only run on the server

严重:误以为loader仅在服务端运行

TanStack Router is client-first. Loaders run on the client by default. They also run on the server when using TanStack Start for SSR, but the default mental model is client-side execution.
tsx
// WRONG — this will crash in the browser
export const Route = createFileRoute('/posts')({
  loader: async () => {
    const fs = await import('fs') // Node.js only!
    return JSON.parse(fs.readFileSync('...')) // fails in browser
  },
})

// CORRECT for an SPA — use a client-safe API helper
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
})
Do NOT put database queries, filesystem access, or server-only code directly in loaders. In TanStack Start, put that work in a server function and call the function from the loader. Do not use a relative
fetch('/api/...')
in an SSR loader.
TanStack Router是客户端优先的。Loader默认在客户端运行。在使用TanStack Start进行SSR时,它们也会在服务端运行,但默认的心智模型是客户端执行。
tsx
// WRONG — this will crash in the browser
export const Route = createFileRoute('/posts')({
  loader: async () => {
    const fs = await import('fs') // Node.js only!
    return JSON.parse(fs.readFileSync('...')) // fails in browser
  },
})

// CORRECT for an SPA — use a client-safe API helper
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
})
不要将数据库查询、文件系统访问或仅服务端可用的代码直接放在loader中。在TanStack Start中,将这些逻辑放在服务端函数中,再从loader调用该函数。不要在SSR loader中使用相对路径的
fetch('/api/...')
。

MEDIUM: Not understanding staleTime default is 0

中等:不了解staleTime默认值为0

Default
staleTime
is
0
. This means data reloads in the background on every route re-match. This is intentional — it ensures fresh data. But if your data is expensive or static, set
staleTime
:
tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  staleTime: 60_000, // Consider fresh for 1 minute
})
staleTime
的默认值为
0
。这意味着每次路由重新匹配时,数据都会在后台重新加载。这是有意设计的 —— 以确保数据新鲜。但如果你的数据请求成本高或属于静态数据,可以设置
staleTime
:
tsx
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  staleTime: 60_000, // Consider fresh for 1 minute
})

HIGH: Using reset() instead of router.invalidate() in error components

高危:在错误组件中使用reset()而非router.invalidate()

reset()
only resets the error boundary UI. It does NOT re-run the loader. For loader errors, use
router.invalidate()
which re-runs loaders and resets the boundary:
tsx
// WRONG — resets boundary but loader still has stale error
function PostErrorComponent({ error, reset }) {
  return <button onClick={reset}>Retry</button>
}

// CORRECT — re-runs loader and resets the error boundary
function PostErrorComponent({ error }) {
  const router = useRouter()
  return <button onClick={() => router.invalidate()}>Retry</button>
}
reset()
仅重置错误边界UI,不会重新执行loader。对于loader错误,使用
router.invalidate()
,它会重新执行loader并重置错误边界:
tsx
// WRONG — resets boundary but loader still has stale error
function PostErrorComponent({ error, reset }) {
  return <button onClick={reset}>Retry</button>
}

// CORRECT — re-runs loader and resets the error boundary
function PostErrorComponent({ error }) {
  const router = useRouter()
  return <button onClick={() => router.invalidate()}>Retry</button>
}

HIGH: Missing double parentheses on createRootRouteWithContext

高危:createRootRouteWithContext缺少双重括号调用

createRootRouteWithContext<Type>()
is a factory — it returns a function. Must call twice:
tsx
// WRONG — missing second call, passes options to the factory
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>({
  component: RootComponent,
})

// CORRECT — factory()({options})
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({
  component: RootComponent,
})
createRootRouteWithContext<Type>()
是一个工厂函数 —— 它返回一个函数,必须调用两次:
tsx
// WRONG — missing second call, passes options to the factory
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>({
  component: RootComponent,
})

// CORRECT — factory()({options})
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({
  component: RootComponent,
})

HIGH: Using React hooks in beforeLoad or loader

高危:在beforeLoad或loader中使用React Hook

beforeLoad
and
loader
are NOT React components. You cannot call hooks inside them. Use router context to inject values from hooks:
tsx
// WRONG — hooks cannot be called outside React components
export const Route = createFileRoute('/posts')({
  loader: () => {
    const auth = useAuth() // This will crash!
    return fetchPosts(auth.userId)
  },
})

// CORRECT — inject hook values via router context
// In your App component:
function InnerApp() {
  const auth = useAuth()
  return <RouterProvider router={router} context={{ auth }} />
}

// In your route:
export const Route = createFileRoute('/posts')({
  loader: ({ context: { auth } }) => fetchPosts(auth.userId),
})
beforeLoad
和
loader
不是React组件。你不能在其中调用Hook。使用路由上下文来注入Hook返回的值:
tsx
// WRONG — hooks cannot be called outside React components
export const Route = createFileRoute('/posts')({
  loader: () => {
    const auth = useAuth() // This will crash!
    return fetchPosts(auth.userId)
  },
})

// CORRECT — inject hook values via router context
// In your App component:
function InnerApp() {
  const auth = useAuth()
  return <RouterProvider router={router} context={{ auth }} />
}

// In your route:
export const Route = createFileRoute('/posts')({
  loader: ({ context: { auth } }) => fetchPosts(auth.userId),
})

HIGH: Property order affects TypeScript inference

高危:属性顺序影响TypeScript类型推断

Router infers types from earlier properties into later ones. Declaring
beforeLoad
after
loader
means context from
beforeLoad
is unknown in the loader:
tsx
// WRONG — context.user is unknown because beforeLoad declared after loader
export const Route = createFileRoute('/admin')({
  loader: ({ context }) => fetchData(context.user),
  beforeLoad: () => ({ user: getUser() }),
})

// CORRECT — validateSearch → loaderDeps → beforeLoad → loader
export const Route = createFileRoute('/admin')({
  beforeLoad: () => ({ user: getUser() }),
  loader: ({ context }) => fetchData(context.user),
})
路由会从前面的属性向后面的属性推断类型。将
beforeLoad
声明在
loader
之后,意味着loader中无法识别
beforeLoad
提供的上下文类型:
tsx
// WRONG — context.user is unknown because beforeLoad declared after loader
export const Route = createFileRoute('/admin')({
  loader: ({ context }) => fetchData(context.user),
  beforeLoad: () => ({ user: getUser() }),
})

// CORRECT — validateSearch → loaderDeps → beforeLoad → loader
export const Route = createFileRoute('/admin')({
  beforeLoad: () => ({ user: getUser() }),
  loader: ({ context }) => fetchData(context.user),
})

HIGH: Returning entire search object from loaderDeps

高危:从loaderDeps返回整个search对象

tsx
// WRONG — loader re-runs on ANY search param change
loaderDeps: ({ search }) => search

// CORRECT — only re-run when page changes
loaderDeps: ({ search }) => ({ page: search.page })
Returning the whole
search
object means unrelated param changes (e.g.,
sortDirection
,
viewMode
) trigger unnecessary reloads because deep equality fails on the entire object.
tsx
// WRONG — loader re-runs on ANY search param change
loaderDeps: ({ search }) => search

// CORRECT — only re-run when page changes
loaderDeps: ({ search }) => ({ page: search.page })
返回整个
search
对象意味着不相关的参数变化(例如
sortDirection
、
viewMode
)都会触发不必要的重新加载,因为对整个对象进行深相等比较会不通过。

Tensions

冲突与权衡

  • Client-first loaders vs SSR expectations: Loaders run on the client by default. When using SSR (TanStack Start), they run on both client and server. Browser-only APIs work by default but break under SSR. Server-only APIs (fs, db) break by default but work under Start server functions. See router-core/ssr/SKILL.md.
  • Built-in SWR cache vs external cache coordination: Router has built-in caching. When using TanStack Query, set
    defaultPreloadStaleTime: 0
    to avoid double-caching. See compositions/router-query/SKILL.md.

  • 客户端优先loader vs SSR预期:Loader默认在客户端运行。使用SSR(TanStack Start)时,它们会在客户端和服务端都运行。仅浏览器可用的API默认能正常工作,但在SSR下会报错。仅服务端可用的API(fs、db)默认会报错,但在Start的服务端函数中可以正常工作。参见 router-core/ssr/SKILL.md。
  • 内置SWR缓存 vs 外部缓存协调:路由内置了缓存。使用TanStack Query时,设置
    defaultPreloadStaleTime: 0
    以避免双重缓存。参见 compositions/router-query/SKILL.md。

Cross-References

相关参考

  • See also: router-core/search-params/SKILL.md —
    loaderDeps
    consumes validated search params as cache keys
  • See also: compositions/router-query/SKILL.md — for external cache coordination with TanStack Query
  • 另见:router-core/search-params/SKILL.md —
    loaderDeps
    使用经过验证的搜索参数作为缓存键
  • 另见:compositions/router-query/SKILL.md — 与TanStack Query的外部缓存协调