Svelte 5 Runes入門 - $state・$derived・$effectと移行の要点

8分 で読める | 2025.12.02

公式ドキュメント

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:clickonclick 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はmounthydrateへ移します。

また、modern browserが前提になり、HTML構造やattribute syntaxの検証が厳しくなった箇所があります。CSS scopeの出力にも変更があるため、DOM snapshotや生成class名に依存するtestは見直します。

段階的な移行手順

公式migration scriptを利用できます。

npx sv migrate svelte-5

自動変換は開始点であり、完了判定ではありません。特に複雑な$:、event modifier、slot、imperative component APIは手動確認が必要です。

  1. Svelte 5と使用中のSvelteKit・adapterの対応版を確認する
  2. testとtype checkが通る基準点を作る
  3. migration scriptを小さなbranchで実行する
  4. component単位でlegacy modeからrunes modeへ移す
  5. $effectを派生計算に使っていないかreviewする
  6. SSRとhydration、form、event、bindingをtestする
  7. 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を確認しながら進めてください。

参考リソース

← 一覧に戻る
PR
PR
PR
PR