Svelte Tutorial を Svelte5 でやっていく

## はじめに
- フロントエンドはAndroidとFlutterをひとかじりくらい
- Svelte自体も初学者
- そろそろSvelte5リリースされるっぽいしチュートリアルもそっちでやってみよう

まだSvelte5はプレビュー版。
参考: https://qiita.com/oekazuma/items/ab617096af10ad94356e

執筆現在では svelte@5.0.0-next.111  を利用しています。

書き換えが必要な章についてのみ言及します。


## Part1: Basic Svelte
### Reactivity
#### Declarations

```svelte
<script>
	let count = $state(0);
	const doubled = $derived(count * 2);

	function increment() {
		count += 1;
	}
</script>

<button onclick={increment}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>

<span>Doubled: {doubled}</span>
```

`$: let` は `let = $state` `let = $derived` で置き換え。

ちなみに `$state` などSvelte5な機能を記述すると自動的に Runes mode となるので `$: let` の利用など非推奨のものはlintエラーが出るようになります。

#### Updating arrays and objects

```svelte
<script>
	let numbers = $state([1, 2, 3, 4]);

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

	let sum = $derived(
        numbers.reduce((total, currentNumber) => total + currentNumber, 0)
    );
</script>

<p>{numbers.join(' + ')} = {sum}</p>

<button on:click={addNumber}> Add a number </button>
```
ポイント: Svelte4の `$: let` と異なり、 `$state` はデフォルトで `deeply reactive` です。

Svelte 5 preview docsより:
> Objects and arrays are made deeply reactive

Svelte 4 の LEARN.SVELTE.DEV より
> Because Svelte's reactivity is triggered by assignments, using array methods like push and splice won't automatically cause updates. For example, clicking the 'Add a number' button doesn't currently do anything, even though we're calling numbers.push(...) inside addNumber.

そのため再代入なしでもpush/popなどをするだけでnumbersが置換されます。

個人的にはこれは大きな変更で、Svelte4までで「再代入されない限り更新されない」ことを利用したコードを書いていた場合、その代替を考える必要がありそうです。例えばasync/joinで多数の変更を待ち合わせ、最後に値を更新してユーザーに見せる、などの場合がありそうです。

