@tanstack/solid-router 实战指南:Accessor 响应式路由与 Solid 绑定全解析
2026/9/16 22:16:45 网站建设 项目流程

@tanstack/solid-router 实战指南:Accessor 响应式路由与 Solid 绑定全解析

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

导读

本文围绕 @tanstack/solid-router 的 Solid 绑定技能展开,系统讲解如何将 TanStack Router 以Accessor<T> 响应式返回值、Solid 原生原语(createSignal / createMemo / Show / For 等)、createLink组件工厂与@solidjs/meta头部管理的方式接入 Solid 应用。读者学完后将能完成从 Vite 文件路由脚手架搭建、全套 Hooks/Components 使用,到避开「忘记调用 Accessor」「在 loader 中用 hooks」等高频误区的完整实战闭环。

本仓库对应的技能元数据见 packages/solid-router/skills/_artifacts/skill_spec.md(版本 1.166.2 已审查),完整技能正文见 packages/solid-router/skills/solid-router/SKILL.md。

三条必须牢记的 CRITICAL 原则

阅读任何代码之前,先建立三个心智模型:

  1. 类型完全推断(FULLY INFERRED):TanStack Router 的类型系统基于路由树自动推导,禁止手写 cast 或给推断值加类型注解,否则会破坏类型安全链路。
  2. 客户端优先(CLIENT-FIRST):loader 默认在客户端运行,而非服务端。这是与 Next.js 等 SSR 框架的核心差异,SSR 场景需显式使用 packages/solid-router/src/ssr/ 下的服务端渲染通道。
  3. 绝大多数 Hooks 返回Accessor<T>:必须先调用(value())才能读到响应式值。这是与 React 版本最大的区别,也是下文所有示例中反复出现的()调用的根源。

另外务必区分两个同名库:@tanstack/solid-router@solidjs/router是完全不同的项目、完全不同的 API,切勿混用。

完整搭建:基于 Vite 的文件路由

1. 安装依赖

npm install @tanstack/solid-router npm install -D @tanstack/router-plugin @tanstack/solid-router-devtools

从仓库 package.json 可以看到运行时依赖仅四个:@solidjs/meta(头部管理)、@tanstack/router-core(核心路由逻辑)、@tanstack/history(历史栈)、@solid-primitives/refs,peer 依赖为solid-js ^1.9.10,Node 要求>=20.19

2. 配置 Vite 插件

// vite.config.ts import { defineConfig } from 'vite' import solidPlugin from 'vite-plugin-solid' import { tanstackRouter } from '@tanstack/router-plugin/vite' export default defineConfig({ plugins: [ // MUST come before solid plugin tanstackRouter({ target: 'solid', // 默认是 'react',Solid 项目必须显式指定 autoCodeSplitting: true, }), solidPlugin(), ], })

插件负责扫描src/routes生成routeTree.gen.tstarget: 'solid'是高频失误点——默认值是'react',漏配会导致生成的文件路由类型与 Solid 绑定不匹配。

3. 创建根路由

// src/routes/__root.tsx import { createRootRoute, Link, Outlet } from '@tanstack/solid-router' export const Route = createRootRoute({ component: RootLayout, }) function RootLayout() { return ( <> <nav> <Link to="/" activeClass="font-bold">Home</Link> <Link to="/about" activeClass="font-bold">About</Link> </nav> <hr /> <Outlet /> </> ) }

4. 创建路由文件

// src/routes/index.tsx import { createFileRoute } from '@tanstack/solid-router' export const Route = createFileRoute('/')({ component: HomePage, }) function HomePage() { return <h1>Welcome Home</h1> }

5. 创建 Router 实例并注册类型

// src/main.tsx import { render } from 'solid-js/web' import { RouterProvider, createRouter } from '@tanstack/solid-router' import { routeTree } from './routeTree.gen' const router = createRouter({ routeTree }) // REQUIRED — without this, Link/useNavigate/useSearch have no type safety declare module '@tanstack/solid-router' { interface Register { router: typeof router } } render( () => <RouterProvider router={router} />, document.getElementById('root')!, )

