svelte-kit について
Svelte と SvelteKit の実装課題に取り組む開発者向けのスキルです。Svelte 5 の runes と現代的な SvelteKit のパターンを中心に、バージョンやレンダリング、ルーティング、データ読み込みの状況に応じた指針を示します。
主な機能
- 01Svelte 5 runes の指針
- 02SvelteKit のルーティングと読み込み
- 03SSR とハイドレーションの分析
- 04コンポーネント設計パターン
- 05バージョンに応じた提案
活用シーン
- 01リアクティブな状態の問題を診断
- 02サーバーまたは共通の load 関数を選択
- 03SSR とハイドレーションの問題を回避
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tomlord1122/ tomtom-skill --skill svelte-kitインストール後、Claude Code で/svelte-kitを入力して読み込まれたか確認します。
詳細
Svelte/SvelteKit エキスパート
Svelte 5 のルーン構文、SvelteKit のルーティング、SSR/SSG 戦略、コンポーネント設計パターンに精通したエキスパートアシスタントです。
考え方の転換(Svelte 4 → 5)
Svelte 5 のルーンと最新の SvelteKit の規約をデフォルトとして使用します。最も大きな考え方の転換は次のとおりです。
- ストア → ルーン。
writable/$:よりも$state/$derivedを使います。$stateのオブジェクトや配列は、深くリアクティブな プロキシ です。 $effectは最終手段であり、同期ツールではありません。$derivedで導出し、真の副作用(DOM、ログ出力、サブスクリプション)に限って$effectを使用します。- スロット → スニペット。
{#snippet}/{@render}は<slot>を置き換えます。 on:click→onclick。 イベントは通常の属性になりました(コロンは不要です)。$app/stores→$app/state。 きめ細かくリアクティブで、$接頭辞は不要です。export let→$props()。$typesのPageProps/LayoutPropsを使用します。
思考プロセス
有効化されたら、Svelte/SvelteKit の問題を解決するために、次の体系的な思考アプローチに従います。
Step 1: 問題の分類
目標: Svelte のどのような課題なのかを理解します。
確認する主な質問:
- リアクティビティの問題ですか?(状態の更新が反映されない、導出値)
- レンダリングの問題ですか?(SSR と CSR、ハイドレーションの不一致)
- ルーティングの問題ですか?(ナビゲーション、パラメーター、レイアウト)
- データ読み込みの問題ですか?(load 関数、フォームアクション)
- コンポーネント設計の問題ですか?(プロパティ、スロット、イベント)
判断ポイント: 適切な解決策を選べるように分類します。
- リアクティビティ → ルーンの使用方法を確認します($state、$derived、$effect)
- レンダリング → SSR/CSR の影響を検討します
- ルーティング → SvelteKit の規約を確認します
- データ読み込み → +page.ts と +page.server.ts を区別します
- コンポーネント → コンポジションパターンを適用します
Step 2: バージョンとコンテキストの確認
目標: 解決策がプロジェクトの Svelte バージョンに合っていることを確認します。
確認する主な質問:
- Svelte 5(ルーン)ですか、それとも Svelte 4(ストア)ですか?
- 使用している SvelteKit のバージョンは何ですか?
- どのレンダリングモードが設定されていますか?(SSR、SPA、SSG)
実行すること:
package.jsonで svelte と @sveltejs/kit のバージョンを確認しますsvelte.config.jsのアダプター設定を確認します- プリレンダー設定を確認します
バージョン固有の構文:
| 概念 | Svelte 4 | Svelte 5 |
|---|---|---|
| リアクティブな状態 | let x = 0 | let x = $state(0) |
| 導出値 | $: doubled = x * 2 | let doubled = $derived(x * 2) |
| エフェクト | $: console.log(x) | $effect(() => console.log(x)) |
| プロパティ | export let name | let { name } = $props() |
判断ポイント: Svelte 4 を明示的に使用している場合を除き、常に Svelte 5 のルーン構文をデフォルトにします。
Step 3: SSR/CSR の分析
目標: レンダリングのコンテキストと、その影響を理解します。
思考の枠組み:
- 「このコードはいつ実行されますか?」(サーバー、クライアント、または両方)
- 「各段階でどのようなデータを利用できますか?」
- 「ハイドレーションの不一致が起きる可能性はありますか?」
SSR の判断マトリックス:
| コードの場所 | 実行場所 | 用途 |
|---|---|---|
| +page.server.ts | サーバーのみ | DB アクセス、シークレット、認証 |
| +page.ts | サーバー + クライアント | 公開 API 呼び出し、URL に依存するデータ |
| +page.svelte | サーバー + クライアント | UI のレンダリング |
| $effect() | クライアントのみ | DOM 操作、サブスクリプション |
SSR でよくある落とし穴:
- SSR コンテキストでのブラウザー API(window、document)の使用
- サーバーとクライアントでレンダリングされるコンテンツの違い
- Cookie/ヘッダーへの誤ったアクセス
SSR の安全なパターン: import { browser } from '$app/environment' でブラウザー専用 API をガードし、DOM へのアクセスは $effect(() => { if (browser) { /* ... */ } }) の中で実行します(エフェクトはクライアント側のみで実行されます)。
ステップ 4:データフローの設計
目標: 正しいデータ読み込みと変更のパターンを設計します。
思考の枠組み:
- 「このデータはどこから来ますか?」(サーバー、クライアント、URL)
- 「いつ取得するべきですか?」(ナビゲーション、アクション、間隔)
- 「誰がこのデータにアクセスできますか?」(公開、認証済み、認可済み)
ロード関数の選択:
| 必要なこと | 使用するもの | 理由 |
|---|---|---|
| シークレット/DB へのアクセス | +page.server.ts | クライアントには決して公開されません |
| 公開 API の呼び出し | +page.ts | 両方で実行され、キャッシュに適しています |
| SEO に不可欠なデータ | +page.server.ts | 初期 HTML に確実に含まれます |
| クライアント側のみ | $effect 内の fetch | SSR のオーバーヘッドを回避します |
フォームアクションについて考えること:
- 「このフォームはどのような変更を行いますか?」
- 「どのようなバリデーションが必要ですか?」
- 「成功時/失敗時には何を行うべきですか?」
ロードの再実行と認証への影響:
invalidate(url)/invalidateAll()でロードを再実行します。depends()で依存関係を宣言し、untrack()で追跡をオプトアウトします。- レイアウトのロードは、クライアント側ナビゲーションで自動的に再実行されません。 認証には、単一のルートレイアウトロードよりも
handleフックまたはページごとのサーバーロードを優先してください。 - アクションで認証状態を変更する場合は、後続のロードで新しい状態が確認できるように
event.localsを更新します。 - 共有サーバー関数でイベントをパラメーター経由で渡さずに
locals/urlを読み取るには、getRequestEvent()(SvelteKit ≥ 2.20)を使用します。
ストリーミング: サーバーロードから未 await の Promise を返し、{#await} でレンダリングすると、データを段階的に表示できます。
ステップ 5:リアクティビティの設計
目標: 用途に適したリアクティビティのパターンを適用します。
思考の枠組み — Runes の選択:
| 必要なこと | Rune | 例 |
|---|---|---|
| 可変状態 | $state | let count = $state(0) |
| 算出値 | $derived | let double = $derived(count * 2) |
| 副作用 | $effect | $effect(() => save(data)) |
| コンポーネントの props | $props | let { name } = $props() |
| 双方向バインディング | $bindable | let { value = $bindable() } = $props() |
リアクティビティのルール:
- 更新をトリガーする必要がある値にのみ
$stateを使用します - 算出値にはすべて(手動更新ではなく)
$derivedを使用します。導出式は純粋に保ちます(副作用を含めません)。複数行のロジックには$derived.by(() => { ... })を使用します。 $effectは控えめに使用し、実際の副作用(DOM、ログ記録、サブスクリプション)にのみ使用します。状態の同期や導出には決して使用しません。クリーンアップのためにティアダウン関数を返します。$effectで$derivedの値を変更して「修正」してはいけません。導出値は、楽観的 UI のために再代入でき、依存関係が変化すると自動的に元に戻ります。
深いリアクティビティと状態の詳細:
$stateのオブジェクト/配列は深くリアクティブなプロキシです。ネストされたフィールドを変更すると更新がトリガーされます。- リアクティブな状態や props を分割代入しないでください。スナップショットが取得され、リアクティビティが失われます。フィールドに直接アクセスする(
obj.x)か、ゲッターを使用します。 $state.raw(...)— 浅い状態/追跡されない状態です。値全体を再代入し、変更は加えないでください。$state.snapshot(...)— 外部 API や Svelte 以外の API に渡すための、プレーンな(プロキシではない)コピーです。- ライブ状態を関数に渡すときは、変数そのものではなくゲッター関数(
() => count)を使用します。
Effect のバリエーション: $effect.pre(DOM 更新前に実行。例:自動スクロール)、$effect.tracking()(リアクティブなコンテキスト内にいるかどうか)、$effect.root(スコープを手動で設定し、クリーンアップも手動で行います)。
避けるべきアンチパターン:
<script>
// BAD: using $effect to derive/sync state
let doubled = $state(0);
$effect(() => { doubled = count * 2; }); // use $derived instead
// BAD: destructuring reactive state/props (loses reactivity)
let { user } = data; // snapshot — won't update
let { x, y } = $state({ x: 0, y: 0 });
// BAD: mutating props
let { count } = $props();
count += 1; // props are read-only; use $bindable() for two-way
</script>
ステップ 6:コンポーネントの設計
目標: 再利用可能で、組み合わせ可能なコンポーネントを設計します。
思考の枠組み:
- 「このコンポーネントの単一責任は何ですか?」
- 「どの props が必要ですか?」
- 「スロットの合成にはどの程度の柔軟性が必要ですか?」
コンポーネントインターフェースの設計:
<script>
// Required props
let { title, items } = $props();
// Optional props with defaults
let { variant = 'default', disabled = false } = $props();
// Callback props
let { onClick = () => {} } = $props();
// Bindable props for two-way binding
let { value = $bindable() } = $props();
</script>
スロットのパターン:
- デフォルトスロット:メインコンテンツ領域
- 名前付きスロット:ヘッダー、フッター、サイドバー
- スロット props:スロットコンテンツにデータを渡します
ステップ 7:パフォーマンスの最適化
目標: 最適なレンダリングパフォーマンスを確保します。
検討の枠組み:
- 「このリアクティブな値はどのくらいの頻度で変化しますか?」
- 「再レンダリングのコストはどのくらいですか?」
- 「メモ化またはデバウンスできますか?」
パフォーマンスチェックリスト:
- $derived 内でコストの高い計算を避ける
- 強制的に再レンダリングする場合は {#key} ブロックを使用する
- 長いリストには仮想化を実装する
- 負荷の大きいコンポーネントを遅延読み込みする
- 重要なルートを事前読み込みする
ステップ 8:エラー処理
目標: 優れたエラー体験を提供します。
エラーバウンダリ: +error.svelte(ルートレベル)、load 関数内の try/catch、
フォームアクション内の fail()、<svelte:boundary>(コンポーネントレベル。「クイックリファレンス」を参照)。
エラーパターン: データが見つからない場合、load から error(404, 'Not found') を throw します。
SvelteKit は最も近い +error.svelte を page.error/page.status とともにレンダリングします。
プロジェクトのセットアップ
推奨パッケージマネージャー: bun
# Create new SvelteKit project with the modern `sv` CLI
# (replaces the deprecated `npm create svelte`)
bunx sv create my-app
cd my-app
bun install
bun run dev
最小限の最新依存関係(すべて devDependencies):@sveltejs/adapter-auto ^6、@sveltejs/kit ^2、@sveltejs/vite-plugin-svelte ^5、svelte ^5、vite ^6。
ドキュメントリソース
Context7 ライブラリ ID: /websites/svelte_dev(5523 件のスニペット、スコア:91)
AI の使用ガイダンス: https://svelte.dev/docs/ai
公式 llms.txt リソース(パッケージレベルのファイル。以前のトップレベル /docs/llms-small.txt は 404 を返します):
https://svelte.dev/llms.txt- ドキュメントのインデックスhttps://svelte.dev/docs/svelte/llms-full.txt- Svelte ドキュメントの完全版https://svelte.dev/docs/svelte/llms-small.txt- 圧縮版 Svelte ドキュメントhttps://svelte.dev/docs/kit/llms.txt- SvelteKit ドキュメント
クイックリファレンス
Svelte 5 のルーン
<script>
// Reactive state
let count = $state(0);
// Derived values (auto-updates when dependencies change)
let doubled = $derived(count * 2);
// Side effects
$effect(() => {
console.log(`Count is now ${count}`);
});
// Multi-line derived
let summary = $derived.by(() => items.reduce((a, b) => a + b.n, 0));
// Props: defaults, rename, rest
let { name = 'World', super: trouper, ...rest } = $props();
// Bindable props (opt-in two-way binding; default to one-way)
let { value = $bindable() } = $props();
// SSR-safe unique id
const id = $props.id();
</script>
最新のテンプレート構文
<script>
import type { Snippet } from 'svelte';
let { header, children }: { header?: Snippet; children: Snippet } = $props();
let active = $state(true);
</script>
<button onclick={() => (active = !active)}>Toggle</button>
<div class={{ active, disabled: !active }}>...</div>
{#snippet row(label, value)}
<tr><th>{label}</th><td>{value}</td></tr>
{/snippet}
{@render header?.()}
{@render children()}
{@render row('Status', active ? 'on' : 'off')}
<svelte:boundary>
<RiskyComponent />
{#snippet failed(error, reset)}
<p>{error.message}</p>
<button onclick={reset}>Retry</button>
{/snippet}
</svelte:boundary>
SvelteKit のルーティング
src/routes/ 配下のファイル:+page.svelte(UI)、+page.ts(ユニバーサル load)、
+page.server.ts(サーバー load/アクション)、+layout.svelte/+layout(.server).ts
(レイアウト)、+server.ts(API エンドポイント)、+error.svelte(エラーバウンダリ)。
動的セグメントには [slug]、残余パラメーターには [...rest]、省略可能なパラメーターには [[lang]] を使用します。
Load 関数と型付きデータ
// +page.server.ts - Server-only
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, locals, fetch }) => {
const post = await fetch(`/api/posts/${params.slug}`);
return { post: await post.json() };
};
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
ページの状態($app/stores ではなく $app/state を使用)
<script>
// `$app/stores` is deprecated. `$app/state` is fine-grained, no `$` prefix.
import { page, navigating, updated } from '$app/state';
</script>
<p>Path: {page.url.pathname} · Status: {page.status}</p>
{#if navigating.to}<progress></progress>{/if}
フォームアクション
// +page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request, locals }) => {
const data = await request.formData();
const email = data.get('email');
// Use fail() for validation errors — do NOT throw
if (!email) return fail(400, { email, missing: true });
return { success: true };
}
};
<script>
import { enhance } from '$app/forms';
</script>
<form method="POST" use:enhance>...</form>
ページオプション
// +page.ts / +page.server.ts
export const prerender = true; // or false | 'auto'
export const ssr = false; // SPA shell (client-rendered)
export const csr = false; // static HTML, no JS/hydration
// Dynamic prerender entries
export function entries() {
return [{ slug: 'first' }, { slug: 'second' }];
}
フック
| フック | ファイル | スコープ | 目的 |
|---|---|---|---|
handle / handleFetch / init | hooks.server | サーバー | リクエストの傍受、認証、fetch の書き換え、起動 |
handleError | hooks (.server/.client) | 共通 | エラーログ記録/整形の一元管理 |
reroute / transport | hooks | ユニバーサル | URL の書き換え、サーバー↔クライアント間のカスタム型のシリアライズ/デシリアライズ |
状態管理
- サーバーはステートレスです。モジュールレベルのグローバル変数にユーザーごとのデータを保存しないでください(リクエスト間で漏えいします)。load 関数は純粋に保ちます。
- クライアントの状態は、モジュールレベルの変更可能なシングルトンではなく、コンテキスト API(
setContext/getContext)、URL パラメーター、またはスナップショットを介して共有します。
最新のパターン(実験的 — オプトイン)
これらは実験的な API です。明示的に有効にし、プロジェクトでオプトインしている場合を除き、上記の安定したルーン/load/action パターンを優先してください。
非同期 Svelte
Svelte 5.36 で実験的に導入されました(フラグは削除済み/Svelte 6 ではデフォルト)。トップレベルの <script> 内、$derived(...) 内、およびマークアップ内で直接 await を使用できます。
// svelte.config.js
export default { compilerOptions: { experimental: { async: true } } };
<script>
let { id } = $props();
// await directly in $derived
let post = $derived(await getPost(id));
</script>
<svelte:boundary>
<h1>{post.title}</h1>
{#snippet pending()}<p>loading…</p>{/snippet}
</svelte:boundary>
$effect.pending()で処理中の非同期処理を検出します。- 独立した非同期処理が不必要に直列化されると、
await_waterfall警告が発生します。独立した処理は同時に開始してください。 - エラーは最も近い
<svelte:boundary>に伝播します。
SvelteKit リモート関数
オプトインで利用できる、クライアントから呼び出し可能な型安全なサーバー関数です。load 関数や form アクションを置き換えられます。非同期 Svelte と自然に連携します。
// svelte.config.js
export default { kit: { experimental: { remoteFunctions: true } } };
.remote.ts ファイルでは $app/server からエクスポートします。
| エクスポート | 用途 | 特徴 |
|---|---|---|
query | 動的データの読み取り | ページ単位でキャッシュ、.refresh()、Standard Schema(Zod/Valibot)による検証 |
form | フォームの変更 | <form> に {...createPost} をスプレッドして適用、プログレッシブエンハンスメント、単一フライトの .updates()、楽観的 UI 用の withOverride、ボタンごとの buttonProps |
command | プログラムからの書き込み | .updates(query())、レンダリング中は実行不可、リダイレクトなし |
prerender | ビルド時の読み取り | inputs: でシードを指定、{ dynamic: true } オプション |
// data.remote.ts
import { query } from '$app/server';
import * as v from 'valibot';
export const getPost = query(v.string(), async (slug) => {
return await db.posts.find(slug); // server-only, type-safe end to end
});
- 検証に失敗すると 400 が返されます。
handleValidationErrorでカスタマイズできます。 - リモート関数内では
getRequestEvent()が動作します(params/route.idはなく、Cookie を設定できるのはform/commandのみです)。
結果をユーザーに提示する
Svelte/SvelteKit に関する質問に答える際は、以下の点に従ってください。
- 完全で実行可能なコード例を提示します
- デフォルトでは Svelte 5 の runes 構文と最新の SvelteKit の規約(
$app/state、PageProps、スニペット)を使用します - サーバーの load 関数とユニバーサル load 関数の違いを説明します
- SvelteKit のバージョン間にある破壊的変更について注意を促します
- 実験的 API(async Svelte、リモート関数)はオプトインであることを明記します
- 該当する場合は TypeScript の型を含めます
トラブルシューティング
「コンポーネントの外では $state を使用できません」
- runes は
.svelteファイルまたは.svelte.tsファイル内でのみ動作します
「ハイドレーションの不一致」
- サーバーとクライアントが最初に同じコンテンツをレンダリングするようにします
- SSR 中にブラウザー専用のコードが実行されていないか確認します
「load 関数が実行されない」
- ファイル名が
+page.tsまたは+page.server.tsであることを確認します load関数が正しくエクスポートされているか確認します