data-loading について
TanStack Router でデータを扱う開発者向けのルートローダーのスキルです。ローダーのライフサイクル、キャッシュ、待機状態、エラー処理に加え、SSR の境界や変更後のデータ更新も説明します。
主な機能
- 01ルートローダーのライフサイクルと引数
- 02検索パラメーターによるキャッシュキー
- 03Stale-While-Revalidate の設定
- 04待機中表示と遅延データのパターン
- 05コンテキストとエラー処理
- 06変更後のルート無効化
活用シーン
- 01パスと検索パラメーターに応じたデータ読み込み
- 02ローダー結果の鮮度と再利用の調整
- 03ルートデータの読み込み中に状態を表示
- 04作成・更新・削除後のルートデータ更新
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tanstack/ router --skill data-loadingインストール後、Claude Code で/data-loadingを入力して読み込まれたか確認します。
詳細
データの読み込み
セットアップ
データを返す基本的なローダー。useLoaderData から利用します。
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
component: PostsComponent,
})
function PostsComponent() {
const posts = Route.useLoaderData()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
コード分割されたコンポーネントでは、Route をインポートする代わりに getRouteApi を使います。
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/posts')
function PostsComponent() {
const posts = routeApi.useLoaderData()
return <ul>{/* ... */}</ul>
}
ルートの読み込みライフサイクル
ルーターは、URL または履歴が更新されるたびに、次のシーケンスを実行します。
- ルートのマッチング(上から下へ)
route.params.parseroute.validateSearch
- ルートの事前読み込み(順次)
route.beforeLoadroute.onError→route.errorComponent
- ルートの読み込み(並列)
route.component.preload?route.loaderroute.pendingComponent(任意)route.component
route.onError→route.errorComponent
重要:beforeLoad は loader より先に実行されます。親の beforeLoad は、子の beforeLoad より先に実行されます。beforeLoad で例外をスローすると、すべての子の読み込みが中止されます。
基本的なパターン
検索パラメーターに応じたキャッシュキーのための loaderDeps
ローダーには検索パラメーターが直接渡されません。loaderDeps を使って、キャッシュキーに影響する検索パラメーターを宣言します。
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
validateSearch: (search) => ({
offset: Number(search.offset) || 0,
limit: Number(search.limit) || 10,
}),
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps: { offset, limit } }) => fetchPosts({ offset, limit }),
})
依存関係が変わると、staleTime にかかわらずルートが再読み込みされます。
SWR キャッシュの設定
TanStack Router には、ルートの解析済みパス名と loaderDeps をキーとする、組み込みの Stale-While-Revalidate キャッシュがあります。
デフォルト:
staleTime: 0 — データは常に古いとみなされ、再マッチ時にバックグラウンドで再読み込みされますpreloadStaleTime: 30 seconds — 事前読み込みされたデータは 30 秒間、再取得されませんgcTime: 30 minutes — 使用されていないキャッシュエントリーは 30 分後にガベージコレクションされます
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: 10_000, // 10s: data considered fresh for 10 seconds
gcTime: 5 * 60 * 1000, // 5min: garbage collect after 5 minutes
})
SWR キャッシュを完全に無効にします。
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: Infinity,
})
グローバルに設定する場合:
const router = createRouter({
routeTree,
defaultStaleTime: Infinity,
})
保留状態(pendingComponent / pendingMs / pendingMinMs)
デフォルトでは、保留コンポーネントは 1 秒後(pendingMs: 1000)に表示され、ちらつきを防ぐため少なくとも 500 ミリ秒(pendingMinMs: 500)表示されます。
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
pendingMs: 500,
pendingMinMs: 300,
pendingComponent: () => <div>Loading posts...</div>,
component: PostsComponent,
})
createRootRouteWithContext によるルーターコンテキスト(ファクトリーパターン)
createRootRouteWithContext は関数を返すファクトリーです。2 回呼び出す必要があります。1 回目はジェネリック型を渡し、2 回目はルートオプションを渡します。
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
interface MyRouterContext {
auth: { userId: string }
fetchPosts: () => Promise<Post[]>
}
// NOTE: double call — createRootRouteWithContext<Type>()({...})
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => <Outlet />,
})
ルーターの作成時にコンテキストを渡します。
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
context: {
auth: { userId: '123' },
fetchPosts,
},
})
ローダーと beforeLoad で利用します。
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
React フックの値をルーターコンテキストに渡すには、RouterProvider の上位でフックを呼び出し、context プロップを介して注入します。
import { RouterProvider } from '@tanstack/react-router'
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
)
}
beforeLoad を介したルートレベルのコンテキスト:
export const Route = createFileRoute('/posts')({
beforeLoad: ({ context }) => ({
fetchPosts: context.fetchPosts,
}),
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
TanStack Start でルーターを使う場合は、SSR セーフな実装にしてください。ブラウザーのイベントハンドラーでは相対 URL の fetch('/api/posts') を使えますが、Node や多くのサーバーランタイムでは、SSR 中に絶対 URL が必要です。Start でアプリ内部のデータを扱う場合は、ローダーからサーバー関数を呼び出します。
import { createServerFn } from '@tanstack/react-start'
const getPosts = createServerFn({ method: 'GET' }).handler(() => {
return db.posts.findMany()
})
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
})
HTTP 境界そのものが必要な場合に限り、サーバールートとオリジンから導出した絶対 URL を使います。本番環境のオリジンをハードコードしないでください。
遅延データの読み込み
重要度の低いデータは、ローダーから未 await のプロミスを返します。Await コンポーネントを使ってレンダリングします。
import { createFileRoute, Await } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
// Slow data — do NOT await
const slowDataPromise = fetchComments(postId)
// Fast data — await
const post = await fetchPost(postId)
return { post, deferredComments: slowDataPromise }
},
component: PostComponent,
})
function PostComponent() {
const { post, deferredComments } = Route.useLoaderData()
return (
<div>
<h1>{post.title}</h1>
<Await
promise={deferredComments}
fallback={<div>Loading comments...</div>}
>
{(comments) => (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.body}</li>
))}
</ul>
)}
</Await>
</div>
)
}
ミューテーション後の無効化
router.invalidate() は、アクティブなすべてのルートローダーを強制的に再実行し、キャッシュされたすべてのデータを古い状態としてマークします。
import { useRouter } from '@tanstack/react-router'
function AddPostButton() {
const router = useRouter()
const handleAdd = async () => {
await createPost({ title: 'New post' })
await router.invalidate({ sync: true })
}
return <button onClick={handleAdd}>Add Post</button>
}
次のステップで更新済みのローダーデータが必要な場合は、await router.invalidate({ sync: true }) を使います。
ミューテーションと無効化を 1 つのワークフローとして扱います。無効化を開始する前にミューテーションを永続化し、ローダーが同じ信頼できるストアから読み込むようにしてください。レンダリングされたルートで作成、更新、削除を検証し、ページを再読み込みした場合も確認します。コンポーネントのローカル状態によって、ローダーのデータが古いことや書き込みが永続化されていないことが隠れる場合があります。
エラー処理
import {
createFileRoute,
ErrorComponent,
useRouter,
} from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
const router = useRouter()
if (error instanceof CustomError) {
return <div>{error.message}</div>
}
return (
<div>
<ErrorComponent error={error} />
<button
onClick={() => {
// For loader errors, invalidate to re-run loader + reset boundary
router.invalidate()
}}
>
Retry
</button>
</div>
)
},
})
ローダーのパラメーター
loader 関数には次の値が渡されます。
params— 解析済みのパスパラメーターdeps—loaderDepsのオブジェクトcontext— 親コンテキストと beforeLoad コンテキストをマージしたものabortController— ルートの読み込みが解除されるか、古くなるとキャンセルされますcause—'enter'、'stay'、または'preload'preload— 事前読み込み中はtruelocation— 現在のロケーションオブジェクトparentMatchPromise— 親ルートのマッチ結果のプロミスroute— ルートオブジェクト自体
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params: { postId }, abortController }) =>
fetchPost(postId, { signal: abortController.signal }),
})
よくある間違い
重大:ローダーはサーバーでのみ実行されると思い込む
TanStack Router はクライアントファーストです。デフォルトでは、ローダーはクライアントで実行されます。SSR 用に TanStack Start を使用する場合はサーバーでも実行されますが、基本的な考え方はクライアント側での実行です。
// WRONG — this will crash in the browser
export const Route = createFileRoute('/posts')({
loader: async () => {
const fs = await import('fs') // Node.js only!
return JSON.parse(fs.readFileSync('...')) // fails in browser
},
})
// CORRECT for an SPA — use a client-safe API helper
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
データベースへのクエリ、ファイルシステムへのアクセス、サーバー専用のコードをローダーに直接記述しないでください。TanStack Start では、その処理をサーバー関数に記述し、ローダーからその関数を呼び出してください。SSR ローダーでは相対パスの fetch('/api/...') を使用しないでください。
中:デフォルトの staleTime が 0 であることを理解していない
デフォルトの staleTime は 0 です。つまり、ルートが再マッチするたびに、バックグラウンドでデータが再読み込みされます。これは新鮮なデータを確実に取得するための意図的な仕様です。ただし、データの取得コストが高い場合や静的な場合は、staleTime を設定してください。
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: 60_000, // Consider fresh for 1 minute
})
高:エラーコンポーネントで router.invalidate() ではなく reset() を使用する
reset() はエラーバウンダリーの UI だけをリセットします。ローダーは再実行されません。ローダーのエラーには、ローダーを再実行してバウンダリーをリセットする router.invalidate() を使用してください。
// WRONG — resets boundary but loader still has stale error
function PostErrorComponent({ error, reset }) {
return <button onClick={reset}>Retry</button>
}
// CORRECT — re-runs loader and resets the error boundary
function PostErrorComponent({ error }) {
const router = useRouter()
return <button onClick={() => router.invalidate()}>Retry</button>
}
高:createRootRouteWithContext の二重括弧が抜けている
createRootRouteWithContext<Type>() はファクトリーで、関数を返します。2 回呼び出す必要があります。
// WRONG — missing second call, passes options to the factory
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>({
component: RootComponent,
})
// CORRECT — factory()({options})
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({
component: RootComponent,
})
高:beforeLoad または loader で React フックを使用する
beforeLoad と loader は React コンポーネントではありません。その中でフックを呼び出すことはできません。フックから値を渡すには、ルーターのコンテキストを使用してください。
// WRONG — hooks cannot be called outside React components
export const Route = createFileRoute('/posts')({
loader: () => {
const auth = useAuth() // This will crash!
return fetchPosts(auth.userId)
},
})
// CORRECT — inject hook values via router context
// In your App component:
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
// In your route:
export const Route = createFileRoute('/posts')({
loader: ({ context: { auth } }) => fetchPosts(auth.userId),
})
高:プロパティの順序が TypeScript の型推論に影響する
ルーターは、先に宣言されたプロパティから後に宣言されたプロパティへ型を推論します。beforeLoad を loader の後に宣言すると、ローダー内では beforeLoad からのコンテキストが不明になります。
// WRONG — context.user is unknown because beforeLoad declared after loader
export const Route = createFileRoute('/admin')({
loader: ({ context }) => fetchData(context.user),
beforeLoad: () => ({ user: getUser() }),
})
// CORRECT — validateSearch → loaderDeps → beforeLoad → loader
export const Route = createFileRoute('/admin')({
beforeLoad: () => ({ user: getUser() }),
loader: ({ context }) => fetchData(context.user),
})
高:loaderDeps から検索オブジェクト全体を返す
// WRONG — loader re-runs on ANY search param change
loaderDeps: ({ search }) => search
// CORRECT — only re-run when page changes
loaderDeps: ({ search }) => ({ page: search.page })
search オブジェクト全体を返すと、関係のないパラメーターの変更(例:sortDirection、viewMode)によって、オブジェクト全体のディープ比較が失敗し、不要な再読み込みが発生します。
トレードオフ
- クライアントファーストのローダーと SSR への期待:ローダーはデフォルトでクライアント上で実行されます。SSR(TanStack Start)を使用する場合は、クライアントとサーバーの両方で実行されます。ブラウザー専用 API はデフォルトで動作しますが、SSR では問題が発生します。サーバー専用 API(fs、db)はデフォルトでは問題が発生しますが、Start のサーバー関数では動作します。router-core/ssr/SKILL.md を参照してください。
- 組み込みの SWR キャッシュと外部キャッシュの連携:ルーターにはキャッシュ機能が組み込まれています。TanStack Query を使用する場合は、キャッシュの二重化を避けるため、
defaultPreloadStaleTime: 0を設定してください。compositions/router-query/SKILL.md を参照してください。
関連項目
- 関連項目:router-core/search-params/SKILL.md —
loaderDepsは検証済みの検索パラメーターをキャッシュキーとして使用します - 関連項目:compositions/router-query/SKILL.md — TanStack Query との外部キャッシュ連携について