Register接口声明是整个类型安全的开关:没有它,LinkuseNavigateuseSearch等全部退化为stringunknowncreateRouter在源码层面继承自RouterCore,并将 Solid 的 store 工厂注入其中(见 packages/solid-router/src/router.ts),所有 Hooks 都通过该 store 读取路由状态。

Hooks 参考:分清「Accessor」与「函数」

所有 Hooks 均从@tanstack/solid-router导入。下表是返回类型的快速判定:

Hook返回类型读取方式
useRouter()路由器实例(非 Accessor)router.invalidate()
useNavigate()导航函数(非 Accessor)navigate({ to })
useLinkProps()ComponentProps<'a'>(非 Accessor)直接展开到<a>
useMatchRoute()函数,调用后返回Accessor<false \| Params>matchRoute({ to })()
其余大部分 HooksAccessor<T>value()

useRouter()— 返回路由器实例

import { useRouter } from '@tanstack/solid-router' function InvalidateButton() { const router = useRouter() return <button onClick={() => router.invalidate()}>Refresh data</button> }

useRouterState()— 返回Accessor<T>

它暴露整个路由状态,因此有性能成本;需要 matches 或 location 时优先用useMatchesuseLocation。务必传select缩小订阅范围:

import { useRouterState } from '@tanstack/solid-router' function LoadingIndicator() { const isLoading = useRouterState({ select: (s) => s.isLoading }) return ( <Show when={isLoading()}> <div>Loading...</div> </Show> ) }

从源码看,useRouterState在客户端通过createMemo+replaceEqualDeep实现细粒度更新——只有select结果真正变化时下游才会重渲染(packages/solid-router/src/useRouterState.tsx);同时 SSR 分支做了特殊处理,只渲染一次不订阅 store。

useNavigate()— 返回导航函数

import { useNavigate } from '@tanstack/solid-router' function AfterSubmit() { const navigate = useNavigate() const handleSubmit = async () => { await saveData() navigate({ to: '/posts/$postId', params: { postId: '123' } }) } return <button onClick={handleSubmit}>Save</button> }

useSearch({ from })— 返回Accessor<T>

import { useSearch } from '@tanstack/solid-router' function Pagination() { const search = useSearch({ from: '/products' }) return <span>Page {search().page}</span> }

useParams({ from })— 返回Accessor<T>

import { useParams } from '@tanstack/solid-router' function PostHeader() { const params = useParams({ from: '/posts/$postId' }) return <h2>Post {params().postId}</h2> }

useLoaderData({ from })— 返回Accessor<T>

import { useLoaderData } from '@tanstack/solid-router' function PostContent() { const data = useLoaderData({ from: '/posts/$postId' }) return <article>{data().post.content}</article> }

useMatch({ from })— 返回Accessor<T>

import { useMatch } from '@tanstack/solid-router' function PostDetails() { const match = useMatch({ from: '/posts/$postId' }) return <div>{match().loaderData.post.title}</div> }

源码验证useSearchuseParamsuseLoaderData三个 Hook 在实现上都委托给useMatch(分别见 useSearch.tsx、useParams.tsx、useLoaderData.tsx),而useMatch最终用Solid.createMemo包装并对select结果做replaceEqualDeep去重(useMatch.tsx)。这意味着这些 Hook 天然具备 Solid 的细粒度响应式:值变化时只触发真正依赖它的计算。

其他 Hooks 一览

  • useMatches()Accessor<Array<Match>>,全部活跃路由 match
  • useParentMatches()Accessor<Array<Match>>,父级 match
  • useChildMatches()Accessor<Array<Match>>,子级 match
  • useRouteContext({ from })Accessor<T>,读取beforeLoad写入的上下文
  • useLoaderDeps({ from })Accessor<T>,loader 依赖值(用于 loader 缓存失效判定)
  • useBlocker({ shouldBlockFn })— 拦截导航,保护未保存的修改
  • useCanGoBack()Accessor<boolean>
  • useLocation()Accessor<ParsedLocation>
  • useMatchRoute()— 返回函数,调用后返回Accessor<false | Params>,用于「当前是否命中某路由」判断
  • useHydrated()Accessor<boolean>,水合完成标记

