execution-model について
TanStack Start のコードがサーバーとクライアントでどう実行されるか、環境境界をどう設定するかを説明するスキルです。秘密情報やサーバー専用処理のクライアントバンドルへの混入を防ぎ、ブラウザー専用コードやハイドレーションを扱います。
主な機能
- 01サーバー専用・クライアント専用関数 API
- 02環境ごとの実装切り替え
- 03ファイルのインポート保護
- 04環境変数の扱い
- 05ハイドレーションを考慮したレンダリング
活用シーン
- 01データベースアクセスと秘密情報をサーバーに限定
- 02ブラウザー API をクライアントだけで実行
- 03実行環境に応じて実装を選択
- 04コンポーネントのハイドレーション不一致を防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tanstack/ router --skill execution-modelインストール後、Claude Code で/execution-modelを入力して読み込まれたか確認します。
詳細
実行モデル
コードがどこで実行されるかを理解することは、TanStack Start の基本です。このスキルでは、アイソモーフィックな実行モデルと、環境境界を制御する方法について説明します。
CRITICAL: TanStack Start のコードはすべて、デフォルトでアイソモーフィックです。サーバーバンドルとクライアントバンドルの両方で実行されます。ルートローダーは、SSR 中のサーバーと、ナビゲーション中のクライアントの両方で実行されます。サーバー専用の処理には、必ず
createServerFnを使用してください。 CRITICAL: モジュールレベルでのprocess.envへのアクセスは、2 つの観点で誤りです。セキュリティ上、値がクライアントバンドルに漏えいするうえ、実行時の正確性にも問題があります。Cloudflare Workers などのエッジランタイムでは、環境変数はリクエストごとに注入されるため、モジュールレベルで読み取ると、サーバー上でもundefinedになります。環境変数は.handler()内など、リクエストごとに実行される関数内で読み取り、モジュールスコープでは読み取らないでください。 CRITICAL:VITE_が接頭辞に付いた環境変数は、クライアントバンドルに公開されます。サーバーシークレットには、VITE_を接頭辞として付けないでください。 CRITICAL: 相対 URL はブラウザー専用コードで使用してください。アイソモーフィックなローダーは SSR 中にも実行されるため、fetch('/api/...')にベース URL がない場合があります。ローダーからサーバー関数を呼び出すか、明示的な環境境界の内側に fetch を記述してください。
実行制御 API
| API | 使用例 | クライアントの動作 | サーバーの動作 |
|---|---|---|---|
createServerFn() | RPC 呼び出し、データ変更 | サーバーへのネットワークリクエスト | 直接実行 |
createServerOnlyFn(fn) | ユーティリティ関数 | エラーをスロー | 直接実行 |
createClientOnlyFn(fn) | ブラウザーユーティリティ | 直接実行 | エラーをスロー |
createIsomorphicFn() | 環境ごとに異なる実装 | .client() 実装を使用 | .server() 実装を使用 |
<ClientOnly> | ブラウザー専用コンポーネント | 子要素をレンダリング | フォールバックをレンダリング |
useHydrated() | ハイドレーション依存のロジック | ハイドレーション後に true | false |
import '@tanstack/<fw>-start/server-only' | ファイル全体をサーバー専用に設定 | インポートを拒否 | 許可 |
import '@tanstack/<fw>-start/client-only' | ファイル全体をクライアント専用に設定 | 許可 | インポートを拒否 |
サーバー専用の実行
createServerFn(RPC パターン)
サーバー専用コードを実行する主要な方法です。クライアントでは、呼び出しが fetch リクエストになります。
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createServerFn } from '@tanstack/react-start'
const fetchUser = createServerFn().handler(async () => {
const secret = process.env.API_SECRET // safe — server only
return await db.users.find()
})
// Client calls this via network request
const user = await fetchUser()
createServerOnlyFn(クライアントではエラーをスロー)
クライアントでは決して実行してはならないユーティリティ関数に使用します。
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createServerOnlyFn } from '@tanstack/react-start'
const getSecret = createServerOnlyFn(() => process.env.DATABASE_URL)
// Server: returns the value
// Client: THROWS an error
クライアント専用の実行
createClientOnlyFn
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createClientOnlyFn } from '@tanstack/react-start'
const saveToStorage = createClientOnlyFn((key: string, value: string) => {
localStorage.setItem(key, value)
})
ClientOnly コンポーネント
// Use @tanstack/<framework>-router for your framework (react, solid, vue)
import { ClientOnly } from '@tanstack/react-router'
function Analytics() {
return (
<ClientOnly fallback={null}>
<GoogleAnalyticsScript />
</ClientOnly>
)
}
useHydrated フック
// Use @tanstack/<framework>-router for your framework (react, solid, vue)
import { useHydrated } from '@tanstack/react-router'
function TimeZoneDisplay() {
const hydrated = useHydrated()
const timeZone = hydrated
? Intl.DateTimeFormat().resolvedOptions().timeZone
: 'UTC'
return <div>Your timezone: {timeZone}</div>
}
動作:SSR → false、クライアントでの初回レンダー → false、ハイドレーション後 → true(以後は true のままです)。
環境ごとの実装
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createIsomorphicFn } from '@tanstack/react-start'
const getDeviceInfo = createIsomorphicFn()
.server(() => ({ type: 'server', platform: process.platform }))
.client(() => ({ type: 'client', userAgent: navigator.userAgent }))
インポート保護:ファイルマーカー
実験的な機能です。
.server.* と .client.* のファイル名サフィックス(例:db.server.ts)を付けると、ファイルに Start のインポート保護が適用され、誤った環境からインポートできなくなります。ファイル名を変更できない、または変更したくない場合は、ファイルの先頭に副作用インポートを追加すると、マーカーで同じ保護を適用できます。
// src/lib/secrets.ts (filename can't be *.server.ts)
import '@tanstack/react-start/server-only'
// (or @tanstack/solid-start/server-only, @tanstack/vue-start/server-only)
export function getApiKey() {
return process.env.API_KEY
}
// src/lib/storage.ts
import '@tanstack/react-start/client-only'
// (or @tanstack/solid-start/client-only, @tanstack/vue-start/client-only)
export function savePreferences(prefs: Record<string, string>) {
localStorage.setItem('prefs', JSON.stringify(prefs))
}
ルール:
- 同じファイルに両方のマーカーを付けるとエラーになります。
- 型のみのインポートは無視されます(実行時には何も残らないためです)。
- デフォルトの動作は、本番ビルドでは
error、開発環境ではmockです。モックは再帰的な Proxy を返すため、インポートグラフを修正している間も開発環境で実行を続けられます。
適切なツールを選択してください:
- ファイルを誤った側で絶対に実行させず、クライアント API もない場合 →
*.server.tsのファイル名、またはimport '@tanstack/<fw>-start/server-only'。 - 環境ごとに動作を変える必要があるシンボルが 1 つの場合 →
createIsomorphicFn().client(...).server(...)。 - 誤った側から呼び出された場合にエラーにする関数が 1 つの場合 →
createServerOnlyFn/createClientOnlyFn。 - ハイドレーション後にのみコンポーネントをレンダリングする場合 →
<ClientOnly>またはuseHydrated()。
環境変数
サーバー側(createServerFn 内)
process.env で任意の変数にアクセスできます。
const connectDb = createServerFn().handler(async () => {
const url = process.env.DATABASE_URL // no prefix needed
return createConnection(url)
})
クライアント側(コンポーネント)
VITE_ をプレフィックスに持つ変数のみ利用できます。
// Framework-specific component type (React.ReactNode, JSX.Element, etc.)
function ApiProvider({ children }: { children: React.ReactNode }) {
const apiUrl = import.meta.env.VITE_API_URL // available
// import.meta.env.DATABASE_URL → undefined (security)
return (
<ApiContext.Provider value={{ apiUrl }}>{children}</ApiContext.Provider>
)
}
クライアントのランタイム変数
VITE_ プレフィックスを付けずにサーバー側の変数をクライアントで使用する必要がある場合は、サーバー関数を介して渡してください。
const getRuntimeVar = createServerFn({ method: 'GET' }).handler(() => {
return process.env.MY_RUNTIME_VAR
})
export const Route = createFileRoute('/')({
loader: async () => {
const foo = await getRuntimeVar()
return { foo }
},
component: () => {
const { foo } = Route.useLoaderData()
return <div>{foo}</div>
},
})
環境変数の型安全性
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_NAME: string
readonly VITE_API_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare global {
namespace NodeJS {
interface ProcessEnv {
readonly DATABASE_URL: string
readonly JWT_SECRET: string
}
}
}
export {}
よくある間違い
1. 重大:ローダーはサーバー専用だと思い込む
// WRONG — loader runs on BOTH server and client
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const secret = process.env.API_SECRET // LEAKED to client
return fetch(`https://api.example.com/data`, {
headers: { Authorization: secret },
})
},
})
// CORRECT — use createServerFn
const getData = createServerFn({ method: 'GET' }).handler(async () => {
const secret = process.env.API_SECRET
return fetch(`https://api.example.com/data`, {
headers: { Authorization: secret },
})
})
export const Route = createFileRoute('/dashboard')({
loader: () => getData(),
})
2. 重大:モジュールスコープで process.env を読み取る
モジュールレベルで process.env を読み取るのは、1 つではなく2 つの理由で誤りです。
- セキュリティ: 値がクライアントバンドルにインライン化され、秘密情報が漏えいする可能性があります。
- ランタイムの正確性(エッジランタイム): Cloudflare Workers やその他のエッジ SSR ランタイムでは、リクエスト時に env が注入されます。モジュールレベルのコードは env が存在する前のモジュール読み込み時に実行されるため、サーバー上でも読み取り結果は
undefinedになります。このバグはデプロイ時に初めて表面化します。
// WRONG — leaks to client AND is undefined on Workers
const apiKey = process.env.SECRET_KEY
export function fetchData() {
/* uses apiKey, which is undefined under Worker SSR */
}
// CORRECT — read per-request, inside the handler
const fetchData = createServerFn({ method: 'GET' }).handler(async () => {
const apiKey = process.env.SECRET_KEY
return fetch(url, { headers: { Authorization: apiKey } })
})
同じルールは、ミドルウェアの .server() コールバック、サーバールートのハンドラー、およびリクエストごとに実行されるあらゆる関数にも適用されます。env はファイルの先頭ではなく、そこで読み取ってください。
3. 重大:サーバーの秘密情報に VITE_ プレフィックスを使用する
# WRONG — exposed to client bundle
VITE_SECRET_API_KEY=sk_live_xxx
# CORRECT — no prefix for server secrets
SECRET_API_KEY=sk_live_xxx
# CORRECT — VITE_ only for public client values
VITE_APP_NAME=My App
4. 高:ハイドレーションの不一致
// WRONG — different content server vs client
function CurrentTime() {
return <div>{new Date().toLocaleString()}</div>
}
// CORRECT — consistent rendering
function CurrentTime() {
const [time, setTime] = useState<string>()
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
return <div>{time || 'Loading...'}</div>
}
アーキテクチャの判断基準
サーバー専用(createServerFn / createServerOnlyFn):
- 機密データ(env 変数、秘密情報)
- データベース接続、ファイルシステム
- 外部 API キー
クライアント専用(createClientOnlyFn / <ClientOnly>):
- DOM 操作、ブラウザー API
- localStorage、位置情報
- 分析/トラッキング
アイソモーフィック(デフォルト / createIsomorphicFn):
- データのフォーマット、ビジネスロジック
- 共通ユーティリティ
- ルートローダー(本質的にアイソモーフィックです)
関連項目
- start-core/server-functions — 主なサーバー境界
- start-core/deployment — デプロイ先は実行に影響します