🏄‍♀️

Svelteの基本をSvelte5で学習したメモ

2024/06/19に公開


新たにSvelteを学ぼうと調べていたところ、次のSvelte5から大きめの変更が入る&そっちのが易しいという情報を見つけました。Svelte5は今RCフェーズだそうで、Svelte5.0のGitHubマイルストーンが87%になっており、長くても数か月程度でリリースされそうな雰囲気です。Svelte5で非推奨になっていく内容を学ぶ気になれなかったので、Svelte4のBasicチュートリアル内容をSvelte5の記述方法で理解していった初心者の見返し用メモです。プレビュー版のSvelte5 Web REPLで動作確認しているので、リリース版は少し異なるかもしれません。

基本事項

Svelteコンポーネント

全体

  • Svelteアプリの主要要素の一つにSvelteコンポーネントがある
    • Svelteコンポーネントを組み合わせて一つのアプリを構築していく
    • 1つのSvelteコンポーネントは1つの.svelteファイルで表現される
    • .svelteファイルは以下3つのエリアからなる
      • JSエリア (<script>タグ内)
      • HTMLエリア
      • CSSエリア (<style>タグ内)
    *.svelte
    <script>
      // JS area
    </script>
    
    <!-- HTML area -->
    
    <style>
      /* CSS area */
    </style>
    
    • それぞれのエリアは必須ではないため、無くてもよい

JSエリア

  • 以下のように宣言することでTypeScriptを使用可能
<script lang="ts">
</script>
  • Svelte5から色々な処理を簡潔に記述するための仕組みが導入された
    • この仕組みはRuneと呼ばれている
    • $state等、$から始まる関数がRuneと呼ばれている
    • Runeは以下ファイルで使用可能
      • *.svelte
      • *.svelte.js
      • *.svelte.ts

HTMLエリア

  • {}で囲むことでHTMLエリアでJavaScriptを使用可能
    • {var}とすることで<script>タグ内で宣言された変数値をHTMLエリアに埋め込める
      • {var.toUpperCase()}のようにメソッドも使用可能
    • {@html var}とすることでvarの内容を直接HTMLとして解釈
      • @は単発のSvelte固有機能呼び出しを意味しているようだ
      • XSSに注意
    • 埋め込んだ変数に代入が発生した場合にDOMが更新される
      • 代入が発生しないarray.push()等で変更した場合は更新されない
        • 公式チュートリアルでpush()後にary=ary;する説明があるが動作しない場合がある
        Sample code
        <script>
          let nums1 = [0,1,2];
          let nums2 = [0,1,2];
          function handle1() {
            nums1 = [...nums1, 0];
          }
          function handle2() {
            nums2.push(0);
            nums2 = nums2;
          }
        </script>
        
        <p><button onclick={handle1}>nums1</button>{nums1}</p>
        <p><button onclick={handle2}>nums2</button>{nums2}</p>
        {nums2.join(",")}
        
        • 以下のような動作となる
          • nums1ボタンを押すと{nums1}が更新される
          • nums2ボタンを押しても{nums2}は更新されない
            • {nums2.join(",")}は更新される
        • 以上よりポインタが変更されていない場合再描画省略されているように思われる
        • メソッドが使用されている等変数名と一致しない場合は省略されない模様
  • 属性名と変数名が一致している場合、以下のような省略記法が使用可能
    • <img src={src}> -> <img {src}>
  • Svelte5からHTML要素群の塊を関数のように扱える仕組みが導入された
    • この仕組みはSnippetと呼ばれている

CSSエリア

  • CSSエリアの記述は同ファイル内でのみ有効
    • グローバルCSSのようなものはここを参照
    • CSS継承のようなものはここここ参照でできそう

HTMLエリアでの制御構造

直接的にHTML要素を条件による切り替えや繰り返し配置できるようになる。

条件分岐