そのような用途は [`$state.frozen`](svelte-5-preview.vercel.app/docs/runes#$state-frozen) を用いることで明確に宣言できそうです。更新もpushなどを利用せずに再代入のみによって行われます。
これはつまり、JavaScriptでは破壊的変更である `push` を用いても `$: let` 宣言した変数っぽいものは更新されないなぁという挙動の差異がなくなるということで、わかりやすくなったのではないでしょうか。

```svelte
<script lang="ts">
	let numbers = $state.frozen([1, 2, 3, 4]); // 

	function addNumber() {
		numbers.push(numbers.length + 1);
	}
</script>
```
と記述すると次のエラーが見られます:

> Property 'push' does not exist on type 'readonly number[]'.js(2339)

#### Declaring props

```svelte:Nested.svelte
<script>
	let { answer } = $props();
</script>

<p>The answer is {answer}</p>
```

Svelte4 LEARN.SVELTE.DEV には次のように書かれています:
> NOTE: Just like $:, this may feel a little weird at first. That's not how export normally works in JavaScript modules! Just roll with it for now — it'll soon become second nature.

この `weird` が解消されたということになりますね。
しかし `export` がないと（コード的に）どんなpropsが定義されているのかわかりにくくない？と思ったのですが、次のようにこれは杞憂でした。

```svelte
<script>
	let { answer } = $props();
    let { answer2 } = $props();
</script>
```
> Cannot use $props() more than once | svelte(duplicate-props-rune)

`$props` は１度しか利用できないのですね！
ならば常にファイルの先頭にpropsを記述するようにすれば見通しも良くなりそうです。

#### Default values

これはちょっと悩ましいです。
`$props` の説明 https://svelte-5-preview.vercel.app/docs/runes#$props によると、

```svelte:Main.svelte
<script>
	import Nested from './Nested.svelte';
</script>

<Nested answer={42} />
<Nested />
```

```svelte:Nested.svelte
<script lang="ts">
	let { answer = 'a mystery' } = $props();
</script>
```

と記述すれば良いことになっていますが、これは実際には次の(lint?)エラーを出します。（実行可能）

> Type 'number' is not assignable to type 'string'.

代わりに次のように実装すれば

```svelte:Nested.svelte
<script lang="ts">
	let { answer = 'a mystery' }: { answer?: number | string } = $props();
</script>
```

エラーはなくなりますがこんな記述をしたくはなさそう。
そもそもこの程度でunion typeになるような実装を回避するべきな気はしています。

```svelte:Nested.svelte
<script lang="ts">
	let { answer }: { answer?: number } = $props();
</script>

<p>The answer is {answer || 'not known'}</p>
```

このやり方でも型定義が必要になりますが、デフォルトで表示したいもののためだけにunionになることは回避できます。

もちろん、より複雑な場合においては複数の型を入力したいこともあるでしょうし、その場合はより複雑な型定義をpropsに対して設定するのも良い気がしています。
例えばコールバック関数を受けるようにすることもできそうです。

```svelte:Nested.svelte
<script lang="ts">
	type Props = {
		answer?: number;
		onClick?: () => void
	};
    // `onclick` はnullであってはならないので利用するコンポーネントでデフォルト値を導入する
	let { answer, onClick = () => {} }: Props = $props();
</script>

<p onclick={() => {onClick()}}>
	The answer is {answer || 'not known'}
</p>
```

これにより次のようにonClickを設定できます。

```svelte:Main.svelte
<script>
	import Nested from './Nested.svelte';
</script>

<Nested answer={42} />
<Nested onClick={() => {alert("clicked!")}}/>
```

JS/TSの型定義は初心者なのでもっとイケてるやり方はあるかもしれません。

### Events
#### Event modifiers
Event modifierはSvelte5からはなくなり、自前でやってねという形

```svelte
<script>
	function once(fn) {
		return function (event) {
			if (fn) fn.call(this, event);
			fn = null;
		};
	}
</script>

<button onclick={once(() => alert('clicked'))}>
	Click me
</button>
```

とはいえ明瞭さはなくなった感はある　よく使うものであればライブラリに純粋な関数の状態で良いから含めておけばよかったのではないだろうか…
おそらく外部ライブラリで代替になるものがあるor出てくるだろうと思った

#### Component events

```svelte:Main.svelte
<script lang="ts">
	import Inner from './Inner.svelte';

	type Item = {
		id: string,
		message: string
	}
    
    let items: Array<Item> = $state([])

	function handleMessage(message: string) {
		console.log(message);
		items.push({id: message, message: message})
	}
</script>

<Inner sayHello={handleMessage} />

{#each items as item (item.id)}
	<p>{item.message}</p>
{/each}
```
```svelte:Inner.svelte
<script lang="ts">
	let { sayHello }: { sayHello: (message: string) => void } = $props();
</script>

<button onclick={() => sayHello('Hello!')}> Click to say hello </button>
```

もとのサンプルのようにInner側でメッセージを設定するならこのようなコールバック関数になりそう。

**FYI: `$host`の利用**
Deprecations の項にも書いてあるように、
https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher
custom elementであれば `$host().dispatchEvent` を利用することができそうです。

ざっと調べた感じ、もともとSvelteでWeb Componentsをやる場合イベント周りが独特だったようで(`createEventDispatcher`の都合で)、これが標準的な `dispatchEvent` を利用するようになることで改善したのではなかろうか？

#### Event forwarding
こちらもcreateEventDispatcherのdeprecationとともに糖衣構文もdeprecatedなので、単純にpropsにコールバックをふくめ、それをバケツリレーしていきます。

##### そのままやる場合
Svelteではpropsと変数が同名の場合は `<Foo {bar} />` の形で（この場合は `bar={bar}` を）記述できるので、Tutorialのお題をそのままやるとこうなります

```svelte:Main.svelte
<script>
	import Outer from './Outer.svelte';

	function handle(message) {
		alert(message);
	}
</script>

<Outer eventHandler={handle} />
```
```svelte:Outer.svelte
<script>
	import Inner from './Inner.svelte';

    let { callback } = $props();
</script>

<Inner {callback} />
```
```svelte:Inner.svelte
<script>
	let { callback } = $props();

    function onclick() {
        callback("Hello")
    }
</script>

<button {onclick}>
	Click to say hello
</button>
```

##### Typeをちゃんと伝播させてみる
↑でも良かったんですが、
- 孫コンポーネントで利用するコールバックの形がわからなくなる
- `callback` みたいに孫ではシンプルなattributeをShorthand attributesで上まで伝播するのはあとから混乱を招きそう

なので多少冗長に記述したほうが良いのではと思いました。

```svelte:Main.svelte
<script>
	import Outer from './Outer.svelte';

	function handle(message) {
		alert(message);
	}
</script>

<Outer innerProps={{ callback: handle }} />
```
```svelte:Outer.svelte
<script context="module" lang="ts">
	export type Props = {
		innerProps: InnerProps;
	};
</script>

<script lang="ts">
	import Inner, { type Props as InnerProps } from './Inner.svelte';

	let { innerProps }: Props = $props();
</script>

<Inner {...innerProps} />
```
```svelte:Inner.svelte
<script context="module" lang="ts">
	export type Props = {
		callback: (message: string) => void;
	};
</script>

<script lang="ts">
	let { callback }: Props = $props();

	function onclick() {
		callback('Hello');
	}
</script>

<button {onclick}> Click to say hello </button>
```
ポイントは2点:
- 下端のコンポーネントのProp型を伝播させることで、利用する箇所で困らない
- `<Inner {...innerProps} />` のようにPropsをspreadするので冗長性を抑え、コード変更にも耐えられる

### Bindings
inputやtextareaのbindingについてはSvelte 4までと変わるところはなさそうですね。

### Lifecycle
#### onMount
`onMount()` の代わりに `$effect()` を利用します。

```diff_svelte
<script lang="ts">
-	import { onMount } from 'svelte';
	import { paint } from './gradient.js';

-	onMount(() => {
+	$effect(() => {
		const canvas = document.querySelector('canvas');
		const context = canvas.getContext('2d');

		let frame = requestAnimationFrame(function loop(t) {
			frame = requestAnimationFrame(loop);
			paint(context, t);
		});

		return () => {
			cancelAnimationFrame(frame);
		};
	});
</script>
```

ちなみに同時に `onDestroy()` も deprecated っぽいんですが（`$effect()` 内の `return function` で対応）、実際にdeprecatedと書かれているわけではないです。
`onDestroy()` はSSRでも機能するのですが `$effect()` はそうではないっぽいので。

##### 追記:

Svelte 5 Preview documentによると
> Additionally, you may prefer to use `effect`s in some places where you previously used `onMount` and `afterUpdate` (the latter of which will be deprecated in Svelte 5). There are some differences between these APIs as `$effect` should not be used to compute reactive values and will be triggered each time a referenced reactive variable changes (unless using `untrack`).

ということで、onMount/onDestroy自体はdeprecatedではなさそうでした。
ただし特性は異なるものの `$effect` の利用が良さそうですね。

ついでのように `untrack` に付いて触れられてますが `untrack` も結構重要な関数ですね。 `$effect` や `$derived` の中で別の `$state` をトリガーとして扱わないようにするための関数です。
https://svelte-5-preview.vercel.app/docs/imports#svelte-untrack

```svelte
<script>
	import { untrack } from 'svelte';

	let a = $state(1);
	let b = $state(1);
	let c = $derived(a * untrack(() => b));
</script>

A: <input type="range" bind:value={a} min="1" max="10" />
B: <input type="range" bind:value={b} min="1" max="10" />
<p>{c}</p>
```
この様に記述すると以下のような出力になりますが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58443/d8ba7f6b-d8a7-e6a5-011d-aa7b86067938.png)

