Svelte 5は、reactivityを表すRunesを導入したmajor releaseです。$state、$derived、$effect、$propsなどを使い、.svelte component以外の.svelte.js・.svelte.tsでも同じreactivity modelを利用できます。
移行前に知っておくこと
記事情報: 2025年12月2日初出。Svelte 5公式migration guideとRunes referenceを2026年7月25日に再確認しています。
Svelte 5へ更新しても、既存componentをすべて即座にRunesへ書き換える必要はありません。Svelte 5は旧来のsyntaxをlegacy modeとして扱えるため、component単位で段階的に移行できます。
ただし1つのcomponentでRunesを使い始めると、そのcomponentはrunes modeになります。旧syntaxとの対応を理解し、自動migrationの差分をreviewしてください。
| Svelte 4までの代表例 | Svelte 5 Runes |
|---|---|
top-levelのlet | $state |
$:による派生値 | $derived |
$:による副作用 | $effect |
export let | $props |
<slot> | snippetと{@render} |
on:click | onclick property |
$stateでreactive stateを宣言する
<script lang="ts">
let count = $state(0);
</script>
<button onclick={() => count += 1}>
count: {count}
</button>
$stateは通常のfunctionではなく、Svelte compilerが解釈するruneです。importする必要はありません。primitive valueは通常の変数のように読み書きできます。
arrayやplain objectへ$stateを使うとdeep proxyが作られ、propertyやarrayの変更も追跡されます。
<script lang="ts">
let todos = $state([
{ id: 1, title: 'read docs', done: false }
]);
function completeFirst() {
todos[0].done = true;
}
</script>
class instanceなどはplain objectと同じdeep proxyにならない場合があります。外部libraryへstateを渡すとき、proxyではないsnapshotが必要なら$state.snapshotなど公式APIの用途を確認します。
$derivedで派生値を作る
ほかのstateから計算できる値は、別の$stateと$effectで同期せず$derivedを使います。
<script lang="ts">
let price = $state(1000);
let quantity = $state(2);
let total = $derived(price * quantity);
</script>
<p>合計: {total}円</p>
expressionが長い場合は$derived.byでfunction bodyを書けます。
<script lang="ts">
let scores = $state([70, 80, 90]);
let average = $derived.by(() => {
if (scores.length === 0) return 0;
return scores.reduce((sum, value) => sum + value, 0) / scores.length;
});
</script>
派生計算は副作用を持たない形にします。network request、localStorageへの保存、DOM操作は$derivedの責務ではありません。
$effectは外部との同期に限定する
$effectは、読まれたstateをdependencyとして追跡し、変更後に処理を再実行します。
<script lang="ts">
let theme = $state<'light' | 'dark'>('light');
$effect(() => {
document.documentElement.dataset.theme = theme;
return () => {
delete document.documentElement.dataset.theme;
};
});
</script>
$effectはbrowserで実行され、SSR中には実行されません。serverでも必要な計算は$derivedなどで表現します。
effectで別stateを更新し、その変更がさらにeffectを動かす構造はloopや追跡しづらい依存を作ります。公式documentも、state同期の手段としてeffectを多用するより、派生値を使うよう案内しています。
$propsと$bindable
旧来のexport letは、runes modeでは$props()のdestructuringへ変わります。
<script lang="ts">
type Props = {
title: string;
count?: number;
};
let { title, count = 0 }: Props = $props();
</script>
<h2>{title}</h2>
<p>{count}</p>
propsは親から子への入力です。子から変更を返す場合はcallback propを使うと流れが明確になります。双方向bindingが必要なpropだけを$bindableで明示します。
<script lang="ts">
let { value = $bindable('') } = $props<{
value?: string;
}>();
</script>
<input bind:value />
Svelte 5のrunes modeでは、component propのbindingは明示的に$bindableへする必要があります。すべてのpropをbinding可能にするとdata flowを追いにくくなるため、input componentなど必要な箇所へ限定します。
event handlerの変更
event directiveはproperty形式へ移行できます。
<!-- legacy -->
<button on:click={handleClick}>保存</button>
<!-- Svelte 5 -->
<button onclick={handleClick}>保存</button>
on:click|preventDefaultのようなmodifierは、handler内でevent.preventDefault()を呼ぶか、wrapper functionを作ります。同じevent propertyを複数回書けないため、複数handlerは1つのfunctionから呼びます。
component eventもcreateEventDispatcher中心の形からcallback propへ移せます。これはeventの型とdata flowを通常のpropとして表現しやすくしますが、既存public component APIを変える場合は利用側と同時に更新します。
snippetとrender
Svelte 5では、再利用可能なmarkupをsnippetとして定義し、{@render}で描画できます。
{#snippet row(label: string, value: string)}
<dt>{label}</dt>
<dd>{value}</dd>
{/snippet}
<dl>
{@render row('Status', 'Ready')}
{@render row('Owner', 'Ada')}
</dl>
子componentへ渡すcontentもsnippetで表現できます。旧<slot>を移行するときは、default content、named slot、slot propsがそれぞれどのsnippet propになるか確認します。
childrenはdefault contentに使われる予約propになるため、既存componentで別用途のchildren propを持つ場合はrenameが必要です。
component APIとそのほかの変更
Svelte 5のcomponentはclassではなくなり、new Component(...)でinstance化する旧imperative APIは変わりました。手動mountが必要なcodeはmountやhydrateへ移します。
また、modern browserが前提になり、HTML構造やattribute syntaxの検証が厳しくなった箇所があります。CSS scopeの出力にも変更があるため、DOM snapshotや生成class名に依存するtestは見直します。
段階的な移行手順
公式migration scriptを利用できます。
npx sv migrate svelte-5
自動変換は開始点であり、完了判定ではありません。特に複雑な$:、event modifier、slot、imperative component APIは手動確認が必要です。
- Svelte 5と使用中のSvelteKit・adapterの対応版を確認する
- testとtype checkが通る基準点を作る
- migration scriptを小さなbranchで実行する
- component単位でlegacy modeからrunes modeへ移す
$effectを派生計算に使っていないかreviewする- SSRとhydration、form、event、bindingをtestする
- libraryならSvelte 4/5のsupport方針を決める
性能値だけを目的に全componentを書き換えず、共有stateを.svelte.tsへ移したい、propsやeventを明示したいなど、Runesの利点が出る場所から進めます。
まとめ
RunesはSvelteのreactivityを明示し、component外でも同じmodelを使えるようにします。$stateは状態、$derivedは計算、$effectは外部同期、$propsは入力という役割を分けることが重要です。
Svelte 5はlegacy syntaxとの段階移行を用意しています。自動変換後のcodeを一度に受け入れず、componentのpublic API、SSR、event、bindingを確認しながら進めてください。