パラダイムの転換:コンパイラの魔法からRunesへ
長年、Svelteはそのシンプルさで際立っていました。let count = 0; と記述すれば、あとはコンパイラが処理してくれました。しかし、アプリケーションが成長するにつれ、Writableストアを使用してファイルをまたいで状態を管理することは、ボイラープレートの悪夢となりました。コンポーネント以外のファイルで購読解除を忘れ、メモリリークを心配しながら $ プレフィックスをやりくりすることも珍しくありませんでした。
Svelte 5はRunesでその状況を変えます。.svelte ファイル内でしか機能しないトップレベルの変数宣言に頼るのではなく、Runesはどこでも機能するシグナルベースのリアクティビティシステムを提供します。
これは、SvelteをSolidやVueのようなモダンなフレームワークのリアクティビティの扱いに近づけますが、よりクリーンな構文を備えています。私はこのアプローチを本番環境に適用してきましたが、特に複数のモジュール間で共有する必要のある複雑なデータ構造を扱う際に、一貫して安定した結果が得られています。
従来の手法 vs. Runesの手法
Svelte 4では、リアクティビティはコンポーネント内にスコープされていました。値を計算したい場合は $: ラベルを使用し、状態を共有したい場合は svelte/store を利用していました。現在のコードがどのようになるか、簡単に比較してみましょう。
// Svelte 4 - コンポーネント固有
let count = 0;
$: doubled = count * 2;
// Svelte 5 - どこでも動作
let count = $state(0);
let doubled = $derived(count * 2);
最大の変化は、$state と $derived が明示的になったことです。どの変数がリアクティブであるかをコンパイラの推測に頼る必要はもうありません。この明確さにより, リアクティブな値がどこから発生したかを正確に追跡できるため、デバッグが大幅に容易になります。
Runesアプローチのメリットとデメリット
新しい構文への移行には常にトレードオフが伴います。いくつかの内部ツールをSvelte 5にリファクタリングした結果、このアプローチが優れている点と、注意が必要な点が見えてきました。
メリット
- 汎用的なリアクティビティ:
.jsや.tsファイルでリアクティブな状態を定義できます。リアクティビティを維持するためだけに、ロジックをコンポーネントブロック内に閉じ込める必要はありません。 - 自動購読の廃止:
$store構文がなくなり、変数に直接アクセスするだけになります。これにより、コードを読む際の認知的負荷が大幅に軽減されます。 - きめ細かな更新: Svelte 5は内部でシグナルを使用しているため、コンポーネントロジックの大きな塊を再実行するのではなく、DOMの変更された特定の部分のみが更新されます。
- 統合されたライフサイクル:
$effectがonMount、afterUpdate、onDestroyを、単一のより予測可能なメカニズムに置き換えます。
課題
- 破壊的変更: 大規模なSvelte 4のコードベースがある場合、移行は単なる検索置換では済みません。データの流れを再考する必要があります。
- 冗長性:
let count = 0よりも$state(0)と書く方が少しタイピング量が増えます。開発者によっては、最初は明示的なRunesを少し煩わしく感じるかもしれません。
Svelte 5の推奨セットアップ
Runesの実験を始めたい場合は、新しいViteプロジェクトをセットアップすることをお勧めします。Svelte 5はエコシステムの現在の方向性であるため、ツールチェーンはすでに十分に成熟しています。
# 新しいSvelteプロジェクトを作成
npm create vite@latest my-svelte-app -- --template svelte-ts
# ディレクトリに移動
cd my-svelte-app
# 依存関係をインストール
npm install
# 最新のSvelteバージョンであることを確認
npm install svelte@next
svelte.config.js でRunesを有効にするための特別な設定は必要ありません。現在はコアライブラリの一部です。ただし、特に $props() を定義する際には、Runesが提供する型安全性を最大限に活用するために、常にTypeScriptを有効にすることをお勧めします。
実装ガイド:リアクティブなアプリの構築
4つの主要なRunes($state、$derived、$props、$effect)を使用して、基本的なリアクティブなアプリの構築を実装する方法を見ていきましょう。
1. $stateによる状態管理
単純な代入の代わりに、UIの更新をトリガーする必要があるデータには $state を使用します。これはプリミティブ、配列、オブジェクトに機能します。
<script>
let user = $state({
name: 'エンジニア',
tasks: []
});
function addTask(task) {
user.tasks.push(task); // Svelte 5はこれを自動的に追跡します!
}
</script>
2. $derivedによる派生値
他の状態に依存する値が必要な場合は常に $derived を使用します。これは古い $: リアクティブ宣言を置き換えます。通常の変数のように動作しながら同期が保たれるため、非常にクリーンだと感じます。
<script>
let items = $state([10, 20, 30]);
let total = $derived(items.reduce((a, b) => a + b, 0));
</script>
<p>合計は: {total}</p>
3. $propsによるコンポーネント間通信
コンポーネントへのデータの受け渡しには、以前は export let name; を使用していました。Svelte 5では $props() を使用します。これはモダンなJavaScriptの分割代入に近く、デフォルト値の処理も改善されています。
<script>
let { title, status = '保留中' } = $props();
</script>
<h1>{title}</h1>
<span>ステータス: {status}</span>
4. $effectによるサイドエフェクトの処理
ここからが面白くなるところです。$effect はほぼすべてのライフサイクルフックを置き換えます。内部のリアクティブな値が変更されるたびに実行されます。ログ出力、ローカルストレージとの同期、IDが変更されたときのデータ取得などに最適です。
<script>
let userId = $state(1);
$effect(() => {
console.log(`ユーザーIDが変更されました: ${userId}`);
// クリーンアップロジック(onDestroyを置き換え)
return () => {
console.log('次の実行前、またはコンポーネント破棄前にクリーンアップします');
};
});
</script>
5. ストアを共有状態に置き換える
Svelte 5における真の「プロの技」は、svelte/store を使わずに共有状態ファイルを作成することです。$state 値を含むオブジェクトをエクスポートするだけで済みます。最近の本番環境へのデプロイにおいて、これがグローバルな状態を管理する最も安定した方法であることがわかりました。
// logger.js (または .ts)
export const appState = $state({
theme: 'dark',
toggleTheme() {
this.theme = this.theme === 'dark' ? 'light' : 'dark';
}
});
あとは任意のコンポーネントで appState をインポートして直接使用するだけです。$ プレフィックスも、手動の購読も、ボイラープレートも必要ありません。
最後に
RunesはSvelteにとって大きな進化を象徴しています。コンパイラ重視のアプローチから明示的なリアクティビティシステムへの移行は大きな変化に感じるかもしれませんが、コードの再利用性と明確さの面でのメリットは否定できません。私のチームでは、Runesに切り替えた直後から、「リアクティブなバグ」(コンパイラが依存関係を捉えられなかったために変数が更新されないという奇妙な現象)が減少したことに気づきました。
今日新しいプロジェクトを始めるなら、最初からSvelte 5を採用することを強くお勧めします。開発体験がより予測可能になり、SvelteをWebエコシステムの未来に適合させることができます。