Aを変更すると結果がすぐ更新されるものの、Bを変更しても結果は変更されません。
しかしBの値は実際には変わっているので再度Aを変更するとAとB両方の変更が適用されます。
言い換えると `$derived` のトリガーから `b` が外されたわけですね。

（しかし `untrack(() => b)` ってもうちょっとどうにかならんのかな）

#### `beforeMount` and `afterMount`
`beforeMount` `afterMount` は明確にdeprecatedです。

ちょっと長いのでscriptの部分だけ書きますと、

```svelte
<script>
	import Eliza from 'elizabot';

	let div;
	let autoscroll = false;
	let comments = $state.frozen([]);

	const eliza = new Eliza();
	const pause = (ms) => new Promise((fulfil) => setTimeout(fulfil, ms));

	const typing = { author: 'eliza', text: '...' };

	$effect.pre(() => {
		comments;
		if (div) {
			const scrollableDistance = div.scrollHeight - div.offsetHeight;
			autoscroll = div.scrollTop > scrollableDistance - 20;
		}
	});

	$effect(() => {
		comments;
		if (autoscroll) {
			div.scrollTo(0, div.scrollHeight);
		}
	});

	async function handleKeydown(event) {
		if (event.key === 'Enter' && event.target.value) {
			const comment = {
				author: 'user',
				text: event.target.value
			};

			const reply = {
				author: 'eliza',
				text: eliza.transform(comment.text)
			};

			event.target.value = '';
			comments = [...comments, comment];

			await pause(200 * (1 + Math.random()));
			comments = [...comments, typing];

			await pause(500 * (1 + Math.random()));
			comments = [...comments, reply].filter((comment) => comment !== typing);
		}
	}
</script>
```

