UNPKG

@tanstack/solid-router

Version:

Modern and scalable routing for Solid applications

498 lines (380 loc) 12.1 kB
--- name: solid-router description: >- Solid bindings for TanStack Router: RouterProvider, useRouter, useRouterState, useMatch, useMatches, useLocation, useSearch, useParams, useNavigate, useLoaderData, useLoaderDeps, useRouteContext, useBlocker, useCanGoBack, Link, Navigate, Outlet, CatchBoundary, ErrorComponent. Solid-specific patterns with Accessor<T> returns, createSignal/createMemo/createEffect, Show/Switch/Match/Dynamic, and @solidjs/meta for head management. type: framework library: tanstack-router library_version: '1.166.2' framework: solid requires: - router-core sources: - TanStack/router:packages/solid-router/src --- # Solid Router (`@tanstack/solid-router`) This skill builds on router-core. Read [router-core](../../../router-core/skills/router-core/SKILL.md) 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 ```bash npm install @tanstack/solid-router npm install -D @tanstack/router-plugin @tanstack/solid-router-devtools ``` ### 2. Configure Vite Plugin ```ts // 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 ```tsx // 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 ```tsx // 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 ```tsx // 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) ```tsx import { useRouter } from '@tanstack/solid-router' function InvalidateButton() { const router = useRouter() return <button onClick={() => router.invalidate()}>Refresh data</button> } ``` ### `useRouterState()` — returns `Accessor<T>` ```tsx 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) ```tsx 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>` ```tsx import { useSearch } from '@tanstack/solid-router' function Pagination() { const search = useSearch({ from: '/products' }) return <span>Page {search().page}</span> } ``` ### `useParams({ from })` — returns `Accessor<T>` ```tsx 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>` ```tsx 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>` ```tsx 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` ```tsx <RouterProvider router={router} /> ``` ### `Link` Type-safe navigation link. Children can be a function for active state: ```tsx ;<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: ```tsx function Layout() { return ( <div> <Sidebar /> <main> <Outlet /> </main> </div> ) } ``` ### `Navigate` Declarative redirect (triggers navigation in `onMount`): ```tsx import { Navigate } from '@tanstack/solid-router' function OldPage() { return <Navigate to="/new-page" /> } ``` ### `Await` Renders deferred data with Solid's `Suspense`: ```tsx 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`: ```tsx 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: ```tsx 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: ```tsx import { ScrollRestoration } from '@tanstack/solid-router' // In root route component ;<ScrollRestoration /> ``` ### `ClientOnly` Renders children only after hydration: ```tsx import { ClientOnly } from '@tanstack/solid-router' ;<ClientOnly fallback={<div>Loading...</div>}> <BrowserOnlyWidget /> </ClientOnly> ``` ### Head Management Uses `@solidjs/meta` under the hood: ```tsx 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` ```tsx 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 ```tsx 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 ```tsx 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. ```tsx // 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. ```tsx // 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](../../../router-core/skills/router-core/SKILL.md) — all sub-skills for domain-specific patterns (search params, data loading, navigation, auth, SSR, etc.)