$state: リアクティブな状態変数と、バインディングの違い

Svelte開発において、「状態管理」と「バインディング」は似て非なる概念です。$stateはコンポーネント内部でリアクティブな状態を管理するための仕組みであり、bind:はDOM要素やコンポーネント間でデータを同期させるための仕組みです。

初心者向けの解説

この2つは組み合わせて使うことが多いですが、それぞれ異なる目的と役割を持っています。$stateで定義した変数をbind:でDOM要素に接続することで、ユーザー入力とアプリケーションの状態を連動させることができます。

このページでは、Svelte 5の新しいリアクティビティモデル「Runes」の中核となる`$state`と、従来から存在する`bind:`構文の違いを、実践的な例を交えて詳しく解説します。

$state(Svelte 5の新リアクティブ変数)

概要

  • $stateは、リアクティブな状態(状態の変更を自動で追跡)を定義する方法
  • Svelte 5では、runesという新しい仕組みで、より明示的なリアクティビティ制御が可能
  • $state()で定義された変数は直接アクセス・更新が可能($プレフィックスは不要)

<script>
  let count = $state(0);
</script>

<button onclick={() => count++}>
  Clicked {count} times
</button>

特徴

  • $state()で定義された変数は、プリミティブな値やオブジェクトでもリアクティブに追跡される
  • 変数に直接アクセスし、通常の変数のように読み取り・書き込みができる
  • 書き込み時も自動的に再描画が起きる

bind:(双方向バインディング)

概要

  • DOM要素の属性(例: value, checked, selected)と変数を同期させる。
  • ユーザーの入力に応じて変数が自動的に更新される。

<script>
  let name = $state('');
</script>

<input bind:value={name} />
<p>Hello {name}!</p>

特徴

  • DOMとの 双方向バインディング を行う
  • ユーザー入力と内部状態を同期させたい場合に便利
  • コンポーネント間のbind:propによるバインディングも可能

違いのまとめ

項目$state(Svelte 5)bind: (従来機能)
主な用途状態管理(内部)DOMや子コンポーネントとの同期
宣言方法$state()関数bind:xxx={var}
リアクティブ性直接アクセス・更新DOMイベントで自動更新
状態更新時自動再描画自動再描画
Svelteバージョン5以降(Runes)3以降(継続使用可)

実践例:両者を組み合わせた使用

フォーム入力の例

<script lang="ts">
  // $stateで状態を定義
  let username = $state('');
  let email = $state('');
  let isValid = $derived(username.length > 0 && email.includes('@'));
</script>

<!-- bind:でDOM要素と状態を同期 -->
<input type="text" bind:value={username} placeholder="ユーザー名" />
<input type="email" bind:value={email} placeholder="メールアドレス" />

<button disabled={!isValid}>
  送信
</button>

<p>入力状況: {isValid ? '有効' : '無効'}</p>

コンポーネント間のデータ共有

Parent.svelte

<script lang="ts">
  import Child from './Child.svelte';

  let parentValue = $state('親の値');
</script>

<Child bind:value={parentValue} />
<p>親コンポーネント: {parentValue}</p>

Child.svelte

<script lang="ts">
  let { value = $bindable() } = $props();
</script>

<input bind:value={value} />
React/Vue経験者への注意
  • ReactのuseStateとは異なり、$stateの値は直接変更可能です(セッター関数は不要)
  • Vueのrefに似ていますが、.valueプロパティは不要です
  • bind:は Vue のv-modelに相当しますが、より柔軟で明示的です

Function bindings:「変数」と「バインディング」の中間

ここまで見てきたように、$state変数としてリアクティブに振る舞い、bind:変数や子コンポーネントの prop と要素を結びつける構文です。両者は性格が違うため、「片方向に派生させた値を、双方向にバインドしたい」というケースでは表現が難しくなります。

Svelte 5.9.0 で導入された Function bindings は、まさにこの中間を埋める構文で、bind:value変数の代わりに getter/setter のペアを渡せます。

<!-- bind:value に 2-tuple `() => getter, (v) => setter(v)` を渡す -->
<input
  bind:value={
    () => value,
    (v) => value = v.toLowerCase()
  }
/>
Svelte 5.9.0 以降の機能

Function bindings は Svelte 5.9.0 以降で利用可能です。

「変数を bind」と「関数を bind」の違い

これまでの bind: と、Function bindings の違いは、「バインドする対象が lvalue(代入可能な変数)か、それとも getter / setter のペアか」 という点に集約されます。