Components 参考

RouterProvider

<RouterProvider router={router} />

Link

类型安全导航链接,子节点可以是函数以响应激活态:

<Link to="/posts/$postId" params={{ postId: '42' }}> View Post </Link> {/* Function children for active state */} <Link to="/about"> {(state) => <span classList={{ active: state.isActive }}>About</span>} </Link>

Link底层由useLinkProps支撑,内部用Solid.splitProps拆分发散 props(如activePropspreloadpreloadDelayresetScrollviewTransition等),并接入useHydrated与 IntersectionObserver 实现预加载(packages/solid-router/src/link.tsx)。

Outlet

渲染匹配到的子路由组件:

function Layout() { return ( <div> <Sidebar /> <main> <Outlet /> </main> </div> ) }

Navigate

声明式重定向(在onMount中触发导航):

import { Navigate } from '@tanstack/solid-router' function OldPage() { return <Navigate to="/new-page" /> }

Await

结合 Solid 的Suspense渲染 deferred 数据:

import { Await } from '@tanstack/solid-router' import { Suspense } from 'solid-js' function PostWithComments() { const data = Route.useLoaderData() return ( <Suspense fallback={<div>Loading...</div>}> <Await promise={data().deferredComments}> {(comments) => <For each={comments}>{(c) => <li>{c.text}</li>}</For>} </Await> </Suspense> ) }

CatchBoundary

包装Solid.ErrorBoundary的错误边界:

import { CatchBoundary } from '@tanstack/solid-router' <CatchBoundary getResetKey={() => 'widget'} errorComponent={({ error }) => <div>Error: {error.message}</div>} > <RiskyWidget /> </CatchBoundary>

其他组件

  • CatchNotFound— 捕获子级notFound()错误;fallback接收错误数据
  • Block— 声明式导航拦截器,配合shouldBlockFnwithResolver实现自定义确认 UI
  • ScrollRestoration已废弃,改用createRouterscrollRestoration: true选项
  • ClientOnly— 水合后才渲染子级,接受fallbackprop

Block详解

import { Block } from '@tanstack/solid-router' <Block shouldBlockFn={() => formIsDirty()} withResolver> {({ status, proceed, reset }) => ( <Show when={status === 'blocked'}> <div> <p>Are you sure?</p> <button onClick={proceed}>Yes</button> <button onClick={reset}>No</button> </div> </Show> )} </Block>

源码中Block的 resolver 状态是'blocked' | 'idle'的判别联合,blocked分支携带currentnext两个完整位置对象(含routeIdfullPathparamssearch)以及proceed/reset(packages/solid-router/src/useBlocker.tsx),因此确认弹窗可以展示「从哪里跳到哪里」的完整上下文。

ScrollRestoration(旧方案)

import { ScrollRestoration } from '@tanstack/solid-router' // In root route component <ScrollRestoration />

ClientOnly

import { ClientOnly } from '@tanstack/solid-router' <ClientOnly fallback={<div>Loading...</div>}> <BrowserOnlyWidget /> </ClientOnly>

头部管理(Head Management)

底层使用@solidjs/meta(见 package.json 的 dependencies):

import { HeadContent, Scripts } from '@tanstack/solid-router' function RootDocument(props) { return ( <html> <head> <HeadContent /> </head> <body> {props.children} <Scripts /> </body> </html> ) }

Solid 专属模式(Solid-Specific Patterns)

createLink定制链接组件

