05. Data Loading:loader 生命周期

TanStack Router 把“路由将要去哪”作为数据加载入口。它比组件内 useEffect 更早,比随手 fetch 更可追踪。

生命周期

URL change → route matching top-down → params.parse → validateSearch → beforeLoad serial → loaderDeps → loader parallel → pending/error/component

基础 loader

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

function PostsPage() {
  const posts = Route.useLoaderData()
  return <PostList posts={posts} />
}

loaderDeps:缓存 key 的声明

loaderDeps 告诉 Router:这个 loader 的结果依赖哪些 search 参数。依赖变了,缓存 key 变,loader 重新跑。

export const Route = createFileRoute('/posts')({
  validateSearch: z.object({ page: z.number().catch(1), q: z.string().catch('') }),
  loaderDeps: ({ search }) => ({ page: search.page, q: search.q }),
  loader: ({ deps }) => fetchPosts(deps),
})
常见错误:loaderDeps: ({ search }) => search。如果 loader 只用 page,却把 view、panel、debug 等无关 search 也放进依赖,任何 UI 状态变化都会导致数据重载。缓存 key 不是垃圾桶,第二次提醒,说明人类真的很爱垃圾桶。

beforeLoad:守卫与 context

export const Route = createFileRoute('/dashboard')({
  beforeLoad: async ({ context }) => {
    const user = await context.auth.getUser()
    if (!user) throw redirect({ to: '/login' })
    return { user }
  },
})

父 route 的 context 可以被子 route 继承,用于 auth、feature flags、queryClient、tenant 等。

pending/error

TanStack Router 有 pendingComponent、errorComponent、onError/onCatch。和 Next 的 loading.tsx/error.tsx 类似,但更贴近 route option,而不是纯文件约定。

快速判断

loaderDeps 应包含什么?