solid-router について
`@tanstack/solid-router` を使った開発向けのスキルです。ファイルベースルーティング、Solid 固有のフックやコンポーネント、型とリアクティビティに関する注意点を解説します。
主な機能
- 01Vite によるファイルベースルーティング設定
- 02ルーターフックとコンポーネントの利用
- 03ナビゲーションとルートデータの処理
- 04Solid のリアクティブなアクセサーパターン
- 05一般的な設定・型の誤りの回避
活用シーン
- 01Solid アプリへの型安全なルート追加
- 02ナビゲーションとルート単位のデータ読み込み
- 03アクセサーやプラグイン設定の問題調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tanstack/ router --skill solid-routerインストール後、Claude Code で/solid-routerを入力して読み込まれたか確認します。
詳細
Solid Router(@tanstack/solid-router)
このスキルは router-core を基盤としています。基本概念については、最初に @tanstack/router-core#router-core をお読みください。
CRITICAL:TanStack Router の型は完全に推論されます。キャストしたり、推論された値に型注釈を付けたりしないでください。 CRITICAL:TanStack Router はクライアントファーストです。ローダーは既定でクライアント上で実行され、サーバー上では実行されません。 CRITICAL:ほとんどのフックは
Accessor<T>を返します。リアクティブな値を読み取るには、アクセサー(value())を呼び出す必要があります。これは React 版との最大の違いです。 CRITICAL:@tanstack/solid-routerと@solidjs/routerを混同しないでください。これらは API が異なる、まったく別のライブラリです。
完全なセットアップ:Vite を使ったファイルベースのルーティング
1. 依存関係をインストールする
npm install @tanstack/solid-router
npm install -D @tanstack/router-plugin @tanstack/solid-router-devtools
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',
autoCodeSplitting: true,
}),
solidPlugin(),
],
})
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. ルーターインスタンスを作成し、型を登録する
// 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')!,
)
フックのリファレンス
すべてのフックは @tanstack/solid-router からインポートします。ほとんどのフックは Accessor<T> を返します。値を読み取るには、結果を呼び出してください。
useRouter() — TRouter を返します(Accessor ではありません)
import { useRouter } from '@tanstack/solid-router'
function InvalidateButton() {
const router = useRouter()
return <button onClick={() => router.invalidate()}>Refresh data</button>
}
useRouterState() — Accessor<T> を返します
状態全体を公開するため、パフォーマンスコストが発生します。マッチやロケーションには、useMatches と useLocation を使用してください。
import { useRouterState } from '@tanstack/solid-router'
function LoadingIndicator() {
const isLoading = useRouterState({ select: (s) => s.isLoading })
return (
<Show when={isLoading()}>
<div>Loading...</div>
</Show>
)
}
useNavigate() — 関数を返します(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 }) — 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>
}
その他のフック
すべて @tanstack/solid-router からインポートします。
useMatches()—Accessor<Array<Match>>。アクティブなルートマッチすべてを返しますuseParentMatches()—Accessor<Array<Match>>。親ルートのマッチを返しますuseChildMatches()—Accessor<Array<Match>>。子ルートのマッチを返しますuseRouteContext({ from })—Accessor<T>。beforeLoadからのコンテキストを返しますuseLoaderDeps({ from })—Accessor<T>。ローダーの依存値を返しますuseBlocker({ shouldBlockFn })— 未保存の変更がある場合にナビゲーションをブロックしますuseCanGoBack()—Accessor<boolean>useLocation()—Accessor<ParsedLocation>useLinkProps({ to, params?, search? })—ComponentProps<'a'>を返します(Accessor ではありません)useMatchRoute()— 関数を返します。呼び出すとAccessor<false | Params>を返しますuseHydrated()—Accessor<boolean>
コンポーネントのリファレンス
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>
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 を使って遅延データをレンダリングします。
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— 宣言的なナビゲーションブロッカーです。カスタム UI にはshouldBlockFnとwithResolverを使用しますScrollRestoration— 非推奨です。代わりにcreateRouterのscrollRestoration: trueオプションを使用してくださいClientOnly— ハイドレーション後にのみ子要素をレンダリングし、fallbackプロパティを受け取ります
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>
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 の管理
内部では @solidjs/meta を使用します。
import { HeadContent, Scripts } from '@tanstack/solid-router'
function RootDocument(props) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{props.children}
<Scripts />
</body>
</html>
)
}
Solid 固有のパターン
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>
)
}
ルーターの状態で Solid Primitives を使う
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>
)
}
ルーターコンテキストを使った認証
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' })
}
}
よくある間違い
1. 重要:Accessor の呼び出し忘れ
フックは Accessor<T> を返すため、値を読み取るには呼び出す必要があります。これは 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. 高:リアクティブな値の分割代入
分割代入すると、Solidのリアクティビティの追跡が中断されます。
// WRONG — loses reactivity
const { page } = useSearch({ from: '/products' })()
// CORRECT — access through accessor
const search = useSearch({ from: '/products' })
<span>Page {search().page}</span>
3. 高:beforeLoad または loader でのReactフックの使用
beforeLoad と loader はコンポーネントではなく、通常の非同期関数です。ReactまたはSolidのフックは使用できません。代わりに、ルーターのコンテキスト経由で状態を渡してください。
4. 中:プラグインのターゲットの誤り
ルータープラグインの設定で target: 'solid' を指定する必要があります。デフォルトは 'react' です。
相互参照
@tanstack/router-core#router-core— ドメイン固有のパターンに関するすべてのサブスキル(検索パラメーター、データ読み込み、ナビゲーション、認証、SSRなど)