Svelte 4 → 5 へのマイグレーションを手作業でやってみた

個人のプロジェクトでSveltekitを使ったちょっとしたアプリを開発しています。
昨年10月にSvelte 5がリリースされ、構文に大幅な変更が入りました。
公式からマイグレーションスクリプトが公開されていますが、アプリケーションの規模が小さいため、変更点の勉強を兼ねて手作業でマイグレーションしてみることにしました。
はじめに
細かい変更も含めたマイグレーションの手順については公式のドキュメントがあります。
https://svelte.jp/docs/svelte/v5-migration-guide
本記事では、実際に私のアプリケーションで必要になった作業のみを実際のコードの一部と共に記載しています。
個人開発では4から5でこれくらいのインパクトがあるんだな、と思っていただければと思います。
Runeへの記法の変更
Svelte 5ではRuneと呼ばれる$
から始まる構文が導入され、Svelte 4までのlet
や$:
を使った記法から置き換えられています。
基本的には記法を変えるだけで、特にその他の変更は必要としない修正です。
リアクティブな変数を$state()
で囲う
Svelte 4ではリアクティブな変数はlet
で宣言するだけでしたが、Svelte 5では右辺を$state()
で囲う必要があります。
// let counters: CounterParameters[] = new Array();
let counters: CounterParameters[] = $state(new Array());
プロパティとなるexport let
は別のRuneとなっているため後述します。
リアクティブな変数から算出される変数を $derived()
で囲う
Svelte 4では$:
で宣言することで、リアクティブな変数の変更に伴い再計算される変数を定義していました。
Svelte 5では$:
は使用せず、右辺を$derived()
で囲います。
// $: hasUndoStack = undoStack.length > 0;
const hasUndoStack = $derived(undoStack.length > 0);
変更に伴って更新される箇所は$effect()
で囲う
$:
はリアクティブな変数が変更される度に実行される処理の宣言としても利用していました。
この場合は$effect()
を使用します。
算術変数の場合と副作用で記法が変わることになりました。
こちらは今回のコードでは使用していなかったため割愛します。
コンポーネントのプロパティは$props()
を使う
Svelte 4ではコンポーネントのプロパティはexport let
で宣言していましたが、Svelte 5では let foo = $props()
になりました。
// export let counters: CounterParameters[] = new Array();
let {counters}: {counters: CounterParameters[]} = $props(new Array());
この変更に伴い、$$props
と$$restProps
は廃止されています(元々非推奨ではありましたが)。
Storeの代わりに.svelte.js(.ts)
を使う(option)
RuneはSvelte 4までのlet
や$:
と異なり、コンポーネントのトップレベル以外でも使用できます。
これに合わせて、Runeを使用できるJavaScript/TypeScriptファイルである.svelte.js
/.svelte.ts
ファイルが登場しています。
これを利用してStoreを使わずに状態管理を行うことができるようになりました。writable
は$state
に、derived
は$derived
に概ね置換できますが、注意が必要なのは2点です
$state
の中身はオブジェクトにラップする必要がある
// これはNG
// export const point = $state(0);
// オブジェクトをexportする
export const point = $state({
value: 0
});
$derived
は直接exportできない
// これはNG
// export const pointDouble = $derived(point *2);
// カプセル化が必要
const _pointDouble = $derived(point *2);
export const pointDouble = () => _pointDouble
svelte/store
自体は廃止になったわけではなく、Storeが必要なワークロードのために引き続き利用できますが、そうではないケースではRuneを利用した方が一貫性があり簡便だと思います。
イベント周りの変更
on:
キーワードを削除
イベントリスナーはプロパティになったためon:
キーワードが不要になりました。on:click
の場合は:
を削除します。
<!-- <Button type="button" outline={true} color="dark" on:click={allReset} > -->
<Button type="button" outline={true} color="dark" onclick={allReset} >
カスタムイベントの発行
Svelte 4ではカスタムイベントを発行する場合、createEventDispather()
を利用していました。
Svelte 5ではイベントの関数をプロパティとして渡す必要があります。
Svelte 4
export let counterParameter: CounterParameters
const dispatch = createEventDispatcher()
const deleteClick = () => {
dispatch('delete', counterParameter.id);
}
Svelte 5
interface CounterFrameProps {
counterParameter: CounterParameters
deleted: (id: number) => void
}
let {counterParameter, deleted, changed}: CounterFrameProps = $props()
const deleteClick = () => {
deleted(counterParameter.id);
}
delete
が予約語だったのでイベント名を変更しています。
当然、受け取り側も変更が必要になります。
Svelte 4
<script lang="ts">
const deleteCard = (event: CustomEvent) => {
counters = counters.filter((counter) => counter.id != event.detail)
}
</script>
<CounterFrame bind:counterParameter={counter} on:delete={deleteCard} />
Svelte 5
<script lang="ts">
const deleteCard = (id: number) => {
counters = counters.filter((counter) => counter.id != id)
}
</script>
<CounterFrame bind:counterParameter={counter} deleted={(id) => deleteCard(id)} />
これはそこそこインパクトのある変更かもしれません。
シンプルなプロパティとカスタムイベントを一つの$props
として纏めるのは慣れが必要になりそうです。
その他の変更
<slot>
の代わりに{#snippet}
/{@render}
を使用する
<slot>
は廃止され、{#snippet}
/{@render}
を使うように変更されました。{#snippet}
/{@render}
自体は再利用可能なマークアップを{#snippet}
で宣言し、{@render}
で描画する仕組みですが、{#snippet}
はSnippet型としてプロパティで受け取れるためこれを利用して<slot>
から置き換えます。
また、コンポーネントタグ内で{#snippet}
で宣言されていない記述は自動的にchildren
というプロパティで渡されます。
<slot>
といえばSveltekitを利用している場合+layoutで使われていますが、自動的にchildren
として渡される仕組みを利用していますので、+layoutの修正だけでOKです。
Svelte 4
<slot />
Svelte 5
<script lang="ts">
import type { Snippet } from 'svelte';
let { children }: {children: Snippet} = $props();
</script>
{@render children()}
もちろん、+layout以外で
<svelte:component>
が不要に
コンポーネントを動的に変更したい場合、Svelte 4では<svelte:component>
を利用する必要がありました。
Svelte 5では直接記述できるようになりました。
Svelte 4
<script lang="ts">
$: counter = counterType($rule) === CounterType.score ? ScoreCounter : SimpleCounter
</script>
<svelte:component this={counter} bind:counterParameter on:changed />
Svelte 5
<script lang="ts">
const Counter = $derived(counterType($rule) === CounterType.score ? ScoreCounter : SimpleCounter)
</script>
<Counter bind:counterParameter changed={(id) => changed(id)} />
bind:
を使用するプロパティは$bindable()
が必要に
子コンポーネント→親コンポーネントにデータを流したい場合、Svelte 4ではタグにbind:
を指定するだけでしたが、Svelte 5ではbind:
を指定できるプロパティを$bindable()
を使って明示する必要があります。
子
// counterParameterだけがbindできる
let {counterParameter = $bindable(), deleted, changed} = $props()
親
<!-- 親側の変更は不要 -->
<CounterFrame bind:counterParameter={counter} deleted={(id) => deleteCard(id)} />
bind:value={entry}
→ bind:value=array[i]
に
こちらはmigretion guideでは該当の説明が見つけられなかったのですが、デバッグ実行時にエラーとなりました。
Svelte 4では{#each}
ブロックで展開された引数をbind:
先に指定できたのですが、Svelte 5では展開前の配列に対して指定する必要があるようです。
<!-- {#each counters as counter, index}
<CounterFrame bind:counterParameter={counter} changed={(id) => pushUndoStack(id)} deleted={(id) => deleteCard(id)} />
{/each} -->
{#each counters as _, index}
<CounterFrame bind:counterParameter={counters[index]} changed={(id) => pushUndoStack(id)} deleted={(id) => deleteCard(id)} />
{/each}
今回のコードではブロック引数はもはや不要になったので_
で破棄しています。
参考に、表示されたメッセージとエラーに関するリンクを貼っておきます。
Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`)
https://svelte.dev/docs/svelte/compiler-errors#each_item_invalid_assignment
Sveltekit固有の変更
vitePreprocessの取得元パッケージが@sveltejs/vite-plugin-svelteに
Svelte 5からTypeScriptがネイティブサポートになったためか、SveltekitにはvitePreprocessが含まれなくなりました。
TypeScriptを利用するだけであればvitePreprocessは不要になったようですが、引き続き利用する場合は@sveltejs/vite-plugin-svelteパッケージを取得し、svelte.config.jsを修正する必要があります。
おわりに
手作業でのマイグレーションを通してSvelte 4から5への主な変更点を確認していきました。
ほとんどの箇所はマイグレーションガイドを元に修正を進められましたが、一部変更に対してどう修正すべきなのかが理解できず、ガイド外のSvelte 5のドキュメントを確認した部分もあったので、これから同様に手作業でマイグレーションをしようとしている方の参考になれば幸いです。
Svelteは記述量が少なく学習コストが比較的低いため、開発を始めやすいのが特徴の一つでした。
Svelte 5で若干覚えることは増えた印象ですが、可読性・分かりやすさとのバランスは良くなった印象です。
Runeや.svelte.js
/.svelte.ts
ファイルの活用により、コードの可読性と再利用性が向上し、状態管理がより直感的になりました。また、イベント処理やコンポーネント間のデータ受け渡しの方法が整理され、より一貫性のある設計が可能になったのではないでしょうか。