🗂

Svelte5のドキュメントに記載のない役立つか不明な小ネタ

2024/09/02に公開


Svelte5の地味な検証記事が役に立つ場合があるようだったので、その他の自分で色々やってみた時に発見した?小ネタも記事にしました。こんなの使わんやろ的な内容も多いので役立つかは不明です。一部ドキュメントに載ってたりドキュメントに載せるような基礎的内容じゃなかったり別にSvelte5からじゃないものも含まれてますがご了承ください。また、正しい内容だと自分では思っているのですが、私の認識が誤っている場合もありますのでご了承ください。
[svelte@5.0.0-next.242 が最新リリース状態でのREPLで確認]

(2024/09/05 追記)
任意のタイミングでコンポーネント内の関数を起動するに追記しました。また、 "$effect.trackinguntrack内でfalseを返す"を追記しました。またそのうち何か見つければこっそり追記するかもしれません。

残余引数でchildrenを受け取る

コンポーネントタグ内のコンテンツで{#snippet}{/snippet}で囲まれていないものは全て以下のようにchildrenとして受け取ることができます。

let { children } = $props();

これは特に明示して受け取らずとも、残余引数で受け取り呼び出すこともできます。

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

{@render rest["children"]()}

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo2QQWvDMAyF_4oQg7YQkrublpVddhzsGOdQGqUztWVjOxvF-L8PN2u3lR12s_TeZz0p4ag0BRRdQt4bQoE757DCeHalCO-kI2GFwU7-UDptOHjl4layjMo46yM8WeNg9NaAxLopVT1zEteS2-ab4LaoF7Z122fS2s7g4U3pwRO3TVHb5suGFRo7qFHRgCL6iXJ1y1ks_wgKxg6TJtB7Pm4kxiDxZ_aCQ4JXVs5RhDzHWczfLdbFebG8eOsCbCCVjozdic4CQvSKj72AiU9sP7gqYv698zXG_XxNERLUde0pRMjiNuHBlcdydX-69OiJB_JQgE7i9WQS--Uq_3WrPn8CV4gC6t8BAAA=

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

<Comp>
  <p>Hello from children</p>
</Comp>
Comp.svelte
<script module lang="ts">
  import type { Snippet } from "svelte";
  type Props = {
    [key: string]: unknown,
  };
</script>
<script lang="ts">
  let { ...rest }: Props = $props();
</script>

{@render rest["children"]()}

残余引数で任意のスニペットを受け取る

コンポーネントは通常以下のようにスニペットを受け取ることができます。

let { foo }: { foo: Snippet } = $props();

コンポーネント作成時点でスニペットの名前が確定していない又は個数が確定していない場合、以下のように残余引数で受け取り実行時に呼び出すことができます。(活用事例)

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

{#if typeof rest["foo"] === "function"}
  {@render rest["foo"]()}
{/if}

ただし、以下の例でもただの関数なのかスニペットなのかの厳密な判別をしていないので注意が必要です。(判別方法不明)
https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo2SwWrkMAyGX0WohUkgJPdMMuzSS2EPW-ixHpZMorTuOLaxlZYh5N2L40y3U2Zhb7b8f9IvWRP2UpHH8mlC3QyEJf60FjPkkw0X_0aKCTP0ZnRtiFS-ddLyTmjBcrDGMdyZwULvzAAC8yLc8sgJ3ApdFX8JXYXXhZ1uvJbWEkNvTJLOISa4srt7UsrEdL0xVbHKi1U-X8KHxv0DPjTuOnwpa1-k6hzpqK2K1SBmOJhO9pI6LNmNNGefEwqS_xgRDKYbFYFq9HMtkL3Ar1MLOEzwuDYyRzubmG6zDcpF8uCM9VDDFJt8OtKpBM9O6ud9CaM-avOus_A4X077bON7fUUME-R57sgzzOVnhVsbDkm61G6N9gzr3H7RCWr4fXillnPS7CT5JOBpNJX3UjG5JAnuMnhr1Ej7FOrd0qXpYwTqugaB_ahblkYLPNNDY8_on4gd6ZRuBX_bnumGmvblq6nGB2nciR-OdEcOgq-QbL8sxlQEZr72o_v5A-0-2R3_AgAA

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

<Comp>
  {#snippet foo()}
    <p>Hello from foo</p>
  {/snippet}
  {#snippet bar()}
    <p>Hello from bar</p>
  {/snippet}
  <p>Hello from children</p>
</Comp>
Comp.svelte
<script module lang="ts">
  import type { Snippet } from 'svelte';
  type Props = {
    [key: string]: unknown,
  };
</script>
<script lang="ts">
  let { ...rest }: Props = $props();
  const snippetKey = Object.entries(rest)
    .filter(([key, value]) => typeof value === "function")
    .map(([key, _]) => key);
</script>

{#each snippetKey as key}
  {@render rest[key]()}
{/each}

任意のタイミングでコンポーネント内の関数を起動する

仕様なのか不具合なのか理解していませんが、コンポーネントはモジュールレベルのscriptタグ内でしかexportできません。

App.svelte
<script>
  import { inModule, inScript } from "./Comp.svelte"
  inModule();
  //inScript();  // if uncomment, compile error occurs.
</script>
Comp.svelte
<script module>
  export function inModule() { console.log("in module"); }
</script>
<script>
  export function inScript() { console.log("in script"); }
</script>

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo2Qy2rEMAxFf0VolYCJ98lQKF131eW4i-IoRWBbxo_SEvLvJZ5HW2YWXepxr-7Rigs7yjgeVwxvnnDExxhRYfmKe5E_yBVChVlqsnvnkG3iWB5MMIV9lFRgBQ7PMldHCji8tDlssCTxYHDQT-LjcHIy2HTn9a6f9lLri6rrJwCtgReowYr3FIoCKz6yI6CUJIFYW1MeTDjoaxRU6GXmhWnGsaRKm7ry7Nf_AQS-RWpc9Nm4lhpsYQnwkxdWsBKyOBqcvHcGOZyFBvsJtj-pfr_q1vJCfM_yJLxjeQv6un0DkAowxsQBAAA=

そのため、コンポーネントの各インスタンス固有の状態に関する処理を外部から任意のタイミングで起動するには$effectを使うしかないです。(たぶん)
関数をbindbindableで共有するだけで普通にできました。なぜ最初にこの発想にならなかったのか。
コンポーネントの値検証はコンポーネントに任せ、任意のタイミングで外部から検証結果を取り出すには以下サンプルのようにすればできます。

  • 関数を共有する方法

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACqWS246bMBCGX2UyygWoKfS2LFCttg-Q-_VWcmCSWDG2ZQ9pV4h3rwwhu9nDRdUbxMxv_57DN-BeaQpYPA5oZEdY4L1zuEF-djEIZ9JMuMFge9_ETBkarxxDZ9teE2hpDpVADgJrYQSrzlnP8GA7B3tvOxCY5THKZiuBd8KU-WxSC7PYvfXRxMAUuIAkhaqGnbWapIEK1oElU-bl7yS9W452FII80FVOBLYqOC2fr9KRPAmcr-x707CyBqxptGpOSQpDzAt-MYqvJyn8AIFMfxhUgLPUql2tVgKheJVWZhKyLJu6EzzetChMuVek20BTxKWmA5m2fjgq3UJjO2cNGS7zS346Mw1wp0xbxDogj6PKX9t8YrqVngx_5rrrma2BuNxK4BwJhOEyhrFujtScynxW5juuHi5DGcvcva0DN9jZVu0VtViw72ncXEmKPfwHSvEebL11AaplPR8gMU88fl_wG6A37GVzgnGh8N_xGyYEIlJxD3KnKUlhLK4lrV38eQXhWer-BsEFt4uPCvehUeqnOigOk3DD4o2cpMWV-Uvvnrj3BvJfj9--fn_6ss6zCdHp1fRj7pRxPS_rjrgKnKGaS3VaNnS0uiVfCdxqkoGADJMH03c78gIn8t4v-Wn8C3chaVE6BAAA

code
App.svelte
<script module lang="ts">
	import Comp from "./Comp.svelte";
</script>
<script lang="ts">
	let test: () => boolean = $state.raw();
	let message = $state("display message here");
	function onclick() {
		message = test() ? "text is valid!!!" : "text is invalid...";
	}
</script>

<fieldset>
	<legend>Child component</legend>
	<Comp bind:test />
</fieldset>

<fieldset>
	<legend>Parent component</legend>
	<button type="button" {onclick}>check</button>
	<p>{message}</p>
</fieldset>
Comp.svelte
<script module lang="ts">
	type Props = {
		test: () => boolean;
	};

	import { untrack } from "svelte";
</script>
<script lang="ts">
	let { test = $bindable() }: Props = $props();
	let value = $state("");
	test = isAsciiDigits;
	
	function isAsciiDigits(): boolean {
		return /^[0-9]+$/.test(value);
	}
</script>

<input type="text" bind:value placeholder="Please enter number" />
  • $effectを使う方法 (非推奨)

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACqVTTW-cMBD9KxNrD6xKodcS2CpKcohatZEa5bJOJRaGjbXGRvawSoT475VtYJO0OVS9rHYeM28-_N7AGiHRsmw7MFW2yDJ20XUsZvTcucAeURKymFndm8ohua2M6AhaXfcSQZZqX3BGlrMNV5xE22lDcKnbLobBscAdWorh7vrn3QiN0S1wlqQuIQnsnJ1zlaeBd8PV3OEttUQCQktQwMpSSRg5zuT-4tvN1fp8zmjR2nKPLqlGI45YR77oC3BG-EQgLBxLKeqzszPOIHsBC-U_JEnCWSBselWR0Aq0qqSoDtEaBodzmgbxE9x8v__x9doXjK824SpvBMraoo8ol7hHVW8uH4WsodJtpxUqytMJ9znuMrATqs58j9RdJH1J8w7pbWlQ0Xusu55IK3APUnAWIs5gmBYbN9UjVoc8DV9CTbcZpmuOedq9nYPFrNW1aATWLCPT4xgvGnI7_IeI8MmLqNLKkj8xFOHuAOHWGaheyjhAXgAZuBHiJSlgTSltAEcobSD077QIEwqIXKAb32i9PeCzbl5jD6eSW6M7O08TVJB5nvD67vdkggF6RaasDrAI_98VPyyad5oodxKjNYzZMsmqc3-ikwGOpezx5BHOZjGvsGmwoqQzGEVrKDbzFsLt65sUrxS9iJ3TtMhUN00k7IWthLgSe0E2Wocu3gOcxil6aaI3-RnstJZYqrmNQeqNgvTX9tPHzw8fVmniGkV-ofXf7SVU19OsaudjzoJ3whU6WVb4qGWNpuDsVmJpEVARGlB9u0PDmTfYn1p-GH8DOeutLhsFAAA=

code
App.svelte
<script module lang="ts">
  import Comp, {type Test, TEST} from "./Comp.svelte";
</script>
<script lang="ts">
  let test = $state(TEST.VALID);
  let message = $derived(test ? "text is valid!!!" : "text is invalid...");
  function onclick() {
    test = TEST.INVOKE;
  }
</script>

<fieldset>
  <legend>Child component</legend>
  <Comp bind:test />
</fieldset>

<fieldset>
  <legend>Parent component</legend>
  <button type="button" {onclick}>check</button>
  <p>{message}</p>
</fieldset>
Comp.svelte
<script module lang="ts">
  export const TEST = {
    INVOKE: null,
    VALID: true,
    INVALID: false,
  } as const;
  type Test = (typeof TEST)[keyof (typeof TEST)];
  type Props = {
    test: Test;
  };

  import { untrack } from "svelte";
</script>
<script lang="ts">
  let { test = $bindable() }: Props = $props();
  let value = $state("");
  $effect.pre(() => {
    if (test === TEST.INVOKE) {
      untrack(() => test = isAsciiDigits());
    }
  });
  
  function isAsciiDigits(): boolean {
    return /^[0-9]+$/.test(value);
  }
</script>

<input type="text" bind:value placeholder="Please enter number" />

$effectが2回起動したりして微妙にスマートじゃないので、この方法以外で簡単な方法があれば知りたいです。

useディレクティブはundefinedを渡すとエラーになる

以下のようなタグの属性値にundefinedを渡すと、その属性がないタグが生成されます。(以下例ではspanClassundefinedだとclass属性がなくなる)

<span class={spanClass}></span>

bindディレクティブのある属性でもundefinedでエラーは発生しません。

<script>
  let value = $state();
</script>

<input type="text" bind:value />
<p>{typeof value}</p>

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACkWMQQqDMBBFrxI-XbQguE9joOdourA6QiCOg5lIi3j3Elx0-T_vvR1TTJRhnzu4nwkWDxE00K_UkTdKSmiQl7IO9XF5WKOoDxw0kZqtT4VMZy5Ze6Xr7R7YtX-GXWQpamqvC1D6aIB5Rx7tabY-sBO_V2CZztzhWvGB0WBexjhFGmF1LXS8jh_TVnawsQAAAA==

しかし、useディレクティブはundefinedを渡すとエラーが発生します。コンパイルエラーではなく実行時エラーのようです。

<script>
  let action;
</script>

<input type="text" use:action />

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACkWMQQrDIBBFryJ_LWRvbaDnqF0EM4EBM4qOpSXk7kG66PK_9_gHNk7U4J4HZNkJDo9SYKHfMkZ7U1KCRcu9xkF8i5WLzkGCJlKzROUstyB--hvxLKWrGS_3AKWPBpjeyP1yM81BYLHnlTemFU5rp_N1XpO2Am-QAAAA

{#if}で場合分けすることでエラーを回避することができます。

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo2R3UrEMBCFX2UYvVAo2_vYriz7At47XpR2IoH8kaSihL67JG1XXRS8ywznnPlmklEqzRHFc0Y7GEaBJ--xwfThSxHfWCfGBqObw1g6XRyD8ulIlpIy3oUEZ2c8yOAMEB7aUh1WH-ED2a79ctiuatsjWWzQuElJxROKFGZemgtCEf2DAYybZs2gB_vaE6ZI-B2r2CHDaUzKWVh2wjWuHWq7ElKq0qfgfIQeculQWgWPYguouuXnPjvHNYDmBBmGbbC4JN_68ri7v75KvlGy4jq5u_q-B8LZTiyV5YlwKcGdsn5eNyvz-D0R1mtmwTry35o5stiSq7xVcvntD16WT4fYX9MSAgAA

少し前、私の手元の開発環境では{#if}で場合分けしてもエラーが発生するので仕方なく空アクションを作成していたのですが、不要になったようです。

コンポーネントでstring | Snippetを受け取る

コンポーネントでスニペットを受け取るようにすると色々なケースに対応できるコンポーネントが簡単に作成できます。しかし、そのコンポーネントを軽く使用する場合でも{#snippet}をいちいち定義しなければならず少々ボイラープレート感があります。childrenで受け取ることができれば良いですが、他の用途でchildrenが使われている場合は使えません。そこで、stringでも使用できるようにするとコンポーネントを使うときに使いやすい場合があります。

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo1Sy27cMAz8FYLtIQEcO7kVir1o0R8okNyqHlybXguQJUHi5gHV_x5Isrfd5NKTqOFwNEMo4qQ0BRQ_I5p-IRT4zTmskF9duoQn0kxYYbAnPySkDYNXjg_SSFaLs57hu10cTN4uILFu0q0ucxLvpWmbvxOmzVxWrKmT-JhOUAFm8iQxaz7OKiRo6ZWBwRomw6FOMmn0IA1WuNhRTYpGFOxPtFZn74nyH-ZhseNJE-jeHDuJHLa3tzxpHCI8GOUcMax7tn9CSc6kH966AB3EhEjOuQQE9soc4c-uUJXuMCs9ejLiAl8vV7Q7fG9NE0Msi6tgV4JVnC18dqm4uv6w8VE9waD7EDqJk--XfdHxk5pyVDsVXei6LqXM7iWuxXUbXG_OApko8RBzsbZN6hY5QTrQNhS_ejIj-SJ8dZ3h2KgpF8nS4R1xj7Rx26Zw9jMthl815bE6x9iX_tv6kfxNbgsIVqvx_qLzrEaeBdy5l4xn_bok3iQma_jmmdRxZgFfbm93XtuURz9-uV_rG0NT5LQ6AwAA

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

<Comp title="Title is here">
  This is main contents.
</Comp>
Comp.svelte
<script module lang="ts">
  import type { Snippet } from "svelte";
  type Props = {
    title: string | Snippet,
    children: Snippet,
  };
</script>
<script lang="ts">
  let { title, children }: Props = $props();
</script>

<div class="frame">
  {#if typeof title === "string"}
    <span class="title">{title}</span>
  {:else}
    {@render title()}
  {/if}
  <div>
    {@render children()}
  </div>
</div>

<style>
  .frame {
    border-style: solid;
    border-width: 1px;
  }
  .title {
    font-weight: 800;
  }
</style>

特定タグの属性やイベントを専用型を使って透過的にする

コンポーネント内の特定のタグの属性やイベントを$propsで受け取り透過的に扱いたい場合があります。そのような場合も簡単に型定義し、スプレッド構文で適用できるようになっています。イベント定義がonディレクティブではなくなったので、より簡単に適用できるようになったのだと思います。

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACp1STYvbMBD9K8Owh4Qamb16Y4elLHShhR56q3pQnEnWYMvCGoc2Rv-9jGRvPvZSepLn643nvTfhoWnJY_FzQms6wgKfncMM-Y-TwJ-oZcIMfT8OtWQ2vh4ax9D1-7ElaI09lhrZa6y01dx0rh8YPvedg8PQd6BR5RKpBKXxSdtNnkAqbRe4e5yWGBrrRoYSHjwbptX6SQp1bz2DYR6a3cjkoYSp2Regkek3P2rMoG6N95Kp-85JQi6TWN7YcTLtGDPGGI3hCphOZDmCSk5zb-s3Y49UwIpOBbxIeQ1ltdQ100mxGY7EKqJCCfeZT0KCUufzWWOaCvKEGyK03UTSdo3dF9RSR5bLKVIQYLrcG2BK_xggF_5clZq2aVnY5K7SFjPs-n1zaGiPBQ8jhexdYFnz3woDzArLOEzw5ce3r88XMcIieoLN50N8lF1zHPo-9O6K4ctp2-IObiPhq1z3knCqLM0kBrYFpKJESyU1zlDXsxlAnkd6za6lKMC_W3G6clw2mySDeZlYdMFdrSEU7yc-OPmIzr2VOllb6CiTcTXCpJS60VkptUgdXcFvjS-neWmS_6POv8Jfnnc-F9QDAAA=

code
App.svelte
<script module lang="ts">
  import Comp from "./Comp.svelte";
</script>
<script lang="ts">
  let input = $state();
  const attributes = {id: "text1", class: "comp", name: "name1", value: "aaa"};
  const events = {
    onchange: (ev: Event) => {
      ev.target.value = ev.target.value + "...zzz"
    }
  }
</script>

<Comp bind:element={input} {attributes} {events} />
<p>{input?.value}</p>
Comp.svelte
<script module lang="ts">
  import type { HTMLAttributes } from "svelte/elements";
  type Props = {
    attributes?: HTMLAttributes<HTMLInputElement>,
    events?: InputEvent,
    element?: HTMLInputElement,  // bindable
  };
</script>
<script lang="ts">
  let { attributes, events, element = $bindable() }: Props = $props();
</script>

<input type="text" {...attributes} {...events} bind:this={element} />

$effect.trackinguntrack内でfalseを返す

$effect.tracking$effectの文脈で実行された時、trueを返します。これはマウント時に$effectが処理される時も変わりません。

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACn2Qz2rDMAzGX0WIHhwW2u2aJoG-w27LDpmrdKaOHGy5Y4S8-7Bd9ueyg7D0ff5-wl5xMpYCNi8r8jgTNnhaFqxRPpc0hBtZIawxuOh1UtqgvVmkH3gQSwLaRRboYBdkFFKP1TE52nEQCO_u49mP-mr4Ah2oCroekuUs7a27qB1NE2nZy_2SqnKcB7k7qmTWJCVqZDmW_jdblaVbOVJNkbUYx2BYe5qJRVV_KPDQwdM_qIHbw89LuX2LIo7BsbZGX7v1m7vln8hqaGDN8JwuiR5rnN3ZTIbO2IiPtL1uX-cbExh2AQAA

しかし、untrackの中で$effect.trackingが実行されるとfalseを返します。どうやら$effect.trackingは厳密に以下ドキュメントの記載通り"tracking context"、私の理解では"検知スコープ"内で実行された時にtrueを返すようです。ドキュメント通りと言えばドキュメント通りですが若干困惑する場合がありそうです。

tells you whether or not the code is running inside a tracking context

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo2QwWrDMAyGX0WIHhxWuu2aJoE-x7xD5srF1JGDLQ-GybuPONlGb7vJv7_vR6igdZ4Stm8FeZwIW7zMMx5Rvub1kT7JC-ERU8jRrEmXTHSzDJq1uGkOUaBAZomjucMCNoYJ9O5pPK-YJwETMgv0cEgyCqmXpv4cyFoyolQD_QBljbRU9LzNe_EjsCKcgqeTDze1d5wq6PimmmaXl5_hP_gGsxab2YgLDI5NpIlYVPOwGjz18Lo5mrvnv3tw95FFAkNg45259-W3Yqn3qmlqodSeam_GgEecwtVZR1dsJWZa3pdvvl-myZwBAAA=

クラスのプライベート$stateフィールドを透過的に扱う

クラスのプライベートフィールドを$stateで宣言した時、そのフィールドが配列であればgetterを{#each}に書けば通常の$state配列のように動的に更新してくれます。配列以外の値の場合、たとえオブジェクトであってもうまく動作しません。

https://svelte.dev/playground/untitled#H4sIAAAAAAAACp2STU_DMAyG_4rlcdi0aQWOXTuJC7fd4LTskHUujUiTqnH5UNT_jtIvNoQ0xCVS7Mev39jxaGRJGOOzYcWaTrjCXGlyGO898mcVciGAq5F8qKq1eyPNIXaUjn6LZ9YwGXYYY-KyWlUMWpqXVCA7gVthBGdaOgdP5Bh8uAue5dZCCjeOJdN8f7uCu8Ni0-deiCG3dr4ADzVxUxvgQrl1qNlAO0GP1xh5Oo3MlNzfHpbLc2JH5ZHqS2hdNa6Y3y8GrjsyaxwDhzekYOi9e8588GwE543JWFkD1mRaZa9DZ8GhZD1aGRR_wjsqL-HRVccnUT_XMEuTHBtmayAsLBXY3wSOSqn_7t9uc2uXyyTqoe3fi3dUttvJxpmAMEmju50mWgV5eC-sphh857wfcJtEWl1AiqkcmXFzZ2AS9aKTuJ-RzAoYNUE6eJO6oWFtQdb3gamVj0JJe0Vs-jb_UMQVMn0wxlw31B7aL4n1CKpRAwAA

参考文献

Discussion