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 のリアクティブ宣言をマスターすれば、あなたの開発体験は劇的に向上し、より直感的で効率的なアプリケーション開発が可能になります。ぜひ、この革新的な機能を活用して、素晴らしいユーザー体験を提供するアプリケーションを作成してください。
関連リンク
- article
Jotai を 120%活用するユーティリティライブラリ(jotai-immer, jotai-xstate, jotai-form)の世界
- article
TypeScript × Vitest:次世代テストランナーの導入から活用まで
- article
スマホでも滑らか!React × アニメーションのレスポンシブ対応術
- article
Zustand でリストデータと詳細データを効率よく管理する方法
- article
Nuxt で API 連携:fetch, useAsyncData, useFetch の違いと使い分け - 記事構成案
- article
htmx の history 拡張で快適な SPA ライク体験を実現
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来