import { createLink } from '@tanstack/solid-router' const StyledLinkComponent = (props) => ( <a {...props} class={`styled-link ${props.class ?? ''}`} /> ) const StyledLink = createLink(StyledLinkComponent) function Nav() { return ( <StyledLink to="/posts/$postId" params={{ postId: '42' }}> Post </StyledLink> ) }

createLinkuseLinkPropslinkOptions一同从 packages/solid-router/src/link.tsx 导出,它把 TanStack Router 的类型安全to/params/search全部注入自定义组件,同时保留原有激活态逻辑。

用 Solid 原语消费路由状态

import { createMemo, Show, For } from 'solid-js' import { useRouterState } from '@tanstack/solid-router' function Breadcrumbs() { const matches = useRouterState({ select: (s) => s.matches }) const crumbs = createMemo(() => matches().filter((m) => m.context?.breadcrumb), ) return ( <nav> <For each={crumbs()}> {(match) => <span>{match.context.breadcrumb}</span>} </For> </nav> ) }

基于 Router Context 的认证

import { createRootRouteWithContext } from '@tanstack/solid-router' const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({ component: RootComponent, }) // In main.tsx — provide context at router creation const router = createRouter({ routeTree, context: { auth: authState }, }) // In a route — access via beforeLoad (NOT hooks) beforeLoad: ({ context }) => { if (!context.auth.isAuthenticated) { throw redirect({ to: '/login' }) } }

注意:认证状态通过路由创建时的 context注入,而不是在组件里用 hooks 读取——因为beforeLoad/loader是普通异步函数,运行在组件树之外。

常见错误(Failure Modes)对照表

技能规范 skill_spec.md 明确登记了本技能的 2 个失败模式,扩展后共有 4 条高频陷阱:

#错误优先级修正
1忘记调用 AccessorHIGH始终value()取值
2解构响应式值HIGH通过 Accessor 读取
3在 beforeLoad/loader 中使用 hooksHIGH改用 router context 传参
4插件target配错MEDIUM显式设置target: 'solid'

1. 忘记调用 Accessor(最高频)

Hooks 返回Accessor<T>,必须调用才能读值——这是从 React 迁移到 Solid 的第一大坑:

// WRONG — comparing the accessor function, not its value const params = useParams({ from: '/posts/$postId' }) if (params.postId === '42') { ... } // params is a function! // CORRECT — call the accessor const params = useParams({ from: '/posts/$postId' }) if (params().postId === '42') { ... }

2. 解构响应式值(破坏响应性)

// WRONG — loses reactivity const { page } = useSearch({ from: '/products' })() // CORRECT — access through accessor const search = useSearch({ from: '/products' }) <span>Page {search().page}</span>

解构会在读取瞬间把值「冻结」下来,Solid 的依赖追踪随之失效,页面将显示陈旧数据。这也是domain_map.yaml(packages/solid-router/skills/_artifacts/domain_map.yaml)中「Unwrapping accessors incorrectly」失败模式的底层机制描述。

3. 在 beforeLoad / loader 中使用 hooks

beforeLoadloader不是组件,而是普通异步函数,React 或 Solid 的 hooks 都不能在其中使用。需要共享状态(如用户信息)时,通过createRoutercontext传入,再在beforeLoad: ({ context })中读取。

4. 插件 target 配置错误

tanstackRouter()target默认是'react',Solid 项目必须显式写成'solid',否则生成的路由类型与组件绑定不匹配。

与核心包的衔接

@tanstack/solid-router是薄绑定层:所有路由核心逻辑(匹配、导航、loader、类型推导)都来自@tanstack/router-coreRouterCore基类与routerStores注入,见 packages/solid-router/src/router.ts),历史管理来自@tanstack/history,导出清单可从 packages/solid-router/src/index.tsx 全量核对。

更底层的路由模式(search params 校验、数据加载、导航、认证、SSR 等)在 router-core 技能 中展开——建议按 SKILL.md 的指引先读 router-core 再回来消化本文的 Solid 专属部分,二者配合即可覆盖从类型系统到组件层的完整知识链。

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询