さとまたwiki

状態管理(Runes)

Svelte 5のリアクティブな状態管理

Runesとは?

日常での例え:スマートホーム

普通の家

電気をつけたい
→ スイッチまで歩いて押す

スマートホーム

「暗くなった」という状態変化
→ 自動で電気がつく

なぜ状態管理が必要?

状態管理がないと困ること:

  • 変数を変更しても画面が更新されない
  • 「カートに3個入ってます」の数字が変わらない
  • 手動でHTMLを書き換える必要がある(大変!)

状態管理があれば:データが変わると、画面も自動で更新される!

Svelte 5の新しい書き方「Runes」

Runes(ルーン)は $ で始まる特別な関数です:

  • $state - 変更を追跡する状態(カウンター、入力値など)
  • $derived - 他の状態から自動計算される値(合計金額など)
  • $effect - 状態が変わったら実行される処理(保存、API呼び出し)
  • $props - 親コンポーネントから受け取る値

用語解説

状態(State):アプリの「今どうなっているか」を表すデータ(例:カートの中身、ログイン状態)

リアクティブ:データが変わると、それに関連する表示も自動で変わる仕組み

コンポーネント:画面の部品。ボタン、カード、ヘッダーなど再利用できるパーツ

props(プロップス):親から子コンポーネントに渡すデータ(例:ボタンに「送信」という文字を渡す)

$state - 状態の定義

基本的な$state

変更を追跡する変数

svelte
<script>
  // $stateでリアクティブな状態を作る
  let count = $state(0);
  let name = $state('');
  let items = $state([]);

  function increment() {
    count++;  // 自動で画面が更新される
  }

  function addItem() {
    items.push('新しいアイテム');  // 配列の変更も検知
  }
</script>

<p>カウント: {count}</p>
<button onclick={increment}>+1</button>

<input bind:value={name} />
<p>こんにちは、{name}さん</p>
プレビュー

カウント: 0


こんにちは、さん

コード解説:$stateの動作

① 変数の宣言

let count = $state(0);

$state(0) で「0」という初期値を持つ、変更を追跡する変数を作成。普通の変数との違いは、値が変わると画面も自動で更新されること。

② 関数での更新

function increment() { count++; }

普通のJavaScriptと同じように count++ で値を増やせます。ただし、$stateで作った変数なので、画面の「カウント: 0」も自動で「カウント: 1」に変わります

③ bind:value で双方向バインディング

<input bind:value={name} />

入力欄に文字を打つと name 変数が更新され、同時に {name} で表示している場所も更新されます。

処理の流れ

1. ボタンをクリック → 2. increment()が実行 → 3. count++ でcountが1に → 4. 画面の {count} が自動で「1」に更新

オブジェクトの状態

ネストしたデータも追跡

svelte
<script>
  let user = $state({
    name: '田中',
    profile: {
      age: 25,
      email: 'tanaka@example.com'
    }
  });

  function updateAge() {
    // ネストしたプロパティも変更を検知!
    user.profile.age++;
  }
</script>

<p>{user.name}さん({user.profile.age}歳)</p>
<button onclick={updateAge}>年齢+1</button>
プレビュー
// $stateはディープリアクティブ
// ネストしたプロパティも追跡される

$derived - 計算された値

$derivedとは?

他の状態から自動的に計算される値を作ります。
元の値が変わると、derivedも自動で再計算されます。

基本的な$derived

他の状態から計算

svelte
<script>
  let items = $state([
    { name: 'りんご', price: 100 },
    { name: 'みかん', price: 80 },
    { name: 'バナナ', price: 120 }
  ]);

  // itemsが変わると自動で再計算
  let total = $derived(
    items.reduce((sum, item) => sum + item.price, 0)
  );

  let count = $derived(items.length);

  function addItem() {
    items.push({ name: '新商品', price: 150 });
    // totalとcountは自動更新!
  }
</script>

<p>商品数: {count}</p>
<p>合計: {total}円</p>
プレビュー

商品数: 3

合計: 300円

itemsが変わると自動更新

コード解説:$derivedの動作

① 元のデータ($state)

let items = $state([{name: "りんご", price: 100}, ...]);

商品データの配列。これが「元」になるデータです。

② 合計金額の自動計算

let total = $derived(items.reduce((sum, item) => sum + item.price, 0));
  • reduce():配列の各要素を足し合わせる JavaScript のメソッド
  • $derived():itemsが変わると total も自動で再計算される
  • 100 + 80 + 120 = 300

③ 商品数の自動計算

let count = $derived(items.length);

配列の長さ(3)を自動で追跡。商品が追加されると自動で更新されます。

addItem()を実行すると...

1. items に新商品(150円)が追加される → 2. total が自動で 450 に更新 → 3. count が自動で 4 に更新

手動で total = ... と書き直す必要がないのが $derived の便利なところ!

$derived.by - 複雑な計算

関数で計算ロジックを書く

svelte
<script>
  let items = $state([...]);
  let filter = $state('all');  // 'all' | 'expensive' | 'cheap'

  // 複雑なロジックは $derived.by を使う
  let filteredItems = $derived.by(() => {
    if (filter === 'all') return items;

    if (filter === 'expensive') {
      return items.filter(item => item.price >= 100);
    }

    return items.filter(item => item.price < 100);
  });

  let message = $derived.by(() => {
    const count = filteredItems.length;
    if (count === 0) return '商品がありません';
    return `${count}件の商品があります`;
  });
