solid-router
frameworksolid>-
Solid Router (@tanstack/solid-router)
This skill builds on router-core. Read router-core first for foundational concepts.
This skill covers the Solid-specific bindings, components, hooks, and setup for TanStack Router.
CRITICAL: TanStack Router types are FULLY INFERRED. Never cast, never annotate inferred values. CRITICAL: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, not on the server. CRITICAL: Most hooks return Accessor<T> — you MUST call the accessor (value()) to read the reactive value. This is the #1 difference from the React version. CRITICAL: Do not confuse @tanstack/solid-router with @solidjs/router. They are completely different libraries with different APIs.
Full Setup: File-Based Routing with Vite
1. Install Dependencies
npm install @tanstack/solid-router
npm install -D @tanstack/router-plugin @tanstack/solid-router-devtools
2. Configure Vite Plugin
// 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',
autoCodeSplitting: true,
}),
solidPlugin(),
],
})
3. Create Root Route
// 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. Create Route Files
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/')({
component: HomePage,
})
function HomePage() {
return <h1>Welcome Home</h1>
}
5. Create Router Instance and Register Types
// 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')!,
)
Hooks Reference
All hooks imported from @tanstack/solid-router. Most return Accessor<T> — call the result to read the value.
useRouter() — returns TRouter (NOT an Accessor)
import { useRouter } from '@tanstack/solid-router'
function InvalidateButton() {
const router = useRouter()
return <button onClick={() => router.invalidate()}>Refresh data</button>
}
useRouterState() — returns Accessor<T>
import { useRouterState } from '@tanstack/solid-router'
function LoadingIndicator() {
const isLoading = useRouterState({ select: (s) => s.isLoading })
return (
<Show when={isLoading()}>
<div>Loading...</div>
</Show>
)
}
useNavigate() — returns a function (NOT an Accessor)
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 }) — returns Accessor<T>
import { useSearch } from '@tanstack/solid-router'
function Pagination() {
const search = useSearch({ from: '/products' })
return <span>Page {search().page}</span>
}
useParams({ from }) — returns Accessor<T>
import { useParams } from '@tanstack/solid-router'
function PostHeader() {
const params = useParams({ from: '/posts/$postId' })
return <h2>Post {params().postId}</h2>
}
useLoaderData({ from }) — returns Accessor<T>
import { useLoaderData } from '@tanstack/solid-router'
function PostContent() {
const data = useLoaderData({ from: '/posts/$postId' })
return <article>{data().post.content}</article>
}
useMatch({ from }) — returns Accessor<T>
import { useMatch } from '@tanstack/solid-router'
function PostDetails() {
const match = useMatch({ from: '/posts/$postId' })
return <div>{match().loaderData.post.title}</div>
}
Other Hooks
All imported from @tanstack/solid-router:
- useMatches() — Accessor<Array<Match>>, all active route matches
- useParentMatches() — Accessor<Array<Match>>, parent route matches
- useChildMatches() — Accessor<Array<Match>>, child route matches
- useRouteContext({ from }) — Accessor<T>, context from beforeLoad
- useLoaderDeps({ from }) — Accessor<T>, loader dependency values
- useBlocker({ shouldBlockFn }) — blocks navigation for unsaved changes
- useCanGoBack() — Accessor<boolean>
- useLocation() — Accessor<ParsedLocation>
- useLinkProps({ to, params?, search? }) — returns ComponentProps<'a'> (NOT an Accessor)
- useMatchRoute() — returns a function; calling it returns Accessor<false | Params>
- useHydrated() — Accessor<boolean>
Components Reference
RouterProvider
<RouterProvider router={router} />
Link
Type-safe navigation link. Children can be a function for active state:
;<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>
Outlet
Renders the matched child route component:
function Layout() {
return (
<div>
<Sidebar />
<main>
<Outlet />
</main>
</div>
)
}
Navigate
Declarative redirect (triggers navigation in onMount):
import { Navigate } from '@tanstack/solid-router'
function OldPage() {
return <Navigate to="/new-page" />
}
Await
Renders deferred data with Solid's Suspense:
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
Error boundary wrapping Solid.ErrorBoundary:
import { CatchBoundary } from '@tanstack/solid-router'
;<CatchBoundary
getResetKey={() => 'widget'}
errorComponent={({ error }) => <div>Error: {error.message}</div>}
>
<RiskyWidget />
</CatchBoundary>
Other Components
- CatchNotFound — catches notFound() errors in children; fallback receives the error data
- Block — declarative navigation blocker; use shouldBlockFn and withResolver for custom UI
- ScrollRestoration — deprecated; use createRouter's scrollRestoration: true option instead
- ClientOnly — renders children only after hydration; accepts fallback prop
Block
Declarative navigation blocker component:
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>
ScrollRestoration
Restores scroll position on navigation:
import { ScrollRestoration } from '@tanstack/solid-router'
// In root route component
;<ScrollRestoration />
ClientOnly
Renders children only after hydration:
import { ClientOnly } from '@tanstack/solid-router'
;<ClientOnly fallback={<div>Loading...</div>}>
<BrowserOnlyWidget />
</ClientOnly>
Head Management
Uses @solidjs/meta under the hood:
import { HeadContent, Scripts } from '@tanstack/solid-router'
function RootDocument(props) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{props.children}
<Scripts />
</body>
</html>
)
}
Solid-Specific Patterns
Custom Link Component with 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>
)
}
Using Solid Primitives with Router State
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>
)
}
Auth with 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' })
}
}
Common Mistakes
1. CRITICAL: Forgetting to call Accessor
Hooks return Accessor<T> — you must call them to read the value. This is the #1 migration issue from React.
// 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. HIGH: Destructuring reactive values
Destructuring breaks Solid's reactivity tracking.
// WRONG — loses reactivity
const { page } = useSearch({ from: '/products' })()
// CORRECT — access through accessor
const search = useSearch({ from: '/products' })
<span>Page {search().page}</span>
3. HIGH: Using React hooks in beforeLoad or loader
beforeLoad and loader are NOT components — they are plain async functions. No hooks (React or Solid) can be used in them. Pass state via router context instead.
4. MEDIUM: Wrong plugin target
Must set target: 'solid' in the router plugin config. Default is 'react'.
Cross-References
- router-core/SKILL.md — all sub-skills for domain-specific patterns (search params, data loading, navigation, auth, SSR, etc.)