組み込みリアクティブクラス

Svelte 5では、標準的なJavaScriptの組み込みクラスのリアクティブ版が提供されています。これらのクラスは自動的に変更を追跡し、UIの更新をトリガーします。

概要

Svelte 5の組み込みリアクティブクラスは、通常のJavaScriptクラスと同じAPIを持ちながら、内部的な変更を自動的に追跡します。これにより、配列やMapのような複雑なデータ構造でも簡単にリアクティビティを実現できます。

React/Vue経験者向け
  • ReactのuseStateやVueのrefでラップする必要なく、直接的に操作可能
  • イミュータブルな更新パターンは不要
  • 内部状態の変更が自動的にUIに反映

主要な組み込みクラス

SvelteMap

Mapのリアクティブ版で、キーと値のペアを管理します。

<script lang="ts">
  import { SvelteMap } from 'svelte/reactivity';
  
  // 型安全なMapの作成
  let userScores = $state(new SvelteMap<string, number>());
  
  // 初期データの設定
  userScores.set('Alice', 100);
  userScores.set('Bob', 85);
  userScores.set('Charlie', 92);
  
  // 合計スコアの計算(自動更新)
  let totalScore = $derived(
    Array.from(userScores.values()).reduce((sum, score) => sum + score, 0)
  );
  
  let averageScore = $derived(
    userScores.size > 0 ? totalScore / userScores.size : 0
  );
  
  function addUser() {
    const name = prompt('ユーザー名:');
    if (name) {
      userScores.set(name, 0);
    }
  }
  
  function updateScore(name: string) {
    const currentScore = userScores.get(name) || 0;
    userScores.set(name, currentScore + 10);
  }
  
  function removeUser(name: string) {
    userScores.delete(name);
  }
</script>