のようになるはずです。

今回触っていて意外だったのは `$state` の値への再代入では思ったように動かないということです。

```js
let comments = $state([])

...

comments = [...comments, comment];
comments = [...comments, typing];
comments = [...comments, reply].filter((comment) => comment !== typing);
```

のようにして `$inspect({ comments });` を用いて変化を追ってみると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58443/d5c541db-874a-0dd6-9081-09a80daf991b.png)
と、`typing` をfilterできていないことがわかります。

一方 `$state.frozen` を利用した場合は
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58443/7c227da1-2e5c-0096-b79f-fb3f87724f7c.png)
となり、期待通りにfilterができています。

`$state` で値を保存する際にオブジェクトのコピーが行われていて参照先が変わってしまうため、単純な `==` や `===` での比較ではオブジェクトの同一性が `false` になってしまうのだと思われます。
例えばLodashで `_.isEqual(obj1, obj2)` を利用するなどすれば `$sate` 再代入とfilterでも行けそうですが、このような場合は素直に `$state.frozen` を利用するのが得策っぽいですね。

### Stores
#### Writable stores
`store` はSvelte 5でも続投ですが、 `$state` で置換可能だという言及はブログや「試してみた」系ぶちらほら見られました。（[redditとか](https://www.reddit.com/r/sveltejs/comments/16sgmk3/runes_should_replace_stores_not_simple_variables/)）
一方でSvelte 5 preview docsには
> There are no plans to deprecate onMount or stores at the current time.

https://svelte-5-preview.vercel.app/docs/faq#breaking-changes-and-migration-no-but-really-am-i-going-to-have-to-rewrite-everything

と記載があるだけで、いまいち要領がつかめませんでした。
最終的には以下のように `$state` を用いて解決可能でした 。

```svelte:App.svelte
<script>
	import { store } from './stores.svelte.js';
	import Incrementer from './Incrementer.svelte';
	import Decrementer from './Decrementer.svelte';
	import Resetter from './Resetter.svelte';

	// (1)
	let count = $derived(store.counter);

	// (2)
	$effect(() => {
		console.log(store.counter);
	});
</script>

<h1>The count is {count}</h1>

<Incrementer />
<Decrementer />
<Resetter />
```
```svelte:Incrementer.svelte
<script>
	import { store } from './stores.svelte';

	function increment() {
		store.counter++;
	}
</script>

<button on:click={increment}>
	+
</button>
```
```svelte:stores.svelte.js
let counter = $state(0);

export const store = {
    get counter() { return counter },
    set counter(v) { counter = v },
};
```
(Decrementer, Resetterは省略)

**App.svelte内にコメントを残した箇所について**

もとのコードは次のようになっています
```js
	count.subscribe((value) => {
		count_value = value;
	});
 ```

これはstoreのsubscribe機能を用いて通常ならば `$:` で行うような処理を行っています。が、 `$: {}` の利用はSvelte 5ではdeprecatedなのは前述のとおりで、同じようにsubscribeの利用も抑えられます。
新しい  `store.counter` は `$state` なので他の値を更新するときは(1)のように `$derived`、副作用を発生させたい場合は(2)のように `$effect` を利用できます。

ちなみにこの問題ではそもそも `store.counter` をそのまま利用できます。（store.counterがすでにreactive）
```svelte
<h1>The count is {store.counter}</h1>
```

ちなみに `stores.svelte.js` で直接 `$state` な変数をexportしても意図したようには利用できません。
```svelte:stores.svelte.js
export let counter = $state(0);
```
```svelte:Decrementer.svelte
<script>
    import { counter } from './stores.svelte';

    function decrement() {
        counter--;  // Cannot assign to 'counter' because it is an import.  [js(2632)]
    }
</script>
```
これはimportしたものはconstantになるからですね。

ポイントをまとめると、
- storeはrunes modeならstateで代用可能
- runesは `*.svelte` `*.svelte.js/ts` ファイルでしか利用できない
    - ストア用のファイルを切り出すなら、 `.js/ts` の代わりに `.svelte.js/ts` にすれば良い
- `$state` は直接exportできない
    - getter/setterを利用する


#### Readable stores
こちらも `$state` runeを用いて実装可能ですが、結構再発明っぽくなってしまいそうです。
将来的にはstoreもrunesで用意されるようになるのかもしれませんが、いくつか先のメジャーアップデートになると思われます。

公式Discordサーバーで紹介されていた `$state` による実装を紹介すると以下のようになります。
[実装のplayground](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE5VVbY-jNhD-K1NuJUCXJdeX64dssqtK3W_9B83plsCQ9a2xkT3kGiH-ewdjCOSltxVKAM8843memTFNUAiJNlj93QQqLTFYBX9UVbAI6Fh1L_aAkpDfra5N1q2sbWZERY9btSVRVtoQ_ImlhsLoEsJk2b0kPSx8YCd2k0hwEFbsJMIG7iylhBGZGmN2WC9PAdV6VxNpBVplUmRvmyaKYfM4Af_kH1u3P-n9XmIXo8e5GM0HUQyItvNau_yWbGyWomiZTKlzUQjMg1WXRbsYuXee_5N8A2lG4oDPEktUBO2oRE1CWi9F8s06Naj7LZdAr8JCjlLs0LAa8gi5RqtCgswgL7ADghSWUKGxC4_aYZbWFnu0FApBF5DpHOG7kBIUHtCAwXtTO90zrayWmOyNrqsoFEpQGD9MLVLvo1n6SVYbw_e5mwvwrPIo9hW9w6LAjKK-PI0jNtL62lH5epVIAlybo6456dKp5QC6psUYIpVa7ZkPvTroS5MkSfvC1KX-vuiWjp6sJl70u-Q9_Ixwn6Sn_G7St2hTe9mu1WMfaAXN1YhPSSXTDF-1zLkyT0_cFkkStutlxfi1UFVNMPHYbIOft8Et0y-3Tb_eNv122_T5tul3Z2KjpaPEvt-dm691LiwDjivYSZ29ed3K1OyFWsGn7ko-Ywm9bk41Fwfgv6ZvOjCnKfxmpxOI_7ip60pEZ5O34dZP85THPsp1Vrs6zDwWEGWplLs0ezv17bQrQtdNgvtv7Nihe7wnQV3lXVtvoO_9IeCNLU_okz3Pnw_88JffIgoLNlmhwoUP3oN-BOCpmSMYY5Bqo2A2ljOChg-4w1WCp_2cD74rx3ehLhKllv_bPueiVqwXH_lj7bpjSqRyAfydMBT3NNwnJJX15APi_eLJR2YkxV6fHoZVS7qa6-OV2bPRD2oUD4tzwV78Seerym58Xt0156vtSzyghzsfdNG522STG2foCTty-fgRNhvmE899ttTxYqJOpig6XIbprkG0w8Pc0sazhbbXZ7iu9dE0ufv7idRXk-vTe0qi-Hzj820vE5ok4xNxLLy9HTvo8iz50v4L1pqNPtEIAAA=)
```js
function readable(initial, start) {
	let value = $state(initial);

	let listeners = 0;
	let stop;

	return {
		get current() {
			if ($effect.active()) {
				$effect(() => {
					if (listeners++ === 0) {
						stop = start((v) => {
							value = v;
						});
					}

					return () => {
						if (--listeners === 0) {
							stop?.();
						}
					}
				});
			}

			return value;
		}
	};
}
```

runes mode対応のライブラリなどが出回りそうな気がしますが、とりあえずSvelte 5の間は購読管理と初期化・終了時処理が必要になる複雑なstoreは従来通りstoreを用いると理解しておくと良さそう。

#### Derived stores
問題のベースがReadable storesなのでStore bindingsのものでやってみると

```svelte:App.svelte
<script>
	import { states } from './stores.svelte.js';
</script>

<h1>{states.greeting}</h1>
<input bind:value={states.name} />

<button onclick={() => states.name += '!'}>
	Add exclamation mark!
</button>
```
```js:stores.svelte.js
let name = $state('world');
const greeting = $derived(`Hello ${name}!`);

export const states = {
    get name() { return name },
    set name(value) { name = value },

    get greeting() { return greeting }
}
```

のように実現できます。単純なstoreは `$state` で代替可能で、 `derived` も他のSvelteファイルのように `$derived` で実現可能です。今回もgetter/setterを必要に応じてエクスポートする必要がある点だけ注意が必要です。


---

これで一通り Part1: Basic Svelte の章が終わりました… storeのあたりで結構手間取ってしまいましたが、Svelte5の新機能は概ね（SSR関連以外は）触れることができたと思います。

チュートリアルで触れられなくて大事そうなのは
[svelte/reactivity](https://svelte-5-preview.vercel.app/docs/imports#svelte-reactivity) とかかしら