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() で親から渡された値を受け取ります。onIncrement・onDecrement はボタンを押したときに親のコールバック関数を呼び出します。状態そのものは子コンポーネントが持たず、親が管理して子に渡す設計です。
親コンポーネント
<!-- 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 で管理します。Counter と Display の両方に同じ 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に移行するのが実務ではよくある流れです。