はじめに
以前Svelteの紹介記事を書きました。
この記事はSvelte 3について書いたもので、現在の最新はSvelte 5です。
通知が届くたびに嬉しくなる反面、古い情報なのも申し訳なく思っていたので、自分の学習も兼ねてSvelte 5での記法の紹介をしていきます。
この記事では、以前書いた記事の内容に対応するものを中心に基本的な記法を紹介します。比較対象は以前の記事に合わせてSvelte 3です。
詳しく確認したい方は公式のSvelte 5移行ガイドを読むと変更点がわかりやすいと思います。
Svelte 5について
約18ヶ月の開発期間を経て、2024/10/22にSvelte 5がリリースされました。
開発者体験をさらに向上させるためにSvelte 5は根本から書き直されました。その結果アプリはより高速で軽量になり、信頼性も向上しました。
詳しい背景については以下の公式ブログをご覧ください。
Svelte 5 誕生
Svelte 5の記法
根本から変更されたということでSvelte 5では記法も大きく変わっています。その変更点を見ていきましょう。
チュートリアルもSvelte 5用に書き直されています。一部のみ日本語対応されているようです(2024/12/26現在)。
記事の中ではほんの一部しか取り上げていないので、ぜひご自身で試してみてください。
リアクティビティ($state
)
リアクティブな値を宣言する時にSvelte 3では特別な指定は必要ありませんでしたが、Svelte 5では$state
を使用するようになりました。
この$state
のような特別な記法はrune
と呼ばれます。ルーン文字のルーンです。
"truly reactive"を掲げ、リアクティブな値も普通の変数と同じように書けることがSvelteの魅力でした。しかしコードが複雑になるにつれ、どの値がリアクティブであるかの判断が難しくなります。
Svelte 5ではリアクティブな値を明示的に示すことでコードの可読性が向上しました。
特殊な記法は宣言時にのみ必要で、使用時には通常の変数と同じように扱えます。
// Svelte 3
let count = 0;
// Svelte 5
let count = $state(0);
function increment() {
count += 1; // ここは変わらない
}
また、Svelte 3では参照先のオブジェクトを変えるような変更はリアクティブに反映されませんでした(配列にpush()
しても反映されず、再代入やスプレッド構文を使用する必要があった)。
Svelte 5ではpush()
でも問題なく反映されます(deep reactivity)。
// Svelte 3
let numbers = [1, 2, 3, 4];
numbers.push(numbers.length + 1); // 画面に反映されない
numbers = [...numbers, numbers.length + 1]; // 画面に反映される
// Svelte 5
let numbers = $state([1, 2, 3, 4]);
numbers.push(numbers.length + 1); // 画面に反映される
numbers = [...numbers, numbers.length + 1]; // 画面に反映される
rune
を使うのにimport等は不要です。拡張子が.svelte.js
、.svelte.ts
になっているJavaScriptやTypeScriptファイルでも使用できるので、以下のように記述することも可能です。
export const counter = $state({
count: 0
});
<script>
import { counter } from './shared.svelte.js';
</script>
<button onclick={() => counter.count += 1}>
clicks: {counter.count}
</button>
変数同士の同期($derived
)
$state
の値が変更された時、依存する他の変数も同時に変更することができます。
Svelte 3では$:
を使用していましたが、Svelte 5では$derived
を使用します。
$derived
は読み取り専用です。
// countが変更された時、doubledも変更される
// Svelte 3
let count = 0;
$: doubled = count * 2;
// Svelte 5
let count = $state(0);
let doubled = $derived(count * 2);
doubled = 5; // 読み取り専用なのでエラー
$derived.by
を使って引数に関数を渡すことも可能です。
let doubled = $derived.by(() => {
const double = 2
return count * double
});
$derived
内では副作用(例: count++のような値の変更操作)は禁止です。
let doubled = $derived.by(() => {
count++; // 副作用が発生するためエラー
const double = 2
return count * double
});
変数の追跡
リアクティブな値の変化を追跡するために、console.log()
でログを出力しようとすると「console.log
に$state
プロキシが含まれています。」や「メッセージを複製できませんでした」といった警告が表示されます。
これは$state
で宣言した変数がリアクティブプロキシ(裏で特殊な仕組みで管理されるデータ、例えば変化を検知してUIを更新するなど)として扱われるためです。
回避策としては2つあります。
-
$state.snapshot(...)
を使用する$state.snapshot(...)
を使用するとスナップショットが作成できます。
スナップショットはざっくり言うとその時点での変数です。プロキシではないため警告が表示されません。let numbers = $state([1, 2, 3, 4]); function addNumber() { numbers.push(numbers.length + 1); console.log($state.snapshot(numbers)); // スナップショットを作成 }
-
$inspect
を使用する$inspect
を使用して自動でスナップショットを作成することもできます。
$inspect
は依存する変数が変化すると自動で実行されます。開発中のみ機能し、ビルド時には実行されません。let numbers = $state([1, 2, 3, 4]); function addNumber() { numbers.push(numbers.length + 1); } $inspect(numbers); // numbers.push()が実行されたタイミングで実行
$inspect(...).with(fn)
を利用することでカスタマイズも可能です。以下はその一例です。-
変数が変更された時にデバッガーを起動する
$inspect(count).with((type, count) => { if (type === 'update') { debugger; // デバッガーを起動 } });
-
状態変更を追跡してスタックトレースを出力する
$inspect(count).with(console.trace());
-
状態の変化に応じた処理($effect
)
$effect
を使用することで、$state
や$derived
の変化に応じて処理を実行することができます。
チュートリアルでの使用例は以下のようになっています。ボタンを押すことでカウントアップが加速・減速するコード例です。
$effect
の中にclearInterval()
を入れることでクリック(interval
が変更される)時にsetInterval()
を解除しています。
<script>
let elapsed = $state(0);
let interval = $state(1000);
$effect(() => {
const id = setInterval(() => {
elapsed += 1;
}, interval);
return () => {
clearInterval(id);
};
});
</script>
<button onclick={() => interval /= 2}>speed up</button>
<button onclick={() => interval *= 2}>slow down</button>
<p>elapsed: {elapsed}</p>
$derived
と同じような処理が可能ですが、多用してしまうとコードの可読性が下がってしまうので注意が必要です。
イベントハンドラや$derived
で解決できる場合は$effect
を使うのを避けましょう。例えば状態の変化に応じて単純な計算を行う場合は、$derived
を利用する方がシンプルで保守性の高いコードになります。
使用すべきでない例も挙げられているのでこちらも確認してください。
イベントハンドラ
Svelte 3ではon:event
の形で記述していましたが、Svelte 5では非推奨になりonevent
での記述が推奨されています。
<!-- Svelte 3 -->
<button on:click={handleClick}>Click me</button>
<!-- Svelte 5 -->
<button onclick={handleClick}>Click me</button>
関数名とイベントハンドラが一致している場合は省略が可能です。
<script>
let count = $state(0);
function onclick() {
count++;
}
</script>
<button {onclick}>
clicks: {count}
</button>
Svelte 3からの移行
ここまで変更点を見てきたので、試しにSvelte 3からSvelte 5まで移行してみます。
公式の記載通りに実行していきます。参考にした記事は参考に記載しています。
まずはSvelte 3とSvelteKit 1を使ったプロジェクトを作成します。
npm create svelte@3
npm install
npm run dev # 実行確認
今回は以下の条件で作成しました。
元になるプロジェクトが作成できたので移行していきます。
Svelte 3の場合はまずSvelte 4へ移行する必要があるとここに書かれているので従います。
移行用のスクリプトがあるので簡単に移行ができます。以下を実行します。
npx sv migrate svelte-4
実行するとpackage.json
で"svelte": "^4.0.0"
になっているはずです。
eslint-plugin-svelte3
が入っているのですがSvelte 4以降では非推奨ということで公式の案内に従ってeslint-plugin-svelte
に切り替えておきます。
npm rm eslint-plugin-svelte3
npm i eslint-plugin-svelte -D
.eslintrc.cjs
も変更します。
module.exports = {
root: true,
extends: ['eslint:recommended', 'plugin:svelte/recommended', 'prettier'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
}
};
次にSvelte 5への移行を行います。こちらも移行スクリプトがあるので実行します。
npx sv migrate svelte-5
実行すると先にSvelteKit 2へ移行にするように言われたので言われるがままに実行します。
Detected SvelteKit 1. You need to upgrade to SvelteKit version 2 first (`npx sv migrate sveltekit-2`).
✔ Run sveltekit-2 migration now? … yes
Svelte 5への移行コマンドからでも実行できますが、自分で実行する場合にはnpx sv migrate sveltekit-2
でできるようです。
npm install
しようとすると@sveltejs/adapter-auto
のバージョンでエラーが出たので、--legacy-peer-deps
をつけて実行します。
# 依存関係を無視してインストール
npm install --legacy-peer-deps
SvelteKit 2へ移行できたので、今度はSvelte 5への移行をするために移行コマンドを実行します。
npx sv migrate svelte-5
npm install
npm install
を実行するとまたエラーが発生しました。実行後に表示される手順には、失敗したら--force
オプションを使用すると良いと書かれているのでその通りに実行します。
1: install the updated dependencies ('npm i' / 'pnpm i' / etc) (note that there may be peer dependency issues when not all your libraries officially support Svelte 5 yet. In this case try installing with the --force option)
npm install --force
npm run dev # 動作確認
これでSvelte 5へのアップグレードが完了しました。動作も問題ありません。
src/routes/Counter.svelte
の中身を確認すると、自動的にSvelte 5の記法に変更されていました。
<script>
import { spring } from 'svelte/motion';
let count = 0;
const displayed_count = spring();
$: displayed_count.set(count);
$: offset = modulo($displayed_count, 1);
function modulo(n, m) {
// handle negative numbers
return ((n % m) + m) % m;
}
</script>
<script>
import { run } from 'svelte/legacy';
import { spring } from 'svelte/motion';
let count = $state(0);
const displayed_count = spring();
function modulo(n, m) {
// handle negative numbers
return ((n % m) + m) % m;
}
run(() => {
displayed_count.set(count);
});
let offset = $derived(modulo($displayed_count, 1));
</script>
参考
記事を書くにあたってはこの辺りを参考にしました。
おわりに
かなり軽く使っていた自分としてはリアクティブ宣言必要になったんだ、という感じですがしっかり使う人にとってはかなり便利な変更がされている印象です。
今回は触れていませんが、コンポーネントなどで複数ファイルを扱う際の変更も色々とあり、今までよりも使いやすくなっているようです。気になる方はSvelte 5 移行ガイドで詳細をご確認ください。
色々変更されてはいますが書き方はシンプルなままなので、より幅広いところで使われることを期待しています。