@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 原则
阅读任何代码之前,先建立三个心智模型:
- 类型完全推断(FULLY INFERRED):TanStack Router 的类型系统基于路由树自动推导,禁止手写 cast 或给推断值加类型注解,否则会破坏类型安全链路。
- 客户端优先(CLIENT-FIRST):loader 默认在客户端运行,而非服务端。这是与 Next.js 等 SSR 框架的核心差异,SSR 场景需显式使用 packages/solid-router/src/ssr/ 下的服务端渲染通道。
- 绝大多数 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.ts。target: '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接口声明是整个类型安全的开关:没有它,Link、useNavigate、useSearch等全部退化为string与unknown。createRouter在源码层面继承自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 })() |
| 其余大部分 Hooks | Accessor<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 时优先用useMatches、useLocation。务必传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> }源码验证:useSearch、useParams、useLoaderData三个 Hook 在实现上都委托给useMatch(分别见 useSearch.tsx、useParams.tsx、useLoaderData.tsx),而useMatch最终用Solid.createMemo包装并对select结果做replaceEqualDeep去重(useMatch.tsx)。这意味着这些 Hook 天然具备 Solid 的细粒度响应式:值变化时只触发真正依赖它的计算。
其他 Hooks 一览
useMatches()—Accessor<Array<Match>>,全部活跃路由 matchuseParentMatches()—Accessor<Array<Match>>,父级 matchuseChildMatches()—Accessor<Array<Match>>,子级 matchuseRouteContext({ 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(如activeProps、preload、preloadDelay、resetScroll、viewTransition等),并接入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— 声明式导航拦截器,配合shouldBlockFn与withResolver实现自定义确认 UIScrollRestoration—已废弃,改用createRouter的scrollRestoration: 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分支携带current、next两个完整位置对象(含routeId、fullPath、params、search)以及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> ) }createLink与useLinkProps、linkOptions一同从 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 | 忘记调用 Accessor | HIGH | 始终value()取值 |
| 2 | 解构响应式值 | HIGH | 通过 Accessor 读取 |
| 3 | 在 beforeLoad/loader 中使用 hooks | HIGH | 改用 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
beforeLoad与loader不是组件,而是普通异步函数,React 或 Solid 的 hooks 都不能在其中使用。需要共享状态(如用户信息)时,通过createRouter的context传入,再在beforeLoad: ({ context })中读取。
4. 插件 target 配置错误
tanstackRouter()的target默认是'react',Solid 项目必须显式写成'solid',否则生成的路由类型与组件绑定不匹配。
与核心包的衔接
@tanstack/solid-router是薄绑定层:所有路由核心逻辑(匹配、导航、loader、类型推导)都来自@tanstack/router-core(RouterCore基类与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),仅供参考