T-CREATOR

Svelte のリアクティブ宣言で書くモダンな状態管理

Svelte のリアクティブ宣言で書くモダンな状態管理

Svelte のリアクティブ宣言は、状態管理の概念を根本から変える革新的な機能です。

従来の React や Vue では、状態の変更を明示的に管理し、コンポーネントの再レンダリングを手動で制御する必要がありました。しかし、Svelte のリアクティブ宣言を使えば、状態の依存関係を自動的に追跡し、必要な部分だけを効率的に更新できるようになります。

この記事では、Svelte のリアクティブ宣言の魅力と実践的な使い方を、実際のコード例とエラーケースを交えて詳しく解説していきます。あなたの開発体験が劇的に変わる瞬間を、一緒に体験してみましょう。

リアクティブ宣言の基本概念

Svelte のリアクティブ宣言は、$:という特殊な構文を使って実現されます。この記号は「この値が変更されたら、以下の処理を実行する」という意味を持ちます。

基本的なリアクティブ宣言

javascript<script>
  let count = 0;
  let doubled = 0;

  // リアクティブ宣言:countが変更されると自動的にdoubledも更新される
  $: doubled = count * 2;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>
  Count: {count}, Doubled: {doubled}
</button>

この例では、countの値が変更されるたびに、doubledが自動的に再計算されます。従来のフレームワークでは、このような依存関係を手動で管理する必要がありましたが、Svelte では宣言的に記述するだけで済みます。

リアクティブ文の活用

リアクティブ宣言は値の代入だけでなく、任意の JavaScript 文を実行することもできます。

javascript<script>
  let user = { name: '田中', age: 25 };
  let isAdult = false;

  // 複雑な条件判定もリアクティブに実行
  $: {
    isAdult = user.age >= 20;
    console.log(`${user.name}${isAdult ? '成人' : '未成年'}です`);
  }
</script>

<div>
  <p>{user.name} ({user.age}歳)</p>
  <p>ステータス: {isAdult ? '成人' : '未成年'}</p>
</div>

従来の状態管理との違い

React での状態管理

React では、状態の変更を明示的に管理し、適切なタイミングでコンポーネントを再レンダリングする必要があります。

javascriptimport React, { useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [doubled, setDoubled] = useState(0);

  // useEffectで副作用を管理
  useEffect(() => {
    setDoubled(count * 2);
  }, [count]);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}, Doubled: {doubled}
    </button>
  );
}

Vue での状態管理

Vue でも同様に、computed プロパティや watch を使って状態の依存関係を管理します。

javascript<template>
  <button @click="increment">
    Count: {{ count }}, Doubled: {{ doubled }}
  </button>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    };
  },
  computed: {
    doubled() {
      return this.count * 2;
    }
  },
  methods: {
    increment() {
      this.count += 1;
    }
  }
};
</script>

Svelte のリアクティブ宣言の優位性

Svelte のリアクティブ宣言は、これらの複雑な設定を不要にします。

javascript<script>
  let count = 0;

  // たった1行で依存関係を宣言
  $: doubled = count * 2;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>
  Count: {count}, Doubled: {doubled}
</button>

コードが簡潔になり、可読性が大幅に向上します。また、パフォーマンスも最適化されており、必要な部分だけが効率的に更新されます。

リアクティブ宣言の実装方法

基本的な実装パターン

リアクティブ宣言には、いくつかの実装パターンがあります。それぞれの特徴を理解して、適切な場面で使い分けましょう。

1. 単純な値の計算

javascript<script>
  let price = 1000;
  let quantity = 2;
  let tax = 0.1;

  // 基本的な計算
  $: subtotal = price * quantity;
  $: taxAmount = subtotal * tax;
  $: total = subtotal + taxAmount;
</script>

<div>
  <p>小計: ¥{subtotal}</p>
  <p>税額: ¥{taxAmount}</p>
  <p>合計: ¥{total}</p>
</div>

2. 条件付きの計算

javascript<script>
  let score = 85;
  let grade = '';

  // 条件に基づく値の設定
  $: {
    if (score >= 90) grade = 'A';
    else if (score >= 80) grade = 'B';
    else if (score >= 70) grade = 'C';
    else grade = 'D';
  }
</script>

<div>
  <p>スコア: {score}点</p>
  <p>評価: {grade}</p>
</div>

3. 配列やオブジェクトの処理

javascript<script>
  let items = [
    { name: 'りんご', price: 100, quantity: 3 },
    { name: 'バナナ', price: 80, quantity: 2 },
    { name: 'オレンジ', price: 120, quantity: 1 }
  ];

  // 配列の集計処理
  $: totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
  $: totalCost = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  $: averagePrice = totalCost / totalItems;
</script>

