Svelte5の記法と移行方法

## はじめに

以前Svelteの紹介記事を書きました。

https://qiita.com/kurata04/items/d39e004dc3c837bfc4a1

この記事はSvelte 3について書いたもので、現在の最新はSvelte 5です。
通知が届くたびに嬉しくなる反面、古い情報なのも申し訳なく思っていたので、自分の学習も兼ねてSvelte 5での記法の紹介をしていきます。

この記事では、以前書いた記事の内容に対応するものを中心に基本的な記法を紹介します。比較対象は以前の記事に合わせてSvelte 3です。
詳しく確認したい方は公式の[Svelte 5移行ガイド](https://svelte.jp/docs/svelte/v5-migration-guide)を読むと変更点がわかりやすいと思います。

## Svelte 5について

約18ヶ月の開発期間を経て、2024/10/22にSvelte 5がリリースされました。
開発者体験をさらに向上させるためにSvelte 5は根本から書き直されました。その結果アプリはより高速で軽量になり、信頼性も向上しました。
詳しい背景については以下の公式ブログをご覧ください。
[Svelte 5 誕生](https://svelte.jp/blog/svelte-5-is-alive)

## Svelte 5の記法

根本から変更されたということでSvelte 5では記法も大きく変わっています。その変更点を見ていきましょう。
[チュートリアル](https://svelte.jp/tutorial/svelte/welcome-to-svelte)もSvelte 5用に書き直されています。一部のみ日本語対応されているようです(2024/12/26現在)。
記事の中ではほんの一部しか取り上げていないので、ぜひご自身で試してみてください。

### リアクティビティ(`$state`)

リアクティブな値を宣言する時にSvelte 3では特別な指定は必要ありませんでしたが、Svelte 5では`$state`を使用するようになりました。
この`$state`のような特別な記法は`rune`と呼ばれます。ルーン文字のルーンです。

"truly reactive"を掲げ、リアクティブな値も普通の変数と同じように書けることがSvelteの魅力でした。しかしコードが複雑になるにつれ、どの値がリアクティブであるかの判断が難しくなります。
Svelte 5ではリアクティブな値を明示的に示すことでコードの可読性が向上しました。
特殊な記法は宣言時にのみ必要で、使用時には通常の変数と同じように扱えます。

```js
// Svelte 3
let count = 0;

// Svelte 5
let count = $state(0);

function increment() {
  count += 1; // ここは変わらない
}
```

また、Svelte 3では参照先のオブジェクトを変えるような変更はリアクティブに反映されませんでした(配列に`push()`しても反映されず、再代入やスプレッド構文を使用する必要があった)。
Svelte 5では`push()`でも問題なく反映されます(deep reactivity)。

```js
// 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ファイルでも使用できるので、以下のように記述することも可能です。

```javascript:shared.svelte.js
export const counter = $state({
  count: 0
});
```

```svelte:Counter.svelte
<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`は読み取り専用です。

```js
// 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`を使って引数に関数を渡すことも可能です。

```js
let doubled = $derived.by(() => {
  const double = 2
  return count * double
});
```

`$derived`内では副作用(例: count++のような値の変更操作)は禁止です。

```js
let doubled = $derived.by(() => {
  count++; // 副作用が発生するためエラー
  const double = 2
  return count * double
});
```

### 変数の追跡

リアクティブな値の変化を追跡するために、`console.log()`でログを出力しようとすると「`console.log`に`$state`プロキシが含まれています。」や「メッセージを複製できませんでした」といった警告が表示されます。
これは`$state`で宣言した変数がリアクティブプロキシ(裏で特殊な仕組みで管理されるデータ、例えば変化を検知してUIを更新するなど)として扱われるためです。

回避策としては2つあります。

1. `$state.snapshot(...)`を使用する

    `$state.snapshot(...)`を使用するとスナップショットが作成できます。
    スナップショットはざっくり言うとその時点での変数です。プロキシではないため警告が表示されません。

    ```js
    let numbers = $state([1, 2, 3, 4]);

    function addNumber() {
      numbers.push(numbers.length + 1);
      console.log($state.snapshot(numbers)); // スナップショットを作成
    }
    ```

2. `$inspect`を使用する

    `$inspect`を使用して自動でスナップショットを作成することもできます。
    `$inspect`は依存する変数が変化すると自動で実行されます。開発中のみ機能し、ビルド時には実行されません。

    ```js
    let numbers = $state([1, 2, 3, 4]);

    function addNumber() {
      numbers.push(numbers.length + 1);
    }

    $inspect(numbers); // numbers.push()が実行されたタイミングで実行
    ```

    `$inspect(...).with(fn)`を利用することでカスタマイズも可能です。以下はその一例です。

    - 変数が変更された時にデバッガーを起動する

      ```js
      $inspect(count).with((type, count) => {
        if (type === 'update') {
          debugger; // デバッガーを起動
        }
      });
      ```

    - 状態変更を追跡してスタックトレースを出力する

      ```js
      $inspect(count).with(console.trace());
      ```

### 状態の変化に応じた処理(`$effect`)

`$effect`を使用することで、`$state`や`$derived`の変化に応じて処理を実行することができます。

チュートリアルでの使用例は以下のようになっています。ボタンを押すことでカウントアップが加速・減速するコード例です。
`$effect`の中に`clearInterval()`を入れることでクリック(`interval`が変更される)時に`setInterval()`を解除しています。

```svelte
<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`を利用する方がシンプルで保守性の高いコードになります。
[使用すべきでない例](https://svelte.jp/docs/svelte/$effect#When-not-to-use-$effect)も挙げられているのでこちらも確認してください。

### イベントハンドラ

Svelte 3では`on:event`の形で記述していましたが、Svelte 5では非推奨になり`onevent`での記述が推奨されています。

```svelte
<!-- Svelte 3 -->
<button on:click={handleClick}>Click me</button>

<!-- Svelte 5 -->
<button onclick={handleClick}>Click me</button>
```

関数名とイベントハンドラが一致している場合は省略が可能です。

```svelte
<script>
  let count = $state(0);

  function onclick() {
    count++;
  }
</script>

<button {onclick}>
  clicks: {count}
</button>
```

## Svelte 3からの移行

ここまで変更点を見てきたので、試しにSvelte 3からSvelte 5まで移行してみます。
公式の記載通りに実行していきます。参考にした記事は[参考](#参考)に記載しています。
まずはSvelte 3とSvelteKit 1を使ったプロジェクトを作成します。

```bash
npm create svelte@3
npm install
npm run dev # 実行確認
```

今回は以下の条件で作成しました。

![プロジェクト作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1954468/4a18e4ca-ee6a-b977-2304-73497606fd90.png)


元になるプロジェクトが作成できたので移行していきます。
Svelte 3の場合はまずSvelte 4へ移行する必要があると[ここ](https://svelte.dev/blog/svelte-5-is-alive#How-do-I-upgrade)に書かれているので従います。
移行用のスクリプトがあるので簡単に移行ができます。以下を実行します。

```bash
npx sv migrate svelte-4
```

実行すると`package.json`で`"svelte": "^4.0.0"`になっているはずです。
`eslint-plugin-svelte3`が入っているのですがSvelte 4以降では非推奨ということで[公式の案内](https://svelte.jp/docs/svelte/v4-migration-guide#New-eslint-package)に従って`eslint-plugin-svelte`に切り替えておきます。

```bash
npm rm eslint-plugin-svelte3
npm i eslint-plugin-svelte -D
```

`.eslintrc.cjs`も変更します。

```javascript
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への移行を行います。こちらも移行スクリプトがあるので実行します。

```bash
npx sv migrate svelte-5
```

実行すると先にSvelteKit 2へ移行にするように言われたので言われるがままに実行します。

```bash
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`をつけて実行します。

```bash
# 依存関係を無視してインストール
npm install --legacy-peer-deps
```

SvelteKit 2へ移行できたので、今度はSvelte 5への移行をするために移行コマンドを実行します。

```bash
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)

```bash
npm install --force
npm run dev # 動作確認
```

これでSvelte 5へのアップグレードが完了しました。動作も問題ありません。
`src/routes/Counter.svelte`の中身を確認すると、自動的にSvelte 5の記法に変更されていました。

```svelte:変更前
<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>
```

```svelte:変更後
<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公式](https://svelte.jp/)
  - [概要](https://svelte.dev/docs/svelte/overview)
  - [Svelte 5 誕生](https://svelte.jp/blog/svelte-5-is-alive)
  - [Svelte 4 移行ガイド](https://svelte.jp/docs/svelte/v4-migration-guide)
  - [Svelte 5 移行ガイド](https://svelte.jp/docs/svelte/v5-migration-guide)
  - [SvelteKit 2 移行ガイド](https://svelte.dev/docs/kit/migrating-to-sveltekit-2)

## おわりに

かなり軽く使っていた自分としてはリアクティブ宣言必要になったんだ、という感じですがしっかり使う人にとってはかなり便利な変更がされている印象です。
今回は触れていませんが、コンポーネントなどで複数ファイルを扱う際の変更も色々とあり、今までよりも使いやすくなっているようです。気になる方は[Svelte 5 移行ガイド](https://svelte.jp/docs/svelte/v5-migration-guide)で詳細をご確認ください。
色々変更されてはいますが書き方はシンプルなままなので、より幅広いところで使われることを期待しています。