</script>

<select bind:value={filter}>
  <option value="all">すべて</option>
  <option value="expensive">100円以上</option>
  <option value="cheap">100円未満</option>
</select>

<p>{message}</p>
プレビュー
$derived(単純な式)
$derived.by(() => 複雑なロジック)

$effect - 副作用

$effectとは?

状態が変わったときに自動で実行される処理を定義します。
ログ出力、ローカルストレージ保存、外部APIの呼び出しなどに使います。

基本的な$effect

状態変化時に実行

svelte
<script>
  let searchQuery = $state('');
  let results = $state([]);

  // searchQueryが変わるたびに実行
  $effect(() => {
    console.log('検索クエリ:', searchQuery);

    // 空でなければAPIを呼ぶ
    if (searchQuery.length > 2) {
      fetch(`/api/search?q=${searchQuery}`)
        .then(r => r.json())
        .then(data => {
          results = data;
        });
    }
  });
</script>

<input bind:value={searchQuery} placeholder="検索..." />
プレビュー
// $effect内で使った$stateが変わると
// 自動的に再実行される

クリーンアップ処理

effectの後片付け

svelte
<script>
  let isConnected = $state(false);

  $effect(() => {
    // WebSocket接続
    const ws = new WebSocket('wss://example.com');

    ws.onopen = () => {
      isConnected = true;
    };

    ws.onclose = () => {
      isConnected = false;
    };

    // クリーンアップ関数を返す
    return () => {
      ws.close();  // コンポーネント破棄時に実行
    };
  });
</script>

<p>接続状態: {isConnected ? '接続中' : '切断'}</p>
プレビュー
// return () => { ... }
// コンポーネント破棄時や
// 次のeffect実行前に呼ばれる

ローカルストレージとの連携

よくある使用例

svelte
<script>
  // 初期値はローカルストレージから
  let theme = $state(
    localStorage.getItem('theme') || 'light'
  );

  // themeが変わるたびに保存
  $effect(() => {
    localStorage.setItem('theme', theme);
    document.documentElement.classList.toggle('dark', theme === 'dark');
  });

  function toggleTheme() {
    theme = theme === 'light' ? 'dark' : 'light';
  }
</script>

<button onclick={toggleTheme}>
  {theme === 'light' ? '🌙' : '☀️'} テーマ切替
</button>
プレビュー

$props - プロパティ

コンポーネントのprops

親から値を受け取る

svelte
<!-- Button.svelte -->
<script>
  // propsを受け取る
  let {
    label,
    variant = 'primary',  // デフォルト値
    disabled = false,
    onclick  // イベントハンドラ
  } = $props();
</script>

<button
  class="btn btn-{variant}"
  {disabled}
  {onclick}
>
  {label}
</button>

<!-- 親コンポーネントから使う -->
<Button
  label="送信"
  variant="primary"
  onclick={() => console.log('クリック')}
/>
プレビュー
let { prop1, prop2 } = $props()
// 分割代入でpropsを受け取る

TypeScriptとの組み合わせ

型安全なprops

svelte
<script lang="ts">
  interface Props {
    title: string;
    count: number;
    items?: string[];  // オプショナル
    onSave: (data: string) => void;
  }

  let {
    title,
    count,
    items = [],
    onSave
  }: Props = $props();
</script>

<h2>{title}</h2>
<p>カウント: {count}</p>
<ul>
  {#each items as item}
    <li>{item}</li>
  {/each}
</ul>
プレビュー
interface Props {...}
// TypeScriptで型定義

グローバル状態

コンポーネント間で状態を共有するには?

別ファイルで状態を定義し、importして使います。
Svelte 5では.svelte.tsファイルでrunesが使えます。

グローバルストア

.svelte.tsファイルで状態管理

typescript
// src/lib/stores/counter.svelte.ts
let count = $state(0);

export function getCount() {
  return count;
}

export function increment() {
  count++;
}

export function decrement() {
  count--;
}

// ----- 使う側 -----
// src/routes/+page.svelte
<script>
  import { getCount, increment, decrement } from '$lib/stores/counter.svelte';

  // リアクティブに値を取得
  let count = $derived(getCount());
</script>

<p>カウント: {count}</p>
<button onclick={increment}>+</button>
<button onclick={decrement}>-</button>
プレビュー
counter.svelte.ts
// .svelte.ts でrunesが使える
// 複数コンポーネントで共有

ユーザー状態の例

認証状態の管理

typescript
// src/lib/stores/auth.svelte.ts
interface User {
  id: number;
  name: string;
  email: string;
}

let user = $state<User | null>(null);

export function getUser() {
  return user;
}

export function isLoggedIn() {
  return user !== null;
}

export function login(userData: User) {
  user = userData;
}

export function logout() {
  user = null;
}

// ----- 使う側 -----
<script>
  import { getUser, isLoggedIn, logout } from '$lib/stores/auth.svelte';

  let user = $derived(getUser());
  let loggedIn = $derived(isLoggedIn());
</script>

{#if loggedIn}
  <p>ようこそ、{user.name}さん</p>
  <button onclick={logout}>ログアウト</button>
{:else}
  <a href="/login">ログイン</a>
{/if}
プレビュー

ようこそ、田中さん