<div>
  <p>総アイテム数: {totalItems}個</p>
  <p>総コスト: ¥{totalCost}</p>
  <p>平均価格: ¥{averagePrice}</p>
</div>

高度な実装テクニック

非同期処理との組み合わせ

javascript<script>
  let userId = 1;
  let user = null;
  let loading = false;
  let error = null;

  // 非同期処理を含むリアクティブ宣言
  $: {
    if (userId) {
      loading = true;
      error = null;

      fetch(`/api/users/${userId}`)
        .then(response => response.json())
        .then(data => {
          user = data;
          loading = false;
        })
        .catch(err => {
          error = err.message;
          loading = false;
        });
    }
  }
</script>

{#if loading}
  <p>読み込み中...</p>
{:else if error}
  <p>エラー: {error}</p>
{:else if user}
  <div>
    <h2>{user.name}</h2>
    <p>Email: {user.email}</p>
  </div>
{/if}

複数の依存関係を持つリアクティブ宣言

javascript<script>
  let firstName = '田中';
  let lastName = '太郎';
  let showFullName = true;

  // 複数の変数に依存するリアクティブ宣言
  $: displayName = showFullName
    ? `${lastName} ${firstName}`
    : firstName;

  // 条件付きで実行されるリアクティブ宣言
  $: if (showFullName) {
    console.log(`フルネーム表示: ${displayName}`);
  }
</script>

<div>
  <label>
    <input type="checkbox" bind:checked={showFullName}>
    フルネームを表示
  </label>
  <p>表示名: {displayName}</p>
</div>

実践的な使用例

ショッピングカートの実装

リアクティブ宣言を使って、実際のアプリケーションでよく使われるショッピングカートを実装してみましょう。

javascript<script>
  let cart = [
    { id: 1, name: '商品A', price: 1000, quantity: 2 },
    { id: 2, name: '商品B', price: 1500, quantity: 1 },
    { id: 3, name: '商品C', price: 800, quantity: 3 }
  ];

  // リアクティブ宣言で自動計算
  $: subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  $: tax = subtotal * 0.1;
  $: shipping = subtotal > 5000 ? 0 : 500;
  $: total = subtotal + tax + shipping;
  $: itemCount = cart.reduce((sum, item) => sum + item.quantity, 0);

  function updateQuantity(id, newQuantity) {
    cart = cart.map(item =>
      item.id === id ? { ...item, quantity: newQuantity } : item
    );
  }

  function removeItem(id) {
    cart = cart.filter(item => item.id !== id);
  }
</script>

<div class="cart">
  <h2>ショッピングカート ({itemCount}アイテム)</h2>

  {#each cart as item}
    <div class="cart-item">
      <span>{item.name}</span>
      <input
        type="number"
        min="1"
        value={item.quantity}
        on:change={(e) => updateQuantity(item.id, parseInt(e.target.value))}
      >
      <span>¥{item.price * item.quantity}</span>
      <button on:click={() => removeItem(item.id)}>削除</button>
    </div>
  {/each}

  <div class="summary">
    <p>小計: ¥{subtotal}</p>
    <p>税額: ¥{tax}</p>
    <p>送料: ¥{shipping}</p>
    <p><strong>合計: ¥{total}</strong></p>
  </div>
</div>

フォームバリデーション

リアクティブ宣言を使って、リアルタイムのフォームバリデーションを実装します。

javascript<script>
  let email = '';
  let password = '';
  let confirmPassword = '';

  // リアクティブなバリデーション
  $: emailError = email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
    ? '有効なメールアドレスを入力してください'
    : '';

  $: passwordError = password && password.length < 8
    ? 'パスワードは8文字以上で入力してください'
    : '';

  $: confirmPasswordError = confirmPassword && password !== confirmPassword
    ? 'パスワードが一致しません'
    : '';

  $: isValid = email && password && confirmPassword &&
               !emailError && !passwordError && !confirmPasswordError;

  function handleSubmit() {
    if (isValid) {
      console.log('フォーム送信:', { email, password });
    }
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <div>
    <label for="email">メールアドレス:</label>
    <input
      id="email"
      type="email"
      bind:value={email}
      class:error={emailError}
    >
    {#if emailError}
      <span class="error-message">{emailError}</span>
    {/if}
  </div>

  <div>
    <label for="password">パスワード:</label>
    <input
      id="password"
      type="password"
      bind:value={password}
      class:error={passwordError}
    >
    {#if passwordError}
      <span class="error-message">{passwordError}</span>
    {/if}
  </div>

  <div>
    <label for="confirm-password">パスワード確認:</label>
    <input
      id="confirm-password"
      type="password"
      bind:value={confirmPassword}
      class:error={confirmPasswordError}
    >
    {#if confirmPasswordError}
      <span class="error-message">{confirmPasswordError}</span>
    {/if}
  </div>

  <button type="submit" disabled={!isValid}>
    登録
  </button>
</form>

データフィルタリングとソート

リアクティブ宣言を使って、動的なデータフィルタリングとソート機能を実装します。

javascript<script>
  let products = [
    { id: 1, name: 'りんご', price: 100, category: 'フルーツ' },
    { id: 2, name: 'バナナ', price: 80, category: 'フルーツ' },
    { id: 3, name: 'にんじん', price: 60, category: '野菜' },
    { id: 4, name: 'トマト', price: 120, category: '野菜' },
    { id: 5, name: 'オレンジ', price: 150, category: 'フルーツ' }
  ];

  let searchTerm = '';
  let selectedCategory = 'すべて';
  let sortBy = 'name';
  let sortOrder = 'asc';

  // リアクティブなフィルタリングとソート
  $: filteredProducts = products.filter(product => {
    const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase());
    const matchesCategory = selectedCategory === 'すべて' || product.category === selectedCategory;
    return matchesSearch && matchesCategory;
  });

  $: sortedProducts = [...filteredProducts].sort((a, b) => {
    let aValue = a[sortBy];
    let bValue = b[sortBy];

    if (typeof aValue === 'string') {
      aValue = aValue.toLowerCase();
      bValue = bValue.toLowerCase();
    }

    if (sortOrder === 'asc') {
      return aValue > bValue ? 1 : -1;
    } else {
      return aValue < bValue ? 1 : -1;
    }
  });

  $: categories = ['すべて', ...new Set(products.map(p => p.category))];
</script>

<div class="product-list">
  <div class="controls">
    <input
      type="text"
      placeholder="商品名で検索..."
      bind:value={searchTerm}
    >

    <select bind:value={selectedCategory}>
      {#each categories as category}
        <option value={category}>{category}</option>
      {/each}
    </select>

    <select bind:value={sortBy}>
      <option value="name">名前順</option>
      <option value="price">価格順</option>
      <option value="category">カテゴリ順</option>
    </select>

    <button on:click={() => sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'}>
      {sortOrder === 'asc' ? '昇順' : '降順'}
    </button>
  </div>

  <div class="products">
    {#each sortedProducts as product}
      <div class="product">
        <h3>{product.name}</h3>
        <p>価格: ¥{product.price}</p>
        <p>カテゴリ: {product.category}</p>
      </div>
    {/each}
  </div>

  <p>表示件数: {sortedProducts.length}件</p>
</div>

パフォーマンスの最適化

リアクティブ宣言の最適化テクニック

リアクティブ宣言は強力ですが、適切に使わないとパフォーマンスの問題を引き起こす可能性があります。以下のテクニックを活用して、効率的なコードを書きましょう。

1. 不要な再計算を避ける

javascript<script>
  let items = [/* 大量のデータ */];
  let filter = '';

  // ❌ 非効率:毎回新しい配列を作成
  $: filteredItems = items.filter(item =>
    item.name.includes(filter)
  );

  // ✅ 効率的:メモ化を活用
  let filteredItemsCache = [];
  $: {
    if (filter === '') {
      filteredItemsCache = items;
    } else {
      filteredItemsCache = items.filter(item =>
        item.name.includes(filter)
      );
    }
  }
</script>

2. 重い計算の最適化

javascript<script>
  let data = [/* 大量のデータ */];

  // ❌ 非効率:毎回全データを処理
  $: expensiveCalculation = data.reduce((result, item) => {
    // 重い計算処理
    return result + complexOperation(item);
  }, 0);

  // ✅ 効率的:必要な時だけ計算
  let lastDataLength = 0;
  let cachedResult = 0;

  $: {
    if (data.length !== lastDataLength) {
      cachedResult = data.reduce((result, item) => {
        return result + complexOperation(item);
      }, 0);
      lastDataLength = data.length;
    }
  }
</script>

3. 条件付きリアクティブ宣言

javascript<script>
  let user = null;
  let userPreferences = null;

  // 条件付きでリアクティブ宣言を実行
  $: if (user && user.id) {
    // ユーザーが存在する時だけ実行
    fetchUserPreferences(user.id).then(prefs => {
      userPreferences = prefs;
    });
  }
</script>

メモリリークの防止

リアクティブ宣言で非同期処理を行う際は、メモリリークに注意が必要です。

javascript<script>
  let userId = 1;
  let userData = null;

  // ✅ 適切なクリーンアップ
  let abortController = null;

  $: {
    if (userId) {
      // 前のリクエストをキャンセル
      if (abortController) {
        abortController.abort();
      }

      abortController = new AbortController();

      fetch(`/api/users/${userId}`, {
        signal: abortController.signal
      })
        .then(response => response.json())
        .then(data => {
          userData = data;
        })
        .catch(error => {
          if (error.name !== 'AbortError') {
            console.error('エラー:', error);
          }
        });
    }
  }

  // コンポーネントのクリーンアップ
  onDestroy(() => {
    if (abortController) {
      abortController.abort();
    }
  });
</script>

よくある落とし穴と対策

1. 無限ループの回避

リアクティブ宣言で最も注意が必要なのは、無限ループを引き起こすことです。

javascript<script>
  let count = 0;

  // ❌ 無限ループ:countが変更されると、またcountが変更される
  $: count = count + 1;

  // ✅ 正しい実装:条件付きで更新
  $: if (someCondition) {
    count = count + 1;
  }
</script>

2. オブジェクトの変更検知

Svelte のリアクティブ宣言は、オブジェクトのプロパティ変更を自動的に検知しません。

javascript<script>
  let user = { name: '田中', age: 25 };

  // ❌ 動作しない:プロパティ変更を検知しない
  $: console.log('ユーザー情報:', user.name);

  // ✅ 正しい実装:オブジェクト全体を再代入
  function updateUserName(newName) {
    user = { ...user, name: newName };
  }

  // または、個別の変数に分離
  let userName = user.name;
  $: console.log('ユーザー名:', userName);
</script>

3. 配列の変更検知

配列の変更も同様に、適切な方法で行う必要があります。

javascript<script>
  let items = ['りんご', 'バナナ', 'オレンジ'];

  // ❌ 動作しない:pushメソッドの変更を検知しない
  function addItem(item) {
    items.push(item);
  }

  // ✅ 正しい実装:新しい配列を作成
  function addItem(item) {
    items = [...items, item];
  }

  // または、配列の代入
  function addItem(item) {
    items = items.concat([item]);
  }
</script>

4. 非同期処理のエラーハンドリング

リアクティブ宣言で非同期処理を行う際は、適切なエラーハンドリングが重要です。

javascript<script>
  let userId = 1;
  let user = null;
  let loading = false;
  let error = null;

  // ✅ 適切なエラーハンドリング
  $: {
    if (userId) {
      loading = true;
      error = null;

      fetch(`/api/users/${userId}`)
        .then(response => {
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          }
          return response.json();
        })
        .then(data => {
          user = data;
          loading = false;
        })
        .catch(err => {
          error = err.message;
          loading = false;
          console.error('ユーザー取得エラー:', err);
        });
    }
  }
</script>

{#if loading}
  <p>読み込み中...</p>
{:else if error}
  <p class="error">エラーが発生しました: {error}</p>
{:else if user}
  <div>
    <h2>{user.name}</h2>
    <p>{user.email}</p>
  </div>
{/if}

5. パフォーマンスの問題

リアクティブ宣言は便利ですが、過度に使用するとパフォーマンスの問題を引き起こす可能性があります。

javascript<script>
  let data = [/* 大量のデータ */];

  // ❌ 非効率:毎回重い計算を実行
  $: processedData = data.map(item => {
    // 重い処理
    return heavyProcessing(item);
  });

  // ✅ 効率的:必要な時だけ計算
  let processedDataCache = [];
  let lastDataHash = '';

  $: {
    const currentHash = JSON.stringify(data);
    if (currentHash !== lastDataHash) {
      processedDataCache = data.map(item => heavyProcessing(item));
      lastDataHash = currentHash;
    }
  }
</script>

まとめ

Svelte のリアクティブ宣言は、状態管理の概念を根本から変える革新的な機能です。

従来のフレームワークでは、状態の変更を明示的に管理し、適切なタイミングでコンポーネントを再レンダリングする必要がありました。しかし、リアクティブ宣言を使えば、状態の依存関係を宣言的に記述するだけで、自動的に最適化された更新が行われます。

この記事で紹介した実践的な例を通じて、リアクティブ宣言の魅力を実感していただけたと思います。ショッピングカート、フォームバリデーション、データフィルタリングなど、実際のアプリケーションでよく使われる機能を、驚くほど簡潔に実装できることがおわかりいただけたでしょう。

ただし、リアクティブ宣言は強力なツールですが、適切に使わないとパフォーマンスの問題や無限ループなどの問題を引き起こす可能性があります。この記事で紹介したベストプラクティスと落とし穴を理解して、効率的で保守性の高いコードを書くことを心がけてください。

Svelte のリアクティブ宣言をマスターすれば、あなたの開発体験は劇的に向上し、より直感的で効率的なアプリケーション開発が可能になります。ぜひ、この革新的な機能を活用して、素晴らしいユーザー体験を提供するアプリケーションを作成してください。

関連リンク