Svelte 5のコンポーネント間の状態共有(propsとstore)を整理した話

Svelte 5でコンポーネント間の状態を共有する方法は主に2つあります。 props(親から子へ渡す)と store(どこからでも参照できる共有ファイル)です。 それぞれの実装方法と使い分けの判断基準を整理します。

2つの方法の概要

// props(親子間)
親が $state を持ち、子に渡す
親 → 子 → 孫  ← 階層が深いとバケツリレーになる

// store(どこからでも)
$lib/store.svelte.ts に状態を置く
コンポーネントA ↘
コンポーネントB → countStore  ← どこからでも直接参照
コンポーネントC ↗

方法① props でコンポーネント間に状態を渡す

親コンポーネントが $state を持ち、子コンポーネントに $props() で受け取らせる方法です。

子コンポーネント

<!-- src/lib/components/Counter.svelte -->
<script lang="ts">
  let { count, onIncrement, onDecrement } = $props();
</script>

<div>
  <p>カウント: {count}</p>
  <button onclick={onIncrement}>+1</button>
  <button onclick={onDecrement}>-1</button>
</div>
<!-- src/lib/components/Display.svelte -->
<script lang="ts">
  let { count } = $props();
</script>

<div>
  <p>Displayコンポーネントから見たカウント: {count}</p>
</div>

$props() で親から渡された値を受け取ります。onIncrementonDecrement はボタンを押したときに親のコールバック関数を呼び出します。状態そのものは子コンポーネントが持たず、親が管理して子に渡す設計です。

親コンポーネント

<!-- src/routes/learn/+page.svelte -->
<script lang="ts">
  import Counter from '$lib/components/Counter.svelte';
  import Display from '$lib/components/Display.svelte';

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

<Counter
  count={count}
  onIncrement={() => count++}
  onDecrement={() => count--}
/>

<Display count={count} />

count は親が $state で管理します。CounterDisplay の両方に同じ count を渡しているので、ボタンを押すと両コンポーネントが同時に更新されます。


方法② store ファイルで状態を共有する

$lib/store.svelte.ts に状態を置いて、どのコンポーネントからでもimportして使う方法です。

store ファイルを作成する

// src/lib/store.svelte.ts
function createCountStore() {
  let count = $state(0);

  return {
    get count() { return count; },
    increment() { count++; },
    decrement() { count--; },
  };
}

export const countStore = createCountStore();

ポイント:

ファイル名が .svelte.ts である点が重要です。.ts だけだと $state が使えません。

get count() はgetterです。通常のプロパティとして返すと、値がコピーされてリアクティビティが失われます。

// ❌ これだと countStore.count は固定値になる
return {
  count: count,  // この時点の値がコピーされる
};

// ✅ getterなら常に最新の値を返す
return {
  get count() { return count; },
};

コンポーネントから直接参照する

<!-- src/lib/components/Counter.svelte -->
<script lang="ts">
  import { countStore } from '$lib/store.svelte';
</script>

<div>
  <p>カウント: {countStore.count}</p>
  <button onclick={() => countStore.increment()}>+1</button>
  <button onclick={() => countStore.decrement()}>-1</button>
</div>
<!-- src/lib/components/Display.svelte -->
<script lang="ts">
  import { countStore } from '$lib/store.svelte';
</script>

<div>
  <p>Displayコンポーネントから見たカウント: {countStore.count}</p>
</div>
<!-- src/routes/learn/+page.svelte -->
<script lang="ts">
  import Counter from '$lib/components/Counter.svelte';
  import Display from '$lib/components/Display.svelte';
</script>

<Counter />
<Display />

親は子に何も渡さなくてよくなります。各コンポーネントが countStore を直接importして使います。


実務での使い分け

props が向いているケース

UIコンポーネントの汎用パーツ

<Button label="送信" onclick={handleSubmit} />
<Card title="記事タイトル" body="本文" />

同じコンポーネントを色々な場所で異なる値で使いたいとき。storeだと1種類の値しか持てませんが、propsなら呼び出しごとに違う値を渡せます。

コンポーネントの階層が浅いとき

2〜3階層程度で状態が親子間だけで完結するならpropsで十分です。

store が向いているケース

ログインユーザー情報

ヘッダー・サイドバー・各ページなど、あらゆる場所でユーザー情報を使うためpropsでは回しきれません。

ショッピングカートの中身

商品一覧・カートアイコン・決済ページなど複数箇所で共有が必要なデータ。

グローバルなUI状態

モーダルの開閉・通知・テーマ切り替えなど。

バケツリレーが3階層以上になるとき

ページ → セクション → リスト → リストアイテム → ボタン

途中のコンポーネントが「自分では使わないのに渡すだけ」の状態になったらstoreに切り替えるサインです。


判断フロー

状態を変更するのは誰か?
  ├─ 子コンポーネントだけ → props + コールバック
  └─ 複数の無関係なコンポーネント → store

状態を参照するのは何箇所か?
  ├─ 直接の親子関係のみ → props
  └─ 離れた場所からも参照 → store

同じコンポーネントを複数インスタンス使うか?
  ├─ 使う(リスト内のカードなど) → props
  └─ 使わない → どちらでも

まとめ

props store
状態の場所 親コンポーネント $lib/store.svelte.ts
アクセス方法 親から子へ渡す どこからでもimport
向いているケース シンプルな親子関係・汎用UIパーツ 複数箇所で共有する状態
ファイル拡張子 .svelte .svelte.ts$stateを使うため)

迷ったらまずpropsで作って、バケツリレーが辛くなってきたタイミングでstoreに移行するのが実務ではよくある流れです。

← トップページに戻る