形式書き方バインドの対象値の変換・派生
通常の bind:(変数バインディング)bind:value={count}リアクティブな変数($state など)不可(同じ値が両方向に流れる)
Function bindings(関数バインディング)bind:value={() => g, (v) => s(v)}getter / setter のペア可能(双方向で変換・派生・正規化できる)

つまり、「変数 vs バインディング」という整理に 「関数による派生バインディング」 という第 3 の選択肢が加わったと考えるとわかりやすいです。

双方向に派生させたいときのパターン

「親が $state で持つ source of truth を、子に派生形で渡し、子からの変更を逆算して親に書き戻したい」というケースに最適です。

<script lang="ts">
  // source of truth は celsius(摂氏)だけ
  let celsius = $state(20);
</script>

<label>
  摂氏: <input type="number" bind:value={celsius} />
</label>

<label>
  華氏:
  <!--
    getter: 摂氏 → 華氏に変換して表示
    setter: 華氏で書き戻された値を、摂氏に逆変換して celsius に反映
  -->
  <input
    type="number"
    bind:value={
      () => celsius * 9 / 5 + 32,
      (v: number) => celsius = (v - 32) * 5 / 9
    }
  />
</label>

$state を 2 つ用意して片方を派生させる必要がないため、データフローが単純になります。

$effect で値を同期させるアンチパターンとの対比

Svelte 4 までは、「$: で双方向の同期を書く」ようなパターンがしばしば見られました。Svelte 5 でも同じことを $effect で書きたくなりますが、双方向の sync を $effect で書くのは原則アンチパターンです。

<!-- ❌ アンチパターン:$effect で双方向 sync -->
<script lang="ts">
  let celsius = $state(20);
  let fahrenheit = $state(68);

  $effect(() => {
    fahrenheit = celsius * 9 / 5 + 32;
  });
  $effect(() => {
    celsius = (fahrenheit - 32) * 5 / 9;
  });
</script>

このコードは

  1. 状態が celsiusfahrenheit2 つになり、どちらが真実かが不明瞭
  2. $effect が互いを書き換え合うため、更新タイミングや無限ループの危険を抱える
  3. 初期値が一致していないとレンダリング結果がブレる

といった問題を持ちます。これを Function bindings で書き換えると、

<!-- ✅ Function bindings:真実は celsius ひとつ -->
<script lang="ts">
  let celsius = $state(20);
</script>

<input type="number" bind:value={celsius} />
<input
  type="number"
  bind:value={
    () => celsius * 9 / 5 + 32,
    (v: number) => celsius = (v - 32) * 5 / 9
  }
/>

状態が 1 つに集約され、変換ロジックがバインディング箇所に局所化されます。

判断基準
  • 「値の表示と書き戻しで形を変換したいだけ」 → Function bindings
  • 「外部の世界(DOM API・タイマー・ローカルストレージ等)に副作用を出したい」 → $effect
  • 「派生値を一方向に計算するだけ」 → $derived / $derived.by

$effect は副作用専用の escape hatch であり、状態どうしを同期させる用途には基本使わないと覚えておくと整理しやすくなります。

TypeScript での型

getter / setter の戻り値・引数の型は、対象プロパティ($bindable の型や要素プロパティ)から推論されます。明示する場合は setter 側に注釈を書きます。

// 子コンポーネント
type Props = {
  value: string;
};

let { value = $bindable('') }: Props = $props();
<!-- 親側:v は string として推論される -->
<Child
  bind:value={
    () => normalized,
    (v) => normalized = v.trim()
    //    ^? (parameter) v: string
  }
/>

<!-- number 型を明示するパターン -->
<input
  type="number"
  bind:value={
    () => count,
    (v: number) => count = Math.max(0, v)
  }
/>

よくある質問

Q: $stateなしでbind:は使えますか?

A: はい、使えます。Svelte 5でも通常の変数にbind:を使用できます。

<script>
  let normalVariable = 'initial';
</script>

<input bind:value={normalVariable} />

Q: $statebind:を常に一緒に使うべきですか?

A: いいえ、必須ではありません。$stateは内部状態の管理に、bind:はDOM要素との同期に使います。それぞれ独立して使用可能です。

まとめ

  • $state: コンポーネント内部のリアクティブな状態を管理する
  • bind:: DOM要素やコンポーネント間でデータを双方向に同期する
  • 両者は補完的な関係にあり、組み合わせることで強力なリアクティブUIを構築できる
ベストプラクティス
  • フォーム要素にはbind:を使って入力を簡潔に処理
  • 複雑な状態管理には$state$derivedを組み合わせる
  • コンポーネント間のデータ共有には$bindableプロップを活用