🎰

[Svelte] slot の子コンテンツに値を渡すのに let が使える

2024/04/16に公開

はじめに

svelte の slot で、slot付きコンポーネントから子コンテンツに値を渡す方法について調べてまとめました。

動機

例として、以下のような1秒ごとに増加するカウントを表示するアプリがあるとします。

<!-- App.svelte -->
<script>
  import { onMount } from "svelte";
  import Count from "./Count.svelte"; // count を表示するコンポーネント

  let count = 0;

  onMount(() => {
    const timerId = setInterval(() => { count++ }, 1000);
    return () => clearInterval(timerId);
  });
</script>
<Count value={ count }/>

これくらい単純な場合はこのままでもいいのですが、App.svelte が肥大化した場合を考えると、カウントアップ部分を別のコンポーネントに切り出したくなります。また、そのコンポーネントに Count コンポーネントをベタ書きしてしまうと「リアクティブに値が変わるデータを更新・提供するコンポーネント」と「値を表示するコンポーネント」二つの役割をもってしまいパッとしないので、slot を使いそれぞれのコンポーネントに切り出したい、という気持ちになります。

<!-- 完成イメージ -->
<!-- App.svelte -->
<script>
  import CountProvider from "./CountProvider.svelte"; // count を更新・提供するコンポーネント
  import Count from "./Count.svelte"; // count を表示するコンポーネント
</script>
<CountProvider>
  <Count value={count}/> <!-- 何らかの方法で CountProvider から count の値をわたす -->
</CountProvider>

let ディレクティブ

let ディレクティブを使うと、slot 付きコンポーネントから slot のコンテンツに対してコンポーネントプロパティ(prop)を渡すことができます。まず、 <slot プロパティ名={公開する変数名}> の形で slot に公開するプロパティを定義します。

例として CountProvider は以下の様に書けます。

<script>
  import { onMount } from "svelte";

  let count = 0;

  onMount(() => {
    const timerId = setInterval(() => { count++ }, 1000);
    return () => clearInterval(timerId);
  });
</script>
<slot {count}> <!-- count={count} の短縮表記 -->
</slot>

親コンポーネントにて、 slot 付きコンポーネントに対して let:参照するプロパティ名={変数名} を記述することで、 slot コンテンツ内で変数としてプロパティが参照できるようになります。

App.svelte は以下のように書けます。

<script>
  import CountProvider from "./CountProvider.svelte";
  import Count from "./Count.svelte";
</script>
<CountProvider let:count><!-- let:count={count} の短縮表記 -->
  <Count value={count}/>
</CountProvider>

そのものまさにという感じですね。

(別解1) bind

親コンポーネントで count 変数を定義し、CountProvider に bind する形でも同じようなことはできます。

<!-- App.svelte -->
<script>
  import CountProvider from "./CountProvider.svelte";
  import Count from "./Count.svelte";

  let count = 0;
</script>
<CountProvider bind:count>
  <Count value={count}>
</CountProvider>

この場合、親コンポーネントのどこからでも count の値を参照・更新できてしまうのでコードが複雑化する懸念があります。小さいアプリとか、個人のアプリならこれでも問題ないと思われますが、せっかく let があるので積極的に bind を採用する理由はないかなと思います。

(別解2) context

Context は slot のコンテンツ内でも参照可能なため、コンテキスト経由で値を渡すことができます。

  • ( ドキュメント にあるように、context でリアクティブな値を渡すには store と組み合わせる必要があります。
<!-- CountProvider.svelte -->
<script>
  import { setContext, onMount } from "svelte";
  import { writable } from "svelte/store";

  const count = writable(0);
  setContext("count", count);

  onMount(() => {
    const timerId = setInterval(() => { $count++ }, 1000);
    return () => clearInterval(timerId);
  });
</script>
<slot>
</slot>
<!-- Count.svelte -->
<script>
  import { getContext } from "svelte";

  const count = getContext("count");
</script>
<p>{ $count }</p>
<!-- App.svelte -->
<script>
  import CountProvider from "./CountProvider.svelte";
  import Count from "./Count.svelte";
</script>
<CountProvider>
  <Count/>
</CountProvider>

Count.svelte が単純な表示だけのコンポーネントでは無くなってしまっていますし、App.svelte で値の受け渡しが見えないので見通しが悪い感じもしていますが、 Context API のチュートリアル にあるような例(canvas の ctx に子コンポーネントから描画する) では使えるかもしれません。

終わりに

コンポーネントの分割に使えそうで良さみを感じました。

Discussion