<div class="scoreboard">
  <h3>スコアボード</h3>
  
  <div class="stats">
    <span>総スコア: {totalScore}</span>
    <span>平均: {averageScore.toFixed(1)}</span>
    <span>参加者: {userScores.size}</span>
  </div>
  
  <ul>
    {#each userScores as [name, score]}
      <li>
        <span>{name}: {score}</span>
        <button onclick={() => updateScore(name)}>+10</button>
        <button onclick={() => removeUser(name)}>削除</button>
      </li>
    {/each}
  </ul>
  
  <button onclick={addUser}>ユーザー追加</button>
</div>
typescript

SvelteSet

Setのリアクティブ版で、一意な値のコレクションを管理します。

<script lang="ts">
  import { SvelteSet } from 'svelte/reactivity';
  
  // 選択されたタグを管理
  let selectedTags = $state(new SvelteSet<string>());
  let availableTags = ['JavaScript', 'TypeScript', 'Svelte', 'React', 'Vue', 'CSS', 'HTML'];
  
  // 選択状態の切り替え
  function toggleTag(tag: string) {
    if (selectedTags.has(tag)) {
      selectedTags.delete(tag);
    } else {
      selectedTags.add(tag);
    }
  }
  
  // 全選択/全解除
  function selectAll() {
    availableTags.forEach(tag => selectedTags.add(tag));
  }
  
  function clearAll() {
    selectedTags.clear();
  }
  
  // 選択されたタグの配列(ソート済み)
  let selectedArray = $derived(
    Array.from(selectedTags).sort()
  );
</script>

<div class="tag-selector">
  <h3>タグ選択</h3>
  
  <div class="controls">
    <button onclick={selectAll}>全選択</button>
    <button onclick={clearAll}>クリア</button>
    <span>選択数: {selectedTags.size}</span>
  </div>
  
  <div class="tags">
    {#each availableTags as tag}
      <button
        class="tag"
        class:selected={selectedTags.has(tag)}
        onclick={() => toggleTag(tag)}
      >
        {tag}
      </button>
    {/each}
  </div>
  
  {#if selectedTags.size > 0}
    <div class="selected">
      <h4>選択中:</h4>
      <p>{selectedArray.join(', ')}</p>
    </div>
  {/if}
</div>

<style>
  .tag {
    padding: 0.5rem 1rem;
    margin: 0.25rem;
    border: 2px solid #ccc;
    background: white;
    cursor: pointer;
    border-radius: 20px;
  }
  
  .tag.selected {
    background: #3b82f6;
    color: white;
    border-color: #3b82f6;
  }
</style>
typescript

SvelteURL

URLのリアクティブ版で、URLの各部分を動的に管理します。

<script lang="ts">
  import { SvelteURL } from 'svelte/reactivity';
  
  // リアクティブなURL
  let apiUrl = $state(new SvelteURL('https://api.example.com/users'));
  
  // クエリパラメータの管理
  let page = $state(1);
  let limit = $state(10);
  let sortBy = $state('name');
  
  // URLの自動更新
  $effect(() => {
    apiUrl.searchParams.set('page', page.toString());
    apiUrl.searchParams.set('limit', limit.toString());
    apiUrl.searchParams.set('sort', sortBy);
  });
  
  // 完全なURLの取得
  let fullUrl = $derived(apiUrl.href);
  
  function nextPage() {
    page++;
  }
  
  function previousPage() {
    if (page > 1) page--;
  }
  
  function changeLimit(newLimit: number) {
    limit = newLimit;
    page = 1; // リセット
  }
</script>

<div class="url-builder">
  <h3>API URLビルダー</h3>
  
  <div class="url-display">
    <code>{fullUrl}</code>
  </div>
  
  <div class="controls">
    <div>
      <label>
        ページ:
        <button onclick={previousPage} disabled={page === 1}></button>
        <span>{page}</span>
        <button onclick={nextPage}></button>
      </label>
    </div>
    
    <div>
      <label>
        表示件数:
        <select bind:value={limit} onchange={() => page = 1}>
          <option value={10}>10</option>
          <option value={25}>25</option>
          <option value={50}>50</option>
          <option value={100}>100</option>
        </select>
      </label>
    </div>
    
    <div>
      <label>
        ソート:
        <select bind:value={sortBy}>
          <option value="name">名前</option>
          <option value="date">日付</option>
          <option value="score">スコア</option>
        </select>
      </label>
    </div>
  </div>
  
  <div class="url-parts">
    <h4>URL構成要素:</h4>
    <ul>
      <li>Protocol: {apiUrl.protocol}</li>
      <li>Host: {apiUrl.host}</li>
      <li>Pathname: {apiUrl.pathname}</li>
      <li>Search: {apiUrl.search}</li>
    </ul>
  </div>
</div>
typescript

SvelteDate

Dateのリアクティブ版で、日時の操作を追跡します。

<script lang="ts">
  import { SvelteDate } from 'svelte/reactivity';
  
  // リアクティブな日時
  let currentDate = $state(new SvelteDate());
  
  // フォーマット済みの表示
  let formattedDate = $derived(
    currentDate.toLocaleDateString('ja-JP', {
      year: 'numeric',
      month: 'long',
      day: 'numeric',
      weekday: 'long'
    })
  );
  
  let formattedTime = $derived(
    currentDate.toLocaleTimeString('ja-JP')
  );
  
  // 相対的な日付計算
  let daysFromNow = $state(0);
  let targetDate = $derived(() => {
    const date = new SvelteDate(currentDate);
    date.setDate(date.getDate() + daysFromNow);
    return date;
  });
  
  function addDays(days: number) {
    currentDate.setDate(currentDate.getDate() + days);
  }
  
  function setToToday() {
    currentDate.setTime(Date.now());
  }
  
  // タイマーで自動更新
  let autoUpdate = $state(false);
  
  $effect(() => {
    if (autoUpdate) {
      const interval = setInterval(() => {
        currentDate.setTime(Date.now());
      }, 1000);
      
      return () => clearInterval(interval);
    }
  });
</script>

<div class="date-picker">
  <h3>日時ピッカー</h3>
  
  <div class="current-datetime">
    <p class="date">{formattedDate}</p>
    <p class="time">{formattedTime}</p>
  </div>
  
  <div class="controls">
    <button onclick={() => addDays(-1)}>前日</button>
    <button onclick={setToToday}>今日</button>
    <button onclick={() => addDays(1)}>翌日</button>
  </div>
  
  <label>
    <input type="checkbox" bind:checked={autoUpdate} />
    リアルタイム更新
  </label>
  
  <div class="relative-date">
    <label>
      相対日付:
      <input type="range" bind:value={daysFromNow} min="-30" max="30" />
      <span>{daysFromNow > 0 ? '+' : ''}{daysFromNow}</span>
    </label>
    <p>対象日: {targetDate.toLocaleDateString('ja-JP')}</p>
  </div>
</div>
typescript

実践的な使用例

タスク管理システム

複数の組み込みクラスを組み合わせた実践例

<script lang="ts">
  import { SvelteMap, SvelteSet, SvelteDate } from 'svelte/reactivity';
  
  type Task = {
    id: string;
    title: string;
    createdAt: SvelteDate;
    tags: SvelteSet<string>;
    completed: boolean;
  };
  
  // タスクの管理
  let tasks = $state(new SvelteMap<string, Task>());
  let selectedTags = $state(new SvelteSet<string>());
  let allTags = $state(new SvelteSet<string>(['work', 'personal', 'urgent', 'someday']));
  
  // フィルタリングされたタスク
  let filteredTasks = $derived(() => {
    if (selectedTags.size === 0) {
      return Array.from(tasks.values());
    }
    
    return Array.from(tasks.values()).filter(task => {
      return Array.from(selectedTags).some(tag => task.tags.has(tag));
    });
  });
  
  // 統計情報
  let stats = $derived(() => {
    const all = Array.from(tasks.values());
    return {
      total: all.length,
      completed: all.filter(t => t.completed).length,
      pending: all.filter(t => !t.completed).length,
      urgent: all.filter(t => t.tags.has('urgent')).length
    };
  });
  
  function addTask(title: string, tagNames: string[] = []) {
    const id = crypto.randomUUID();
    const tags = new SvelteSet(tagNames);
    
    // 新しいタグを全体のタグリストに追加
    tagNames.forEach(tag => allTags.add(tag));
    
    tasks.set(id, {
      id,
      title,
      createdAt: new SvelteDate(),
      tags,
      completed: false
    });
  }
  
  function toggleTask(id: string) {
    const task = tasks.get(id);
    if (task) {
      task.completed = !task.completed;
      tasks.set(id, task); // 更新をトリガー
    }
  }
  
  function deleteTask(id: string) {
    tasks.delete(id);
  }
  
  function toggleTagFilter(tag: string) {
    if (selectedTags.has(tag)) {
      selectedTags.delete(tag);
    } else {
      selectedTags.add(tag);
    }
  }
  
  // サンプルデータ
  addTask('プロジェクト企画書作成', ['work', 'urgent']);
  addTask('買い物リスト作成', ['personal']);
  addTask('新機能の実装', ['work']);
</script>

<div class="task-manager">
  <h2>タスク管理</h2>
  
  <!-- 統計 -->
  <div class="stats">
    <div>全体: {stats.total}</div>
    <div>完了: {stats.completed}</div>
    <div>未完了: {stats.pending}</div>
    <div>緊急: {stats.urgent}</div>
  </div>
  
  <!-- タグフィルター -->
  <div class="tag-filter">
    <h4>フィルター:</h4>
    {#each allTags as tag}
      <button
        class:active={selectedTags.has(tag)}
        onclick={() => toggleTagFilter(tag)}
      >
        {tag}
      </button>
    {/each}
  </div>
  
  <!-- タスクリスト -->
  <div class="task-list">
    {#each filteredTasks as task}
      <div class="task" class:completed={task.completed}>
        <input
          type="checkbox"
          checked={task.completed}
          onchange={() => toggleTask(task.id)}
        />
        <div class="task-content">
          <div class="task-title">{task.title}</div>
          <div class="task-meta">
            <span class="task-date">
              {task.createdAt.toLocaleDateString('ja-JP')}
            </span>
            <div class="task-tags">
              {#each task.tags as tag}
                <span class="tag">{tag}</span>
              {/each}
            </div>
          </div>
        </div>
        <button onclick={() => deleteTask(task.id)}>削除</button>
      </div>
    {/each}
  </div>
</div>
typescript

パフォーマンスと最適化

メモリ効率

組み込みリアクティブクラスは、通常のJavaScriptクラスと比較して最小限のオーバーヘッドで動作します。

<script lang="ts">
  // 大量のデータを扱う場合
  let largeDataSet = $state(new SvelteMap<string, any>());
  
  // 遅延初期化パターン
  let lazyMap = $state.raw(null as SvelteMap<string, any> | null);
  
  function initializeMap() {
    if (!lazyMap) {
      lazyMap = new SvelteMap();
      // 大量のデータをロード
    }
  }
  
  // 必要な部分のみリアクティブに
  class DataManager {
    // リアクティブが不要な参照データ
    private staticData = new Map<string, any>();
    
    // リアクティブが必要な動的データ
    dynamicData = $state(new SvelteMap<string, any>());
    
    getStaticValue(key: string) {
      return this.staticData.get(key);
    }
    
    setDynamicValue(key: string, value: any) {
      this.dynamicData.set(key, value); // UIが更新される
    }
  }
</script>
typescript

更新の最適化

<script lang="ts">
  let items = $state(new SvelteMap<string, Item>());
  
  // バッチ更新パターン
  function batchUpdate(updates: Array<[string, Item]>) {
    // 一時的にリアクティビティを無効化
    const map = $state.raw(items);
    
    updates.forEach(([key, value]) => {
      map.set(key, value);
    });
    
    // 最後に一度だけ更新をトリガー
    items = new SvelteMap(map);
  }
  
  // 条件付き更新
  function conditionalUpdate(key: string, value: Item) {
    const current = items.get(key);
    
    // 実際に変更がある場合のみ更新
    if (!current || !isEqual(current, value)) {
      items.set(key, value);
    }
  }
</script>
typescript

TypeScriptとの統合

型定義の活用

<script lang="ts">
  // ジェネリック型を活用
  class TypedSvelteMap<K, V> extends SvelteMap<K, V> {
    // カスタムメソッドの追加
    getOrDefault(key: K, defaultValue: V): V {
      return this.get(key) ?? defaultValue;
    }
    
    // 型安全な更新メソッド
    update(key: K, updater: (value: V | undefined) => V): void {
      this.set(key, updater(this.get(key)));
    }
  }
  
  // 使用例
  let userSettings = $state(
    new TypedSvelteMap<string, UserSettings>()
  );
  
  userSettings.update('theme', (current) => ({
    ...current,
    darkMode: true
  }));
</script>
typescript

カスタム型ガード

<script lang="ts">
  // 型安全なSetの操作
  class TypedSvelteSet<T> extends SvelteSet<T> {
    hasAll(...values: T[]): boolean {
      return values.every(v => this.has(v));
    }
    
    hasAny(...values: T[]): boolean {
      return values.some(v => this.has(v));
    }
    
    intersection(other: Set<T>): TypedSvelteSet<T> {
      const result = new TypedSvelteSet<T>();
      for (const value of this) {
        if (other.has(value)) {
          result.add(value);
        }
      }
      return result;
    }
  }
  
  // 使用例
  let permissions = $state(
    new TypedSvelteSet<Permission>()
  );
  
  let canEdit = $derived(
    permissions.hasAll('read', 'write')
  );
</script>
typescript

ベストプラクティス

1. 適切なクラスの選択

// ❌ 悪い例:単純な配列操作にMapを使用
let items = $state(new SvelteMap());
items.set(0, 'first');
items.set(1, 'second');

// ✅ 良い例:配列として使用
let items = $state<string[]>([]);
items.push('first');
items.push('second');

// ✅ Mapはキーバリューペアのストレージとして使用
let cache = $state(new SvelteMap<string, CachedData>());
cache.set('user:123', userData);
typescript

2. イミュータブル操作の回避

// ❌ 悪い例:不要なイミュータブル操作
let items = $state(new SvelteSet([1, 2, 3]));
items = new SvelteSet([...items, 4]); // 非効率

// ✅ 良い例:直接操作
let items = $state(new SvelteSet([1, 2, 3]));
items.add(4); // 効率的
typescript

3. 派生値の活用

// ✅ 派生値で計算結果をキャッシュ
let data = $state(new SvelteMap());

let summary = $derived(() => {
  const values = Array.from(data.values());
  return {
    count: values.length,
    sum: values.reduce((a, b) => a + b, 0),
    average: values.length > 0 ? sum / values.length : 0
  };
});
typescript

まとめ

Svelte 5の組み込みリアクティブクラスは、複雑なデータ構造を扱う際の強力なツールです。通常のJavaScriptクラスと同じAPIを保ちながら、自動的な変更追跡とUI更新を提供します。

重要なポイント

  1. 自動的な変更追跡 - 内部状態の変更が自動的に検出される
  2. 標準API準拠 - 通常のMap、Set、URL、Dateと同じメソッド
  3. TypeScript対応 - 完全な型安全性
  4. パフォーマンス - 最小限のオーバーヘッド
  5. 直感的な操作 - イミュータブル更新パターンが不要
関連リンク

次は TypeScriptパターン で、より高度な型定義テクニックを学びましょう。

Last update at: 2025/08/14 07:39:43