↕️

svelte のコンテキストで state を渡す方法の検討

に公開

svelte での親子(先祖子孫)でのコミュニケーションで使われる context ですが、 $state を渡すときの使い方をよく忘れるのでパターンを検証してまとめました。

[NG] (No.1) state で値を直で渡す

初手からNGな例で恐縮ですが、この例では親コンポーネントでの値の更新に対して、子ポーネントが反応してくれません。

Parent.svelte
<script lang="ts">
  import { setContext } from 'svelte';
  import Child from "./Child.svelte";

  let count = $state(3)
  setContext("count", count)

  function add() {
    count++
  }
</script>
<Child/>
<button onclick={add}>{ count }</button>
Child.svelte
<script lang="ts">
  import { getContext } from "svelte"

  let count = getContext("count")
</script>
Child: { count }

これは、 関数に state を渡す にあるように、 setContext 関数の引数で count が与えられる際、変数でなく値が渡されるため、子コンポーネントは親コンポーネントでの値の再代入には反応しない。ということが理由になります。

(No.2) オブジェクトのプロパティとして渡す

以下のようにオブジェクトを state にしてコンテキストに渡すことで、親コンポーネントでのプロパティの更新に子コンポーネントが反応するようになります。

Parent.svelte
<script lang="ts">
  import { setContext } from 'svelte';
  import Child from "./Child.svelte";

  let count = $state({ value: 3 })
  setContext("count", count)

  function add() {
    count.value++
  }
</script>
<Child/>
<button onclick={add}>{ count.value }</button>
Child.svelte
<script lang="ts">
  import { getContext } from "svelte"

  let count = getContext("count")
</script>
Child: { count.value }

(No.3) オブジェクトでの親の再代入

No.1 で再代入に対して子が反応しなくなると書いたので、オブジェクトでも同様か確かめてみます。

Parent.svelte
<script lang="ts">
  import { setContext } from 'svelte';
  import Child from "./Child.svelte";

  let count = $state({ value: 3 })
  setContext("count", count)

  function add() {
    count.value++
  }

  function init() {
    // 再代入
    count = { value: 0 }
  }
</script>
<Child/>
<button onclick={add}>{ count.value }</button>
<button onclick={init}>init</button>
Child.svelte
<script lang="ts">
  import { getContext } from "svelte"

  let count = getContext("count")

  // 子からカウントを変更できるように改良
  function add() {
    count.value++
  }
</script>
Child: { count.value }
<button onclick={add}>add on child</button>

結果

  • 初期状態では子からの変更に親が反応する
  • init で親でオブジェクトを再代入すると、以降は親の変更に子が反応しなくなる
  • 再代入後は、子での値の変更は親は反応しない(子内部では反応が維持される)

のような挙動となり、再代入でリアクティビティが失われること(再代入後は別々のオブジェクトになること)が確認できました。

(No.4) コンテキストで関数を渡す

state_referenced_locally の解説で、解決方法として「変数を関数でラップして渡す」ことが紹介されているので、そのパターンを試してみます。

Parent.svelte
<script lang="ts">
  import { setContext } from 'svelte';
  import Child from "./Child.svelte";

  let count = $state(1)
  // 関数を渡す
  setContext("count", () => count)

  function add() {
    count++
  }

  function init() {
    count = 0
  }
</script>
<Child/>
<button onclick={add}>{ count }</button>
<button onclick={init}>init</button>
Parent.svelte
<script lang="ts">
  import { getContext } from "svelte"

  const count = getContext("count")
</script>
Child: { count() }
<button onclick={add}>add on child</button>

この例では親の変更に対して子が反応してくれますが、子から値を変更する手段がないので、使い方が限定されます(子で更新しないように制限をかける用途で使うこともできます)

[NG] (No.5) 関数呼び出しのタイミングをかえる

No.4 では { count() } のようにテンプレートの中で関数呼び出しをしていますが、書き方として見慣れない感じがあるので、先に関数を呼び出して state を取り出すように変更してみました。が、この書き方だと親と子の間のリアクティビティが失われるようです。

(親のコードは No.4 と同様なため省略

Child.svelte
<script lang="ts">
  import { getContext } from "svelte"

  // 関数呼び出し
  let count = getContext("count")()

  function add() {
    count++;
  }
</script>
Child: { count }
<button onclick={add}>add on child</button>

(この挙動に関しては正直よくわかりません)

(No.6) svelte4 の store 的なオブジェクトを渡す

$state() をそのまま子に公開せず、 getter / modifier を渡すようにしてみます。

types.ts
export interface Counter {
  get value(): number
  add(): void
}
Parent.svelte
<script lang="ts">
  import type { Counter } from "./types";
  import { setContext } from 'svelte';
  import Child from "./Child.svelte";

  let count = $state(0)

  setContext<Counter>("counter", {
    get value() { return count },
    add() { count++ },
  })

  function add() {
    count++
  }

  function init() {
    count = 0
  }
</script>
<Child/>
<button onclick={add}>{ count }</button>
<button onclick={init}>init</button>
Child.svelte
<script lang="ts">
  import type { Counter } from "./types";
  import { getContext } from "svelte"

  const counter = getContext<Counter>("counter")
</script>
Child: { counter.value }
<button onclick={() => counter.add()}>add on child</button>

このような書き方だと、親子双方向でリアクティビティを保ちつつ、 $state のアクセス制御を行うことができます。

まとめ

  • 関係が深いコンポーネント同士では単純にオブジェクトの共有でよいが、細かい制御を行う場合は No.6 のように書くとよさそう。

  • 基本的には context でなくコンポーネントのプロパティで渡した方がわかりやすいので、使い時は要検討

    • グローバルな状態を持つときなど?

Discussion