svelte-lifecycle について
Svelte 5 開発者向けのスキルガイドです。ライフサイクルフック、ストア、Context、テストに加え、SSR の動作とブラウザー互換性も扱います。
主な機能
- 01ライフサイクルフックと DOM 更新
- 02writable、readable、derived ストア
- 03Context と SSR の状態分離
- 04Vitest、Storybook、Playwright によるテスト
- 05ブラウザー対応表と例外
活用シーン
- 01コンポーネントのマウント・破棄時の副作用管理
- 02コンポーネント間でのリアクティブ状態共有
- 03ユニット・コンポーネント・E2E テストの作成
- 04ブラウザーの最低バージョン要件の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ full-stack-skills/ svelte-skills --skill svelte-lifecycleインストール後、Claude Code で/svelte-lifecycleを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlexamples/context-advanced.mdlifecycle-hooks.mdREADME.mdstore-patterns.mdstores-advanced.mdtesting-playwright.mdtesting-vitest.mdreferences/browser-support.mdcontext-api-reference.mdlifecycle-hooks-reference.mdREADME.mdstores-api-reference.mdtesting-reference.md詳細
Svelte のライフサイクル、ストア、コンテキスト、テスト(Svelte 5)
このスキルでは、Svelte 5 の「Runes 以外」でもコアのリアクティブアーキテクチャに属するサブシステムを扱います。ライフサイクルフック(onMount / onDestroy / tick)、ストア(writable / readable / derived / カスタム)、Context API(createContext / setContext / getContext)、およびテスト体系(Vitest のユニットテストとコンポーネントテスト、Storybook、Playwright の e2e)です。また、ブラウザーのサポート対象と例外も扱います。
このスキルを使う場面
- コンポーネントのマウント後に DOM にアクセスする(
onMount)、またはコンポーネントの破棄前にクリーンアップする(onDestroy) - 状態変更後に DOM の更新を待つ(
tick) importモジュールを使わずにコンポーネント間でリアクティブな状態を共有する(ストアまたは Context を使用)- コンポーネント間で非同期ストリームを扱う(Readable/derived ストア)
- prop drilling の代わりに Context を使い、SSR 時にグローバル状態を汚染しないようにする
- Svelte コンポーネントの Vitest ユニットテスト/コンポーネントテスト、Storybook のストーリー、または Playwright の e2e を作成する
- ブラウザーの最低バージョンと例外的な機能を確認する
重要:onMount
onMount(fn) は、コンポーネントが DOM にマウントされた直後に実行されます。ブラウザーでのみ実行されます——SSR 時には呼び出されません。
基本的な使い方
<script>
import { onMount } from 'svelte';
onMount(() => {
console.log('mounted');
});
</script>
クリーンアップ関数を返す
<script>
import { onMount } from 'svelte';
onMount(() => {
const interval = setInterval(() => console.log('beep'), 1000);
return () => clearInterval(interval);
});
</script>
重要な制約:
onMountがクリーンアップを正しく返すには、同期関数を受け取る必要があります。async () => {}は常にPromiseを返すため、アンマウント時にクリーンアップは呼び出されません。
外部モジュールから呼び出す
onMount はコンポーネントのスクリプト最上位に記述する必要はありません。ただし、コンポーネントの初期化時にのみ呼び出す必要があります。同じモジュール内のヘルパー関数から呼び出すこともできます。
重要:onDestroy
onDestroy(fn) は、コンポーネントが破棄される直前に直ちに実行されます。SSR のコンポーネントでも実行されます——4 つのライフサイクルフックのうち、サーバー側で実行される唯一のフックです。
<script>
import { onDestroy } from 'svelte';
onDestroy(() => {
console.log('destroyed');
});
</script>
一般的な用途:setInterval のクリーンアップ、AbortController による fetch のキャンセル、グローバルイベントリスナーの解除。
重要:tick
tick() は Promise を返します。保留中のすべての状態変更が DOM に反映された後に resolve されます。保留中の変更がない場合は、次の microtask で resolve されます。
<script>
import { tick } from 'svelte';
async function handle() {
count = count + 1;
await tick();
// 此时 DOM 已更新
element.scrollIntoView();
}
</script>
一般的な用途:フォーカス、スクロール、DOM の測定、状態変更後にサードパーティの命令型 API を呼び出すこと。
非推奨:beforeUpdate / afterUpdate
Svelte 4 時代の「コンポーネント全体の更新前後」に実行されるフックです。Svelte 5 ではshim により提供されていますが、runes コンポーネントでは使用できません。代わりに以下を使用してください。
| Svelte 4 | Svelte 5 |
|---|---|
beforeUpdate(() => {}) | $effect.pre(() => {}) |
afterUpdate(() => {}) | $effect(() => {}) |
Runes 版は、明示的に読み取られた状態が変化した場合にのみ実行されるため、より正確です。たとえば、チャットウィンドウでは messages が変化した場合にのみ最下部までスクロールし、テーマを切り替えてもスクロール位置はリセットされません。
重要:ストア(writable、readable、derived)
ストアとは「ストア契約」を満たすオブジェクトです。subscribe(fn) → unsubscribe を持つ必要があり、書き込み可能なストアには set(value) も必要です。
<script>
import { writable } from 'svelte/store';
const count = writable(0);
$count; // 自动订阅,读取当前值
count.set(1); // 写入
$count = 2; // 语法糖:等价于 count.set(2)
</script>
writable
import { writable } from 'svelte/store';
const count = writable(0, () => {
// 第一个订阅者订阅时调用
return () => {
// 最后一个订阅者退订时调用
};
});
2 番目の引数は start/stop 関数で、外部接続(WebSocket、タイマー)の確立によく使われます。
readable
外部から set できないストアです。初期値と start 関数を指定します。
import { readable } from 'svelte/store';
const time = readable(new Date(), (set) => {
set(new Date());
const id = setInterval(() => set(new Date()), 1000);
return () => clearInterval(id);
});
derived
1 つ以上のストアから派生させます。
import { derived, writable } from 'svelte/store';
const a = writable(1);
const b = writable(2);
const sum = derived([a, b], ([$a, $b]) => $a + $b);
非同期版では (values, set, update) を受け取り、非同期コールバック内で set / update を呼び出せます。クリーンアップ関数を返すこともでき、第 3 引数で初期値を指定できます。
readonly / get
readonly(store):読み取り専用のビューとしてラップします(set/updateなし)get(store):一度だけ同期的に読み取ります(内部で購読を開始 → 読み取り → 購読解除するため、頻繁に実行される処理では推奨されません)
ストアと $state の使い分け
Svelte 5 では、runes($state、.svelte.js モジュール)を優先して使うことが推奨されています。
| 場面 | 推奨 |
|---|---|
| 再利用可能なロジックの切り出し | .svelte.js ファイル + $state |
| コンポーネント間での状態共有 | モジュールレベルの $state オブジェクト |
| 複雑な非同期データストリーム | ストア(readable + タイマー/購読) |
| RxJS との相互運用 | ストア($ による自動購読) |
| コンポーネント間での単純なカウンター | どちらでも可 |
簡単なルール:runes で対応できる場合は runes を使い、ストアは「イベントストリーム/監視可能なシーケンス」に使います。
重要:Context API
Context を使うと、prop drilling をせずに親コンポーネントから任意の深さの子孫コンポーネントへ値を渡せます。
createContext(推奨、Svelte 5.40+)
// context.ts
import { createContext } from 'svelte';
interface User { name: string; }
export const [getUser, setUser] = createContext<User>();
<script>
import { setUser } from './context';
setUser({ name: 'world' });
</script>
<script>
import { getUser } from './context';
const user = getUser();
</script>
<h1>hello {user.name}</h1>
setContext / getContext(代替手段)
<script>
import { setContext } from 'svelte';
setContext('my-key', value);
</script>
<script>
import { getContext } from 'svelte';
const value = getContext('my-key');
</script>
キーと値には任意の JS 値を指定できます。createContext を使うと、型安全になり、キーも不要になります。
hasContext / getAllContexts
現在のコンポーネントのコンテキスト階層に特定のキーが存在するかどうかを判定します。getAllContexts() は現在のコンテキストをすべて含む Map を返します。ライブラリの内部実装でよく使われます。
state との組み合わせ
$state オブジェクトを context に設定し、再代入しないでください——リアクティブな接続が壊れてしまいます。
<button onclick={() => counter = { count: 0 } }>reset</button>
<button onclick={() => counter.count = 0}>reset</button>
Svelte は警告を出します。
グローバル state の代替(SSR で重要)
モジュールレベルの $state は SSR ではリクエスト間で共有されます(データ漏えいが発生します)。Context はリクエストごとに分離されるため、ユーザー固有のデータを扱う場合は Context を優先してください。
重要:テスト
Vitest のユニットテスト
// counter.svelte.test.js
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
test('Counter', () => {
let count = $state(0);
count = 1;
flushSync();
expect(count).toBe(1);
});
コンポーネントテスト
vite.config.js:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: { environment: 'jsdom' },
resolve: process.env.VITEST ? { conditions: ['browser'] } : undefined
});
import { mount, unmount, flushSync } from 'svelte';
import { expect, test } from 'vitest';
import Component from './Component.svelte';
test('Component', () => {
const c = mount(Component, { target: document.body, props: { n: 0 } });
expect(document.body.innerHTML).toBe('<button>0</button>');
document.body.querySelector('button').click();
flushSync();
expect(document.body.innerHTML).toBe('<button>1</button>');
unmount(c);
});
$effect は mount 時に自動実行されません。テストでは flushSync() を使って、同期的に実行されるようにします。
$effect を使ったテスト
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
test('Effect', () => {
const cleanup = $effect.root(() => {
let count = $state(0);
let log = [];
$effect(() => log.push(count));
flushSync();
expect(log).toEqual([0]);
count = 1;
flushSync();
expect(log).toEqual([0, 1]);
});
cleanup();
});
Storybook
npx sv add storybook で設定します。play 関数でユーザー操作をシミュレートし、アサーションを行います。
<Story name="Filled" play={async ({ canvas, userEvent }) => {
await userEvent.type(canvas.getByTestId('email'), '[email protected]');
await userEvent.click(canvas.getByRole('button'));
await expect(canvas.getByText("You're in!")).toBeInTheDocument();
}} />
Playwright の e2e
// tests/home.spec.js
import { expect, test } from '@playwright/test';
test('h1 visible', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
playwright.config.js で webServer を設定し、プレビューサーバーを起動します。
重要:ブラウザーのサポート
基準となる目標は 2020(Baseline 2020) です。
| ブラウザー | 最低バージョン |
|---|---|
| Chrome/Edge | 87 |
| Firefox | 83 |
| Safari | 14 |
| Opera | 73 |
| Opera(Android) | 62 |
| Samsung Internet | 14.0 |
| Android WebView | 87 |
| Internet Explorer | 非対応 |
例外的な機能(より新しいバージョンが必要)
| 機能 | Chrome/Edge | Firefox | Safari |
|---|---|---|---|
$state.snapshot | 98 | 94 | 15.4 |
bind:devicePixelContentBoxSize | — | 93 | 非対応 |
svelte/animate の flip | — | 126 | — |
Quick Fixes
- onMount 清理不执行 → 函数是
async;改用同步函数并内部await。 - store 引用 UI 不更新 → 必须用
$store前缀读取;不要解构(const { subscribe } = store不会自动订阅)。 - derived 不触发 → 回调里未读取任何 store 值;或数组写法
derived([a,b], ([$a,$b]) => ...)。 - Context 在 SSR 间泄漏 → 模块级
$state全局共享;改用setContext。 - Vitest 中
document is not defined→test.environment: 'jsdom';或在文件顶部加// @vitest-environment jsdom。 - 组件 mount 后状态未生效 → 在 mount 后调用
flushSync()。 - Playwright 找不到元素 → 检查
webServer.command是否正确启动;提高timeout。
Gotchas
- store 必须在组件顶层声明:不能在
if块或函数内。 - 不要给非 store 变量加
$前缀——会被识别为 store。 - onMount 是唯一的"挂载后",没有"渲染后";用
tick()等待 DOM。 - onMount 在 SSR 不运行——访问
window/document必须放 onMount 或 onDestroy(onDestroy 在 SSR 运行)。 - setContext/getContext 必须同步调用,在 setup 阶段(不能在事件处理器内)。
- Context 值若为原始值(数字/字符串),变更不触发更新——用
$state对象或函数 getter。 $state.snapshot浏览器要求 98+ / Safari 15.4+——兼容性发布时检查。- 绑定 onMount 的 cleanup 顺序:onMount 卸载时先调 cleanup,再调 onDestroy。
FAQ
Q: Svelte 5 还需要 stores 吗?
A: 简单状态用 $state + .svelte.js 模块;stores 在复杂异步流、计时器、外部订阅、RxJS 互操作时仍有价值。
Q: onMount vs $effect 怎么选?
A: 需要访问 DOM 节点、启动副作用(fetch、定时器、绑定 window 事件)用 onMount;纯响应式派生/同步副作用用 $effect。
Q: 为什么 onMount 的 async 函数清理不执行?
A: async () => {} 总是返回 Promise;onMount 收到非函数就跳过清理。改用同步函数包异步逻辑:
onMount(() => {
let cancelled = false;
(async () => {
const data = await fetch(...);
if (!cancelled) state = data;
})();
return () => { cancelled = true; };
});
Q: Context 替代 props 的时机?
A: 当一个值要穿透 3+ 层中间组件、或父组件不直接知道子组件(如 {@render children()})时。
Q: Vitest 单元测试和组件测试区别?
A: 单元测试纯逻辑(runes 在 .svelte.js 文件中),无 DOM;组件测试用 jsdom 渲染完整 Svelte 组件。
Q: IE 11 还能用 Svelte 5 吗? A: 不支持。最低 Chrome 87 / Firefox 83 / Safari 14。
Q: SSR 项目用 $state.snapshot 安全吗?
A: 服务端是 Node,无浏览器版本要求;仅客户端使用时检查例外表。
Q: Playwright 跑测试前要 build 吗?
A: playwright.config.js 中配 webServer: { command: 'npm run build && npm run preview', port: 4173 },Playwright 自动起 preview 服务器。
Examples
| 文件 | 内容 |
|---|---|
examples/lifecycle-hooks.md | onMount 清理、onDestroy、tick DOM 测量、deprecated 钩子 |
examples/stores-advanced.md | writable/readable/derived/自定义 store、异步流 |
examples/store-patterns.md | stores vs $state、持久化、跨组件模式 |
examples/context-advanced.md | createContext、setContext、与 state 组合、SSR 安全 |
examples/testing-vitest.md | 单元/组件测试、$effect.root、flushSync、context wrapper |
examples/testing-playwright.md | e2e 测试、Playwright config |
References
| 文件 | 内容 |
|---|---|
references/lifecycle-hooks-reference.md | onMount/onDestroy/tick 完整签名、SSR 注意事项 |
references/stores-api-reference.md | 全部 store API、TypeScript 类型、契约 |
references/context-api-reference.md | createContext/setContext/getContext/hasContext、SSR 行为 |
references/testing-reference.md | Vitest/Storybook/Playwright 完整 setup |
references/browser-support.md | 浏览器矩阵、例外特性、polyfill 指南 |