Svelte 5のリアクティビティ($state・$derived・$effect)を整理した話
Svelte 5からリアクティビティの仕組みが「ルーン(Rune)」という新しい方式に変わりました。
$state・$derived・$effect の3つを中心に、実際に動かしながら挙動を整理します。
Svelte 4との違い
Svelte 4までは let で宣言するだけで変数がリアクティブになりました。Svelte 5では明示的にルーンを使う必要があります。
<!-- Svelte 4 -->
<script>
let count = 0; // これだけでリアクティブ
</script>
<!-- Svelte 5 -->
<script lang="ts">
let count = $state(0); // $stateで明示的に宣言
</script>
3つのルーンの役割
| ルーン | 役割 | 近いイメージ |
|---|---|---|
$state |
変化する値を定義する | ref() (Vue) / useState (React) |
$derived |
他の値から計算される値 | computed (Vue) / useMemo (React) |
$effect |
値の変化に反応して副作用を実行 | watch (Vue) / useEffect (React) |
$state — 変化する値を定義する
<script lang="ts">
let count = $state(0);
</script>
<p>カウント: {count}</p>
<button onclick={() => count++}>+1</button>
<button onclick={() => count--}>-1</button>
$state(初期値) で宣言した変数を変更するだけでUIが自動的に更新されます。
$derived — 他の値から計算される値
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
let message = $derived(count >= 10 ? '10以上です' : 'まだ10未満です');
</script>
<p>カウント: {count}</p>
<p>2倍: {doubled}</p>
<p>状態: {message}</p>
$derived の中で参照している $state が変わると自動的に再計算されます。計算ロジックをテンプレートに直接書かず $derived にまとめることでコードが読みやすくなります。$derived の中に別の $derived を使うこともできます。
$effect — 値の変化に反応して副作用を実行する
<script lang="ts">
import { untrack } from 'svelte';
let count = $state(0);
let history = $state<number[]>([]);
$effect(() => {
history = [...untrack(() => history), count];
});
</script>
<p>履歴: {history.join(' → ')}</p>
$effect は内部で読んだ $state や $derived の値が変わると自動的に再実行されます。
躓きポイント:無限ループ
以下のように書くと無限ループになります。
// ❌ 無限ループ
$effect(() => {
history = [...history, count]; // historyを読んでhistoryを書き換える
});
$effect は内部で読んだ $state をすべて追跡します。history を読んで書き換えると、その書き換えが再び $effect を起動して無限ループになります。
count変化 → $effect実行 → history書き換え → $effect再実行 → history書き換え → ...
解決策:untrack を使う
// ✅ untrackで囲んだ値は追跡されない
$effect(() => {
history = [...untrack(() => history), count];
});
untrack で囲んだ値は「読んでも追跡しない」ようになります。これにより「count が変わったときだけ実行、history の変化では再実行しない」という意図通りの動きになります。
$effect のクリーンアップ
タイマーやイベントリスナーのような「後片付けが必要な処理」は $effect の中で関数を return します。
<script lang="ts">
let count = $state(0);
let autoCount = $state(false);
$effect(() => {
if (!autoCount) return;
const timer = setInterval(() => {
count++;
}, 1000);
return () => clearInterval(timer); // クリーンアップ
});
</script>
<button onclick={() => autoCount = !autoCount}>
{autoCount ? '自動停止' : '自動カウント開始'}
</button>
autoCount が true になるとタイマーが起動して1秒ごとに count が増えます。autoCount が false に戻ると return した関数が実行されてタイマーが停止します。クリーンアップを書かないとタイマーが残り続けてメモリリークになります。
まとめ
<script lang="ts">
import { untrack } from 'svelte';
// $state:変化する値
let count = $state(0);
// $derived:他の値から計算される値
let doubled = $derived(count * 2);
// $effect:値の変化に反応して副作用を実行
$effect(() => {
// countを追跡・historyは追跡しない
history = [...untrack(() => history), count];
});
// $effect のクリーンアップ
$effect(() => {
if (!autoCount) return;
const timer = setInterval(() => count++, 1000);
return () => clearInterval(timer); // 後片付け
});
</script>
| ポイント | 内容 |
|---|---|
$state |
明示的に宣言しないとリアクティブにならない(Svelte 4との違い) |
$derived |
計算ロジックをテンプレートから分離できる |
$effect |
内部で読んだ $state すべてを自動追跡する |
untrack |
読んでも追跡しない。無限ループ防止に使う |
| クリーンアップ | $effect で return した関数が後片付けとして実行される |