Open13

Svelte5 を触ってみる

ykrodsykrods

Intro

  • svelte5 は 2024-04-26 現在開発中
  • 最新のリリースが svelte@5.0.0-next.115 でマイルストーン的には現在 68% なので正式リリースはもうちょっと先かと思われる
ykrodsykrods

svelte5 playground が公開されているが、せっかくなので(?)ローカルで動かす

プロジェクト設定

$ # 普通にプロジェクト始める際は npm create vite@latest myproj などでよい
$ mkdir svelte5-demo
$ cd svelte5-demo
$ npm init -y
$ npm install --save-dev vite svelte@next @sveltejs/vite-plugin-svelte

package.json

+  "type": "module",
  "scripts": {
+      "dev": "vite dev",
+      "build": "vite build",
+      "preview": "vite preview"
  },

vite.config.ts

import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte'

export default defineConfig({
  plugins: [svelte()]
})

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>svelte5 demo</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>

この辺は svelte4 と変わりない

ykrodsykrods

main.ts の書き方が変わっている

svelte4 まで

import App from './App.svelte'

const app = new App({
  target: document.getElementById("app"),
});

export default app

svelte5

import { mount } from 'svelte'

import App from "./App.svelte"

const app = mount(App, {
  target: document.getElementById("app"),
})

export default app
  • コンポーネントがクラスではなく関数になった
ykrodsykrods

TypeScript ネイティブサポート

  • Svelte4 では Svelte の preprocess の仕組みを使ってコンパイル前に ts -> js 変換をしていたが、Svelte5 からは Svelte コンパイラが直接 ts を解釈できるようになった
  • ユーザからみたら、コンパイルが早くなったりする??
    • Svelte のライブラリを配布している人は ts のまま配布できるようになったりすると面倒が減ってうれしいかもしれない
ykrodsykrods

Rune

Svelte4 までは特定の js 構文に特殊な意味を持たせていたのを、Svelte5 からは明示的な関数構文で表現するようになった。

Rune の一例

リアクティブな状態の宣言には $state() を使う

- let count = 0
+ let count = $state(0)

function increment() {
  count += 1
}
  • Rune はオプトインなので既存の構文はそのまま使える。
  • Rune は .svlete.js, .svelte.ts ファイルでも(つまり svelte コンポーネント外でも) 以下のようにかける
// Counter.svelte.ts
export function createCounter() {
  let count = $state(0)

  return {
    get count() { return count },
    increment() { return count += 1 }
  }
}
  • (疑問) 既存の store は Rune で書き換え可能?
ykrodsykrods

Reactive Declaration

$derived() を使う

// svelte4
let count = 0
$: doubled = count * 2

// svelte5
let count = $state(0)
const doubled = $derived(count * 2)

Reactive Statement

$effect() を使う

// svelte4
$: console.log(double)

// svelte5
$effect(() => console.log(double))

$effect()

$effect に渡した関数内で同期的に参照している $state$derived が変化するか、コンポーネントがマウントされたときに関数が実行される

<script lang="ts">
  let count = $state(0)

  const sleep = (ms) => new Promise(r => setTimeout(r, ms))

  $effect(() => {
    // count が変化する度に実行される
    console.log(count)
  })

  $effect(() => {
    // countの参照が非同期なので count が変化してもこの effect は実行されない
    sleep(1000).then(() => { count += 1 })
  })
</script>

onMount と同じように、 $effect() に渡す関数で関数をリターンすると、コンポーネントが破壊(アンマウント)されるか、 effect が再実行される前のタイミングでリターンされた関数が実行される

  let count = $state(0)

  $effect(() => {
    const timerID = setInterval(() => { count += 1 }, 1000)
    // コンポーネントが破壊される際にタイマーを止める
    return () => clearInterval(timerID)
  })

Note

  • foo: は js のラベル構文。Svelte4までは $ というラベルに特殊な意味を持たせていた。
  • Reactive Statement では非同期に参照している値が変わったときに再実行され、予期せぬループが生まれたりしていたので、そこが改善された
  • Reactive Statement と onMount どっちを使うべきかで悩むことがあったので統合されてシンプルになった
  • とはいえ effect も慣れていないと予期せぬタイミングで実行されそうではある
  • effect に async 関数を渡すのは混乱の元なので避けた方がよさそう

(参考)昔書いた記事

ykrodsykrods

Component Props

