Svelte(Kit)の世界: Runes (おまけ) #6


## はじめに

はじめまして、WEB フロントエンドエンジニアの nuintee です。

この度かねてより関心があった Svelte に入門する覚悟が出来たので、
その学習過程をアドベントカレンダーにしてみました。

Svelte に少しでも興味のある方は、ぜひご覧ください。

## フロントエンドの世界 2024 について

「[フロントエンドの世界 2024](https://qiita.com/advent-calendar/2024/frontend-assort-2024)」は普段 [`Next.js`](https://nextjs.org/) を書いている筆者が、同じフロントエンドライブラリである [`Svelte(Kit)`](https://svelte.jp/docs/introduction), [`Remix`](https://remix.run/docs/en/main) ,[`SolidJS`](https://docs.solidjs.com/), [`Qwik(City)`](https://qwik.dev/)の 4 つにアソート形式で触れ、理解を深めていく様子を収めたアドベントカレンダーです。

![frontend-assort-2024-banner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3294810/7666d209-1aef-9ce5-c298-d8e60847e830.png)

## もくじ

- [はじめに](#はじめに)
- [フロントエンドの世界 2024 について](#フロントエンドの世界-2024-について)
- [もくじ](#もくじ)
- [Runes とは？](#runes-とは)
- [Runes で変わること](#runes-で変わること)
- [Runes で書き直してみる](#runes-で書き直してみる)
- [おわりに](#おわりに)

## Runes とは？

Svelte v5 で導入された「Runes」は、リアクティブな状態管理を明示的かつ柔軟に行うための新しい構文です。

従来の Svelte では、トップレベルで宣言された変数や`$:`ラベルを用いてリアクティビティを実現していましたが、これらは`.svelte`ファイル内に限定され、`.js`ファイルでの利用が困難でした。

Runes は、`$state` や`$derived` などの関数を提供し、`.svelte` ファイル外でも一貫したリアクティブな状態管理を可能にします。これにより、コードの可読性と保守性が向上し、複雑なアプリケーションでも直感的にリアクティビティを扱えるようになりました。

(参考: [Svelte v5 で導入された Runes によるリアクティビティシステム](https://azukiazusa.dev/blog/svelte-reactivity-system-with-runes/#statesnapshot))

## Runes で変わること

**1. リアクティブ値の判別 (可読性)**
Runes では、リアクティブな値を明示的に定義することで、どの値がリアクティブであるかを直感的に理解できるように

従来の Svelte では、単に変数を宣言するだけではリアクティブかどうかが明確ではありませんでしたが、Runes の$state を使用すると、その値がリアクティブであることが一目で分かります。

**2. js ファイルでのリアクティブ宣言**

従来の Svelte では、`.svelte`ファイル内でトップレベルに宣言された変数のみがリアクティブに扱われ、`.js`ファイルで宣言された変数はリアクティブではありませんでした。

しかし Runes は、`$state`や`$derived`などの関数を提供し、.svelte ファイル外でも一貫したリアクティブな状態管理を可能にします。

**3. エフェクトの管理の強化**

従来の Svelte では、`onMount`や`beforeUpdate`といったライフサイクル関数を使用して副作用を管理していました。

Runes では、`$effect`関数を使用してエフェクトを定義します。これにより、特定の値が変更されたときや、コンポーネントが DOM にマウントされたときにコードを実行する必要がある場合に、より直感的にエフェクトを管理できるようになりました。

**4. プロパティの受け渡しの簡素化**

従来、コンポーネント間でプロパティを受け渡す際には、`export let`を使用していました。

Runes では、`$props`関数を使用してプロパティを受け渡します。これにより、プロパティのリネームやスプレッド操作が容易になり、コードの簡潔さが向上しました。

## Runes で書き直してみる

:::note warn
この章は [Svelte(Kit)の世界: データ取得と状態管理 #4](https://qiita.com/nuinteedev/private/82526d297b8fc113fed6)までの章を完了していることを前提としています。
:::

### Runes を有効化する

`svelte.config.js`の`compilerOptions`で`runes: true`を 設定します。

```diff_javascript:svelte.config.js
import adapter from "@sveltejs/adapter-cloudflare";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://svelte.dev/docs/kit/integrations
  // for more information about preprocessors
  preprocess: vitePreprocess(),
+ compilerOptions: {
+   runes: true,
+ },
  kit: {
    // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
    // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
    // See https://svelte.dev/docs/kit/adapters for more information about adapters.
    adapter: adapter({
      routes: {
        include: ["/*"],
        exclude: ["<all>"],
      },
      platformProxy: {
        environment: undefined,
        experimentalJsonConfig: false,
        persist: false,
      },
    }),
  },
};

export default config;
```

### リアクティブ変数を Rune 化 ($derived)

元々`$:`で定義されていたリアクティブ変数を `$derived` の形式で書き直して Rune 化します。

`$:`の代わりに `let` を使用し、変更監視対象を`$derived`関数に渡します。

(参考: [$derived](https://svelte.dev/docs/svelte/$derived))

```diff_svelte:+page.svelte
<script lang="ts">
...
- $: displayTexts = $formState.displayTexts
+ let displayTexts = $derived($formState.displayTexts);

- $: options = $formState.answerOptions.filter(
+ let options = $derived($formState.answerOptions.filter(
    (option) =>
      !displayTexts.some(
        (display) => JSON.stringify(option) === JSON.stringify(display)
      )
  )
);
</script>
```

また[Svelte 公式 VSCode 拡張](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode)が入っている場合、Runes が有効の場合に構文チェックをしてくれるので便利です。

![スクリーンショット 2024-11-29 1.19.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3294810/b9040cdd-1678-0565-36a5-e6db0796405f.png)

### 非同期データを Rune 化 ($state)

非同期データの取得状態を保っていた変数を`$state` で Rune 化します。

(参考: [$state](https://svelte.dev/docs/svelte/$state))

```diff_svelte:+page.svelte
<script lang="ts">
...
- let pokemonData: PokemonResponse | null = null;
- let isLoadingPokemon = false;
- let error: string | undefined = ""; // エラーハンドリング用
+ let pokemonData: PokemonResponse | null = $state(null);
+ let isLoadingPokemon = $state(false);
+ let error: string | undefined = $state(""); // エラーハンドリング用
...
</script>
```

### props 定義 を Rune 化 ($props)

元々 `let` を `export` する形で props 定義していたものを `$props`関数呼び出しで定義が行えるようになります。

props の中身はオブジェクトで返ってくるので分割代入やレスト構文での取り出しが可能です。

**型について**
また props に型を付ける場合は以下のように書きます。
`let [propsのオブジェクト]: [型] = $props()`

**注釈**

※ props を受け取っているコンポーネントは他にもありますが、同様の記述で Rune 化出来るので割愛しています。

```diff_svelte:ProgressBar.svelte
<script lang="ts">
- export let maxTime;
- export let time;

+  let { maxTime, time }: { maxTime: number; time: number } = $props();
</script>
```

### ストアを Rune 化

`writable` でストア定義していたものを `$state` を用いた定義に変更します。

(参考: [$state](https://svelte.dev/docs/svelte/$state))

**ファイル名**
ストアのファイル名の拡張子は`.svelte.ts`にする必要があります。
ここでは`form.svelte.ts`とします。

**更新メソッドの定義**
`$state` に対応した値の更新処理は再代入で行うことができます。

```diff_typescript:form.svelte.ts
- import { writable } from "svelte/store";
import type { AnswerType, FormState } from "../types/form";

const INITIAL_FORM_STATE: FormState = {
  displayTexts: [],
  answerOptions: [],
};

- export const formState = writable<FormState>(INITIAL_FORM_STATE);
+ export const formState = store(INITIAL_FORM_STATE);

+ function store(init: FormState) {
+   let store = $state(init);
+
+   return {
+     get get() {
+       return store;
+     },
+     setAnswerOptions(newOptions: AnswerType[]) {
+       store.answerOptions = newOptions;
+     },
+     removeInput(id: string) {
+       store.displayTexts = store.displayTexts.filter(
+         (display) => display.id !== id
+       );
+     },
+     addInput(char: AnswerType) {
+       store.displayTexts = [...store.displayTexts, char];
+     },
+     initForm() {
+       store = INITIAL_FORM_STATE;
+     },
+   };
+ }
```

**呼び出し側**
`$state`によるストア定義の場合、ストア名の前の`$`は不要になるので削除し、ストア値は `get` で参照します。

```diff_typescript:+page.svelte
<script lang="ts">
...
+ import { formState } from "../stores/form.svelte"
...
// $を削除しgetでストア値参照
- let displayTexts = $derived($formState.displayTexts);
+ let displayTexts = $derived(formState.get.displayTexts);

// $を削除しgetでストア値参照
- let options = $derived($formState.answerOptions.filter(
+ let options = $derived(formState.get.answerOptions.filter(
+    (option) =>
+      !displayTexts.some(
+        (display) => JSON.stringify(option) === JSON.stringify(display)
+      )
+  )
+);

...
if (state === "START") {
  try {
-   initForm();
+   formState.initForm();
...
-   setAnswerOptions(
+   formState.setAnswerOptions(
}

</script>
...
<CharButton
- onClick={() => removeInput(display.id)}
+ onClick={() => formState.removeInput(display.id)}
>{display.char}</CharButton>
...
<CharButton
- onClick={() => addInput(option)}
+ onClick={() => formState.addInput(option)}
>{option.char}</CharButton>
```

## おわりに

Runes の書き方は結構 `React` みがあって馴染みやすかったです。
ただ `$derived.by` や `$effect`等の挙動理解はまだまだなので、
もう少し慣れる必要がありそうです。

また本シリーズを通してお気軽にコメントお待ちしております。
また完走賞も目指しているので是非応援お願いします！

---

この記事は [フロントエンドの世界 Advent Calendar 2024](https://qiita.com/advent-calendar/2024/frontend-assort-2024)の 7 記事目です。
次の記事はこちら [Remix の世界: Remix とは？ #1](https://qiita.com/nuinteedev/private/cf4f63e6473c1c81a2e0)