svelte-runtime について
Svelte 5 を使う開発者向けに、状態管理、ライフサイクル、SSR、水和、Vitest テストのランタイム API と実践例をまとめたスキルです。
主な機能
- 01Store、Context、$state による状態管理
- 02ライフサイクルと命令型コンポーネント API
- 03SSR、水和、CSP の設定
- 04Vitest によるコンポーネントテストと同期実行
- 05Svelte 5 ランタイムの実例
活用シーン
- 01コンポーネント間でリアクティブな状態を共有
- 02mount、render、hydrate によるコンポーネント管理
- 03SSR のデータ再利用と CSP の処理
- 04コンポーネントテストの作成とデバッグ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ full-stack-skills/ svelte-skills --skill svelte-runtimeインストール後、Claude Code で/svelte-runtimeを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlexamples/csp-examples.mdmount-hydrate.mdonmount-tick.mdREADME.mdssr-patterns.mdreferences/csp-reference.mdlifecycle-runtime-reference.mdmount-unmount-reference.mdREADME.mdssr-reference.md詳細
Svelte ランタイムリファレンス(Svelte 5)
このスキルでは、Svelte 5 のランタイム API(コンポーネント間の状態共有(Stores/Context)、ライフサイクル、命令型 API、SSR、テスト)を扱います。
このスキルを使用する場面
コンポーネント間で状態を共有する場合、コンポーネントのライフサイクルを扱う場合、mount/render などの命令型 API を使用する場合、または Vitest のコンポーネントテストを作成する場合に使用してください。
重要:Stores(svelte/store)
Store は、コンポーネント間でリアクティブな状態を共有する従来の方法です(Svelte 5 では、.svelte.js 内の $state オブジェクトに置き換えることが推奨されています)。
writable
import { writable } from 'svelte/store';
const count = writable(0);
count.subscribe(v => console.log(v)); // 订阅
count.set(1); // 设置值
count.update(n => n + 1); // 更新
readable(外部から設定できません)
import { readable } from 'svelte/store';
const time = readable(new Date(), (set) => {
const interval = setInterval(() => set(new Date()), 1000);
return () => clearInterval(interval);
});
derived
import { derived } from 'svelte/store';
const doubled = derived(count, $c => $c * 2);
const combined = derived([a, b], ([$a, $b]) => $a + $b);
readonly
import { readonly, writable } from 'svelte/store';
const readOnlyStore = readonly(writableStore);
コンポーネントでの使用
<script>
import { count } from './store.js';
console.log($count); // $ 前缀订阅
$count = 2; // 自动调用 .set()
</script>
Store はコンポーネントのトップレベルで宣言してください。変数の前に
$プレフィックスを付けないでください(それは$countであり、countではありません)。
Svelte 5 で推奨される代替方法:
// state.svelte.js
export const userState = $state({ name: '', count: 0 });
// 其他组件直接 import
重要:Context(svelte)
props を何層にもわたって渡さずに、コンポーネント階層を越えてデータを受け渡します(prop drilling)。
setContext / getContext
// Parent.svelte
<script>
import { setContext } from 'svelte';
setContext('key', 'hello');
</script>
// Child.svelte
<script>
import { getContext } from 'svelte';
const msg = getContext('key');
</script>
createContext(推奨、型安全)
// context.ts
import { createContext } from 'svelte';
export const [getUserContext, setUserContext] = createContext<User>();
// Parent.svelte
<script> setUserContext({ name: 'Ada' }); </script>
// Child.svelte
<script> const user = getUserContext(); </script>
リアクティブな Context
$state オブジェクトを Context に格納します。
setContext('counter', counterState);
// ❌ 不要重新赋值整个对象
// counterState = { count: 0 }
// ✅ 直接修改属性
counterState.count = 0;
重要:ライフサイクルフック
基本的な考え方:Svelte 5 のライフサイクルには、作成と破棄の 2 つの段階しかありません。中間の「状態更新」はエフェクトが処理します。これは、リアクティブな最小単位がコンポーネントではなくエフェクトだからです。そのため、
beforeUpdate/afterUpdateフックはありません。
onMount
コンポーネントが DOM にマウントされた後に実行されます(SSR では実行されません)。
<script>
import { onMount } from 'svelte';
onMount(() => {
console.log('mounted');
return () => console.log('unmounted'); // 返回清理函数
});
</script>
返された関数はコンポーネントのアンマウント時に呼び出されます。async 関数を渡すと、クリーンアップ関数は呼び出されません(async 関数は常に
Promiseを返すためです)。非同期タスクとクリーンアップの両方が必要な場合は、IIFE でラップし、外側のアロー関数から同期的にクリーンアップ関数を返してください。
onDestroy
コンポーネントがアンマウントされる前に実行されます(SSR でも実行されます)。
<script>
import { onDestroy } from 'svelte';
onDestroy(() => cleanup());
</script>
tick
DOM の更新が完了するまで待ってから処理を続行します。
<script>
import { tick } from 'svelte';
$effect.pre(() => {
// DOM 更新前
tick().then(() => {
// DOM 已更新完成
});
});
</script>
ライフサイクルと mount/unmount/hydrate の連携
| 段階 | mount() | hydrate() | render()(SSR) | unmount() |
|---|---|---|---|---|
| 同期:コンポーネントの script、state、テンプレート | ✓ | ✓ | ✓ | — |
| 同期:DOM への挿入/文字列の出力 | ✓ target を置換 | ✓ SSR DOM を再利用 | ✓ head+body を返す | — |
マイクロタスク:$effect/onMount の実行 | ✓ | ✓ | ✗ | — |
| 同期:effect と onDestroy のクリーンアップ | — | — | ✓(render 完了後) | ✓ |
| 非同期:トランジション(outro) | — | — | — | outro:true の場合は待機 |
mount()が返る時点では、effect とonMountはまだ実行されていません。すぐに実行するにはflushSync()を呼び出してください。hydrate()も同様です。tick()とflushSync()はサーバーでは no-op です(リアクティブなスケジューリングがありません)。
重要:命令型コンポーネント API
mount(クライアントでのマウント)
import { mount } from 'svelte';
import App from './App.svelte';
const app = mount(App, {
target: document.querySelector('#app'),
props: { name: 'world' }
});
unmount(アンマウント)
import { mount, unmount } from 'svelte';
unmount(app, { outro: true }); // 播放过渡后卸载
render(サーバーサイドレンダリング)
import { render } from 'svelte/server';
const { head, body } = await render(App, { props: { data } });
hydrate(クライアントでのハイドレーション)
SSR の HTML を再利用し、操作可能な状態にします。
import { hydrate } from 'svelte';
const app = hydrate(App, { target: document.querySelector('#app') });
重要:ハイドレーション可能なデータ
SSR 時にデータをシリアライズし、クライアントのハイドレーション時に再利用します(リクエストの重複を防ぎます)。
<script>
import { hydratable } from 'svelte';
const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
CSP のサポート
hydratable() は、render() が返す head にインラインの <script> を埋め込みます。CSP はデフォルトでこれをブロックするため、nonce または hash を使って許可する必要があります。
// 动态 SSR:每请求一个 nonce
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
csp: { nonce }
});
// 响应头同步加上:
// Content-Security-Policy: script-src 'self' 'nonce-${nonce}'
// 静态 SSG:使用 hash
const { head, body, hashes } = await render(App, {
csp: { hash: true }
});
// Content-Security-Policy: script-src ${hashes.script.map(h => `'${h}'`).join(' ')}
重要:
hydratable()を使用しない場合、render()はインラインの<script>を出力しないため、CSP の厳格モードでも何もブロックされません。- インラインのイベントハンドラー(
<button onclick={...}>)は<script>タグではないため、nonce は必要ありません。 - nonce モードを推奨します。hash モードは将来的にストリーミング SSR と競合します。
- 詳しくは references/csp-reference.md を参照してください。
重要:ベストプラクティス
$state
// ✅ 只对需要响应式的变量用 $state
let count = $state(0);
// ✅ 大数组/不可变数据用 $state.raw
let apiData = $state.raw([]);
apiData = await fetchData(); // 重新赋值触发更新
$derived
// ✅ 派生值用 $derived
let doubled = $derived(count * 2);
// ❌ 不要用 $effect 同步派生
let square;
$effect(() => { square = num * num; }); // ❌
let square = $derived(num * num); // ✅
$effect
// ✅ 仅用于副作用
$effect(() => { myChart.resize(); });
// ❌ 不要同步状态
$effect(() => { left = total - spent; }); // ❌
Props
// ✅ 依赖 props 的值用 $derived
let { type } = $props();
let color = $derived(type === 'danger' ? 'red' : 'green');
イベント
<button onclick={handleClick}>click</button>
<button on:click={handleClick}>click</button>
<svelte:window onkeydown={handleKey} />
Snippets
{#snippet footer()}
<p>footer</p>
{/snippet}
Each ブロック
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
Context
// ✅ 用 createContext 提供类型安全
import { createContext } from 'svelte';
const [getCtx, setCtx] = createContext<MyCtx>();
レガシー機能を避ける
// ✅ Runes mode 全是新项目的默认
let count = $state(0); // ✅
$: count++; // ❌ legacy
export let name; // ❌
let { name } = $props(); // ✅
on:click={fn} // ❌
onclick={fn} // ✅
<slot /> // ❌
{@render children()} // ✅
<svelte:component this={x} /> // ❌
<svelte:element this={x} /> // ✅
重要:テスト(Vitest)
インストールと設定
npm install -D vitest
// vite.config.js
export default defineConfig({
resolve: process.env.VITEST
? { conditions: ['browser'] }
: undefined
});
ユニットテスト
// multiplier.svelte.js
export function multiplier(initial, k) {
let count = $state(initial);
return {
get value() { return count * k; },
set: (c) => { count = c; }
};
}
// test
import { multiplier } from './multiplier.svelte.js';
test('multiplier', () => {
let double = multiplier(0, 2);
expect(double.value).toEqual(0);
double.set(5);
expect(double.value).toEqual(10);
});
コンポーネントテスト
import { mount, unmount } from 'svelte';
import { expect, test } from 'vitest';
test('MyComponent', () => {
const comp = mount(MyComponent, { target: document.body, props: { name: 'Ada' } });
expect(document.body.innerHTML).toContain('Ada');
unmount(comp);
});
flushSync
保留中のすべてのエフェクトを強制的に同期実行します(テストでよく使用します)。
import { flushSync } from 'svelte';
flushSync();
すぐに解決する方法
| 問題 | 解決策 |
|---|---|
| コンポーネント間で状態が同期されない | Context(階層を越える場合)または .svelte.js(グローバル)を使用します |
| onMount のクリーンアップが実行されない | 同期関数を渡していることを確認します(async ではありません)。非同期処理が必要な場合は IIFE でラップします |
| SSR 後にコンポーネントを操作できない | mount ではなく hydrate を使用します |
| テストで状態が更新されない | flushSync() を使用して強制的に同期実行します |
| Context の接続が切れる | Context オブジェクト全体を再代入しないでください |
| mount 後に effect が実行されない | mount() は同期的に返りますが、effect は次の microtask で実行されます。すぐに実行するには flushSync() を呼び出します |
| CSP が hydratable スクリプトをブロックする | csp: { nonce }(動的)または csp: { hash: true }(静的)を使用し、レスポンスヘッダーにも同じ内容を宣言します |
| 非同期 onMount でクリーンアップを取得できない | 外側の同期アロー関数で return () => controller.abort() を返し、async のロジックは IIFE の中に置きます |
注意点
- Store の
$プレフィックスはサブスクリプション構文です —$storeはテンプレートと script の両方でサブスクリプションを意味します。$プレフィックスのない変数は通常の値です - Context の接続が切れる — オブジェクト全体を再代入するとリアクティブな接続が切れます。
obj = {count:0}ではなく.count = 0を使用してください mountは$effectを実行しません — コンポーネントのマウント後、マイクロタスクによって effects が実行されます- テストでの
$effect.root— effects を扱うテストでは$effect.root()でラップする必要があります renderは SSR 環境でのみ使用できます — クライアントではmountを使用してください- async
onMountのクリーンアップは無視されます — async 関数は常に Promise を返すため、Svelte はクリーンアップ関数を取得できません。IIFE パターンを使用してください hydrate()は effect を同期実行しません —mount()と同様、ハイドレーション後に effect を実行するにはflushSync()を使用してください- CSP nonce は使い捨てです — リクエストごとに新しい nonce を使用してください。hash モードは将来のストリーミング SSR と競合します
- ハイドレーション時にランダム値が一致しないことがあります — SSR と CSR では
Math.random()/Date.now()の結果が異なります。hydratable()でラップしてください
よくある質問
Q: Store と Context と .svelte.js はいつ使い分けますか?
A: Store:アプリ全体で共有するシンプルな値に使います。Context:コンポーネントツリーの一部で共有する場合に使います。.svelte.js の $state:Store の代替として推奨される新しい方法です。
Q: tick と await Promise.resolve() は何が違いますか?
A: tick() は DOM の更新後に解決されることを保証しますが、Promise.resolve() は Svelte の更新サイクルを待ちません。
Q: tick() と flushSync() はどう使い分けますか?
A: await tick() は非同期のユーザーコードに適しています。flushSync() はテストや、DOM をただちに同期的に更新する必要がある外部ライブラリとの同期処理に適しています。
Q: $effect を含むコードはどうテストすればよいですか?
A: flushSync() で強制的に実行し、テストコードを $effect.root() でラップします。
Q: SSR 後の hydration でエラーが発生します。
A: SSR と CSR の HTML 構造が一致していることを確認し、mount ではなく hydrate を使います。
Q: onMount と $effect はいつ使い分けますか?
A: onMount は「マウント後に一度だけ実行される」副作用です。$effect は「依存関係が変化するたびに再実行される」リアクティブな副作用です。
Q: hydrate のインラインスクリプトが CSP にブロックされる場合はどうすればよいですか?
A: レンダリング時に csp: { nonce } を使ってスクリプトに nonce を付与し、レスポンスヘッダーにも同じ nonce を指定します。静的サイトでは csp: { hash: true } を使って hashes.script を取得します。
例
Svelte 5 のランタイム API を紹介する実用的な例です。
| ファイル | 説明 |
|---|---|
| examples/README.md | クイックリファレンスへの入り口 |
| examples/mount-hydrate.md | 9 つの例:mount、unmount、hydrate、render、flushSync、tick |
| examples/ssr-patterns.md | SSR パターン:非同期レンダリング、ストリーミング、hydration、CSP |
| examples/onmount-tick.md | 15 のライフサイクル例:onMount(同期/非同期/クリーンアップ)、onDestroy、フォーカス/計測/スクロールでの tick、$effect.pre パターン |
| examples/csp-examples.md | 8 つの CSP の例:nonce(Express/SvelteKit/Cloudflare)、hash(SSG)、診断のヒント |
リファレンス
Svelte 5 のランタイム API に関する詳細な技術リファレンスです。
| ファイル | 説明 |
|---|---|
| references/README.md | クイックリファレンスへの入り口 |
| references/mount-unmount-reference.md | 詳細解説:mount/unmount/hydrate のライフサイクル、flushSync、tick |
| references/ssr-reference.md | SSR の詳細解説:render、非同期 SSR、CSP、hydration |
| references/lifecycle-runtime-reference.md | mount/hydrate ランタイムにおけるライフサイクル:エフェクトが実行されるタイミング、onMount のタイミング、tick と flushSync の比較、SSR フェーズの一覧 |
| references/csp-reference.md | CSP の nonce と hash の比較、ランタイム設定、ストリーミングとの互換性 |