<script lang="ts">
  // svelte4
  export let width = 0
  export let height = 0

  // svelte5
  let { width = 0, height = 0 } = $props()

  // bind可能なプロパティは $bindable() で明示できる(現状 must ではないらしい)
  let { input = $bindable() } = $props()
</script>
ykrodsykrods

Snippets

再利用可能なマークアップのまとまりをコンポーネント内で作成できる。

{#snippet row(name, price)}
  <tr>
    <td>{ name }</td>
    <td>{ price }</td>
  </tr>
{/snippet}

<div>
  <table>
    <thead>
      <tr>
        <th>name</th>
        <th>price</th>
      </tr>
    </thead>
    <tbody>
      {@render row("Apple", 2)}
      {@render row("Banana", 1)}
      {@render row("Orange", 1.5)}
    </tbody>
  </table>
</div>

Snippet は、コンポーネントにプロパティとして渡すことができる

Container.svelte

<script lang="ts">
  import type { Snippet } from "svelte"
  let { content } : { content: Snippet; } = $props()
</script>
<div>
  {#if content }
    {@render content()}
  {/if}
</div>

App.svlete

<script lang="ts">
  import Container from "./Container.svelte"
</script>
{#snippet content() }
  Foo
{/snippet}
<Container {content}/>

<!-- あるいは以下のようにコンポーネントのコンテンツとして書いてもよい -->
<Container>
  {#snippet content() }
    Fooo
  {/snippet}
</Container>

コンポーネント内に記述したコンテンツは children というプロパティでスニペットとして渡される

  • children は特殊なプロパティ扱いなようなので、独自のプロパティとして同じ名前の使用はできない

Container.svelte

<script lang="ts">
  let { children } = $props();
</script>
<div>
  {#if children }
    {@render children()}
  {/if}
</div>

App.svelte

<script lang="ts">
  import Container from "./Container.svelte"
</script>
<Container>
  Bar
</Container>
  • svlete4 の slot は svelte5 では非推奨となり、Snippet の利用が推奨される。とはいえ、slot は使用可能であり、snippet と slot を組み合わせることもできる
  • web component の slot と紛らわしかった(元々 web component の機能をインスパイアして作られたらしいが)ので良くなった
ykrodsykrods

slot の let ディレクティブについて

svelte4 slot の let ディレクティブのように、コンポーネントの値を Snippet に渡したい場合、{#snippet } のブロックで引数を明示する必要がある

  • つまり、引数を渡したい場合は {#snippet children(arg)} を省略することができないということ

Counter.svelte

<script lang="ts">
  import type { Snippet } from "svelte"

  let { content }: { content: Snippet<[Number]>; } = $props()

  let count = $state(0);

  $effect(() => {
    const timerID = setInterval(() => { count += 1 }, 1000)
    return () => clearInterval(timerID)
  })
</script>
{#if content}
  <!-- snippet に count を渡す -->
  {@render content(count)}
{/if}

App.svelte

<script lang="ts">
  import CountProvider from "./CountProvider.svelte"
</script>
<CountProvider>
  {#snippet content(count)}
    <span>{count}</span>
  {/snippet}
</CountProvider>
  • {#snippet children(count)} でも動作としては問題ないようだが別名にしておいたほうが型がわかりやすいのではという感じがある
ykrodsykrods

Event handlers

  • svelte4 の on:click など on ディレクティブが非推奨になり、 onclick プロパティを利用することになった
  • svelte4 の createEventDispatcher を用いたイベントエミットは非推奨となり、コンポーネントプロパティでコールバックを渡すようになった

Timer.svelte

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

 let count = $state(5);

 $effect(() => {
   const timerID = setInterval(() => { count = Math.max(count - 1, 0) }, 1000)
   return () => clearInterval(timerID)
 })

 $effect(() => {
   if (count === 0) {
     onExpired()
   }
 })
</script>
<div>{ count }</div>

App.svelte

<script lang="ts">
  import Timer from "./Timer.svelte"

  let visible = $state(true)
</script>
{#if visible}
  <Timer onExpired={() => { visible = false }}/>
{/if}
ykrodsykrods

まとめ

以下、非推奨になる

  • let foo (リアクティブな変数宣言)
  • コンポーネントの export let
  • $: による Reactive Declaration / Reactive Statement
  • store
  • onMount
  • slot
  • on:
  • createEventDispatcher

こう書くと結構な変更に思えるが、概念的に難しくなったことはないし、すぐに書き換えなければいけないわけではないし、改善されたポイントがいくらかあっていいねという印象。