{#if foo > 10}      // "#" means begining of control structure
  <p>1</p>          // rendered elements by the condition
{:else if foo > 5}  // ":" means continuous of control structure
  <p>2</p>
{:else}
  <p>3</p>
{/if}               // "/" means ending of control structure

繰り返し処理

// items is array or array-like object which has length property
{#each items as item, index}  // iterate each element of items, index is optional
  <p>{index} : {item}</p>     // rendered repeatly for the number of items
{/each}

同期処理

{#await promise}     // handle only the most recent promise
  <p>waiting...</p>  // initial elements
{:then result}       // if resolved
  <p>result is {result}</p>
{:catch error}       // if never reject, can omit catch
  <p>error occured {error.message}</p>
{/await}

最初に何も表示したくない場合は以下のようにも書ける。

{#await promise then result}
  <p>result is {result}</p>
{/await}

イベントの取り扱い

基本

イベントハンドラはHTMLエリアのタグに属性として記述する。

<script>
  function handleClick() {
    console.log("clicked");
  }
</script>

<button onclick={handleClick}>BUTTON</button>
  • リッスンしたいDOMイベントを"onイベント名"として記述
    • 関数名が一致していれば省略形<button {onclick}>も使用可能
    • HTML要素のイベントハンドラー属性ではないイベントも使用可能
      • 例: <button onpointermove={handleMove}>

旧修飾子

Svelte4以前に修飾子として実装されていた機能は非推奨となった。

  • 標準の動作を防止したい場合や一度だけ実行したい場合のサンプル
Sample code
  • 全て関数内で表現する
<script>
  function once(fn) {
    return function (e) {
      if (fn) fn.call(this, e);
      fn = null;
    };
  }
  function handleClick(e) {
    e.preventDefault();
    console.log("clicked");
  }
</script>

<button onclick={once(handleClick)}>BUTTON</button>

値の変更伝播

JS変数間

基本的な記述方法

他の変数へ変更を伝播させるリアクティブな変数とそれに追従する処理は以下のように記述する。

<script>
  let var1 = $state(0);           // upstream (reactive statement)
  let var2 = $derived(var1 * 2);  // downstream by simple expr
  let var3 = $derived.by(() => {  // downstream by complex expr
    if (var1 % 2 == 0) { return var1; } else { return var3; }
  });
  $effect(() => {  // changing $state or $derived vars of this func will trigger the side effects
    console.log(var1);
    console.log(var2);
  });
</script>

追加的な記述方法

<script>
  let obj1 = $state({ foo: 1, bar: 2});         // trigger reactivity by changing obj1.foo, obj1.bar or obj1
  let obj2 = $state.frozen({ foo: 1, bar: 2});  // trigger reactivity by changing obj2 only
  let baz = {};
  obj1.baz = baz;

  $effect.pre(() => {
    console.log(isSideEffect());  // this will true
  });
  function isSideEffect() {
    $effect.tracking()            // "true" if running on $effect context
  }
  function compareBaz() {
    console.log(obj1.baz === baz);          // "false" obj1.baz is wrapped by proxy
    console.log($state.is(obj1.baz, baz));  // "true"
  }
</script>
  • 他に以下のようなRuneもある
    • $state.snapshot
    • $effect.root

HTML要素からJS変数

基本

bind:ディレクティブが使用できる。

<script>
  let foo = "";  // foo is changed if change value of input elem
</script>

<label>
  <input type="text" bind:value={foo} /> {foo}
</label>

色々なHTML要素への適用サンプル

Sample code
<script>
  let text = "";
  let num = 0;
  let check = false;
  let select = "";
  let radiogr = "";
  let checkgr = "";
  let tarea = "";
</script>

<ul>
  <li>
    <label><input type="text" bind:value={text} /> {text}</label>
  </li>
  <li>
    <label><input type="number" bind:value={num} /> {num}</label>
  </li>
  <li>
    <label><input type="checkbox" bind:checked={check} /> {check}</label>
  </li>
  <li>
    <label>
      <select bind:value={select} > <!-- also can specify "multiple" attr -->
        <option value="select1">1</option>
        <option value="select2">2</option>
      </select> {select}
    </label>
  </li>
  <li>
    <label>
      <input type="radio" value="radio1" bind:group={radiogr} />
      <input type="radio" value="radio2" bind:group={radiogr} />
      {radiogr}
    </label>
  </li>
  <li>
    <label>
      <input type="checkbox" value="check1" bind:group={checkgr} />
      <input type="checkbox" value="check2" bind:group={checkgr} />
      {checkgr}
    </label>
  </li>
  <li>
    <label><textarea bind:value={tarea}></textarea> {tarea}</label>
  </li>
</ul>

JS変数からHTML要素

上記で言及済み

コンポーネントの引数定義

基本的な記述方法

通常読み取り専用として定義される。

Parent.svelte
<script>
  // other components can be used by using "import"
  import Comp from "./Child.svelte";
</script>
<Comp arg1="txt" arg2={1} arg3="foo" />
Child.svelte
<script>
  const {
    arg1,        // default is undefined
    arg2 = 3,    // with default value
    ...args      // assign rests of props value
  } = $props();  // declaration of component arguments
</script>
  • もう少し複雑な例
Sample code
App.svelte
<script>
  import Comp from "./Comp.svelte";
</script>

<Comp
  arg1="foo"
  arg2={3}
  arg3={false}
  arg4
  arg5={[1,2,3]}
  arg6={{foo:4, bar:"bar"}}
/>
Comp.svelte
<script>
  const { arg1, arg2 = 3, ...args } = $props();
  let iter = [
    arg1,
    arg2,
    args.arg3,
    args.arg4,
    args.arg5[1],
    args.arg6.bar
  ];
</script>

<ul>
  {#each iter as x}
    <li>{x}</li>
  {/each}
</ul>
  • 結果
    img_prop_normal_result

複数の引数を一括で渡す

引数名(props名)とプロパティ名が対応しているオブジェクトを用意すれば、オブジェクトを展開渡しできる。

Sample code
App.svelte
<script>
  import Comp from "./Comp.svelte";
  const args = {
    arg1: "Called",
    arg2: "spread",
    arg3: "props",
  };
</script>

<Comp {...args} />
Comp.svelte
<script>
  const { arg1, arg2, arg3 } = $props();
</script>

<p>{[arg1,arg2,arg3].join(" ")}</p>
  • 結果
    • Called spread props

可変変数としての記述方法

コンポーネント内で変数値が変化しても親には伝播しない。

<script>
  // if $props assume 0, the behavior like `let arg1 = 0;`
  let { arg1 = $bindable() } = $props();
</script>

関数を引数で渡す

親の関数を引数として受け渡していくことで、子,孫へと親の処理を伝送可能。

Sample code
App.svelte
<script>
  import Parent from "./Parent.svelte";
  function setLog() {
    console.log("running app func");
  }
</script>

<Parent appFunc={setLog} />
Parent.svelte
<script>
  import Child from "./Child.svelte";
  const { appFunc } = $props();
</script>

<Child {appFunc} />
Child.svelte
<script>
  const { appFunc } = $props();
</script>

<button onclick={appFunc}>Run app func</button>

値の変更処理順

厳密な検証結果ではないため参考。HTMLエリアで使用されている変数に代入が発生した際は都度DOM更新される模様。(画面描画は不明)

初回表示時

  1. $effect.pre
  2. $derive
  3. $derive.by
  4. DOM構築
  5. $effect

イベント発火時

  1. イベント発火
  2. 通常のスクリプト処理
  3. $effect.pre
  4. $derive
  5. $derive.by
  6. $effect
  7. await tick()後の処理

確認コード

fireEvent内に変数読取を含めると$effect.pre$derive.byの順番が入れ替わる。

Sample code
App.svelte
<script>
  import Comp from "./Comp.svelte";
</script>

<Comp arg1={0} />
Comp.svelte
<script>
  import { tick } from "svelte";
	const { arg1 } = $props();

  let dummy;
  let var1 = $state(1);
  let var2 = $derived(var1 + 1);
  let var3 = $derived.by(() => {
    console.log(`4:[$derived.by1]_${getStatus()}`);  // should not do this in $derived.by
    return var1 + 2;
  });
  let var4 = $derived.by(() => {
    console.log(`5:[$derived.by2]_${getStatus()}`);  // should not do this in $derived.by
    return var1 + 3;
  });

  $effect.pre(() => {
    console.log(`3:[$effect.pre]__${getStatus()}`);
    dummy = var1  // should not do this in $effect.pre
    toggleMainStyle();
  });
  $effect(() => {
    console.log(`6:[$effect]______${getStatus()}`);
    dummy = var1  // should not do this in $effect
    toggleMainStyle();
  });

  function fireEvent(fn) {
    return function (e) {
      console.log(`1:[fireEvent]____${getStatus()}`);
      fn.call(this, e);
    };
  }
  async function showSequence() {
    var1 = var1 + 1;
    console.log(`2:[beforeTick]___${getStatus()}`);
    await tick();
    console.log(`7:[afterTick]____${getStatus()}`);
  }

  function getStatus() {
    let ary = [];
    for (const id of ["props", "state", "derive", "deriveby1", "deriveby2"]) {
      ary.push(getPText(id));
    }
    ary.push(getMainStyle());
    return ary.join(";");
  }
  function getPText(id) {
    const p = document.getElementById(id);
    return p === null ? "null" : p.textContent;
  }
  function getMainStyle() {
    const p = document.getElementById("props");
    return p === null ? "null" : p.style.color;
  }
  function toggleMainStyle() {
    const p = document.getElementById("props");
    if (p === null) { return; }

    if (p.style.color === "red") {
      p.style.color = "blue";
    } else {
      p.style.color = "red";
    }
  }
</script>

<button onclick={fireEvent(showSequence)}>
  show sequence
</button>
<p id="props">{arg1}</p>
<p id="state">{var1}</p>
<p id="derive">{var2}</p>
<p id="deriveby1">{var3}</p>
<p id="deriveby2">{var4}</p>

関数のようなHTML要素群

基本

HTMLエリアでスニペットと呼ばれるHTML要素の塊を定義できる。スニペットは関数のように扱うことができる。

  • {#snippet snip(arg)}...{/snippet}で定義
  • {@render snip(arg)}で呼出
<script>
  let data = $state([
    { id: 1, name: "foo", score: 2 },
    { id: 2, name: "bar", score: 1 },
    { id: 3, name: "baz", score: 3 },
  ]);
  function increment(id) {
    data.find(x => x.id === id).score += 1;
  }
</script>

{#snippet comp(arg)}
  <div>
    <label for={arg.id}>{arg.name}</label>
    <button name={arg.id} onclick={() => increment(arg.id)}>{arg.score}</button>
  </div>
{/snippet}

{#each data as x}
  {@render comp(x)}
{/each}

引数として指定

関数同様スニペットを子要素に渡すこともできる

通常の方法

App.svelte
<script>
  import Comp from "./Comp.svelte";
</script>

{#snippet snip(arg)}
  <section>
    <h3>{arg.id}</h3>
    <p>{arg.name}</p>
  </section>
{/snippet}

<Comp {snip} />
Comp.svelte
<script>
  const { snip } = $props();
  let data = { id: 1, name: "foo" };
</script>

{@render snip(data)}

子要素の中でスニペットを定義する方法

App.svelte
<script>
  import Comp from "./Comp.svelte";
</script>

<Comp>
  {#snippet snip(arg)}
    <section>
      <h3>{arg.id}</h3>
      <p>{arg.name}</p>
    </section>
  {/snippet}

  <p>additional elements</p>
</Comp>
Comp.svelte
<script>
  const {
    snip,
    children,  // must be named "children"
  } = $props();
  let data = { id: 1, name: "foo" };
</script>

{@render snip(data)}
{@render children()} <!-- this is "<p>additional elements</p>" -->

参考文献

Discussion