$state: $derived vs $effect vs derived.by 完全比較ガイド
Svelte 5のリアクティビティシステムは、状態の変更を追跡し、UIを自動的に更新する仕組みです。このページでは、リアクティブな値を扱うための3つの主要な方法について、実践的な例を交えながら詳しく解説します。
React/Vue経験者向け
$derived
は React のuseMemo
や Vue のcomputed
に相当$effect
は React のuseEffect
や Vue のwatchEffect
に相当derived.by
は複雑な計算ロジックを整理するための Svelte 独自の機能
3つのリアクティビティ手法
Svelte 5では、リアクティビティを扱うための3つの主要な方法があります。
$derived
- 他の値から計算される派生値(シンプルな計算向け)$effect
- 副作用を実行するための仕組み(DOM操作、API呼び出しなど)derived.by
- より複雑な派生ロジックのための関数ベースアプローチ
それぞれには明確な役割があり、適切に使い分けることで、効率的でメンテナブルなコードを書くことができます。
基本的な違い
$derived
- シンプルな派生値
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
let message = $derived(`カウント: ${count}, 2倍: ${doubled}`);
</script>
svelte
特徴
- 式ベースの派生値
- 自動的に依存関係を追跡
- 純粋な計算に最適
- 副作用は実行できない
$effect
- 副作用の実行
<script lang="ts">
let count = $state(0);
$effect(() => {
console.log(`カウントが変更されました: ${count}`);
// クリーンアップ関数を返せる
return () => {
console.log('クリーンアップ');
};
});
</script>
svelte
特徴
- 副作用(DOM操作、API呼び出し、ログ出力など)に使用
- クリーンアップ関数をサポート
- 値を返さない
derived.by
- 関数ベースの派生値
<script lang="ts">
let items = $state<Item[]>([]);
let filter = $state('');
let sortOrder = $state<'asc' | 'desc'>('asc');
// 複雑なロジックを含む派生値
let filteredAndSorted = $derived.by(() => {
// ステップ1: フィルタリング
const filtered = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
// ステップ2: ソート
const sorted = [...filtered].sort((a, b) => {
const comparison = a.name.localeCompare(b.name);
return sortOrder === 'asc' ? comparison : -comparison;
});
// ステップ3: 結果を返す
return sorted;
});
</script>
svelte
特徴
- 関数内で複雑なロジックを記述可能
- 中間変数を使用できる
- デバッグしやすい
- 純粋な計算のみ(副作用は禁止)
実践的な比較例
ショッピングカートの実装
<script lang="ts">
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
let cartItems = $state<CartItem[]>([
{ id: 1, name: 'ノートPC', price: 120000, quantity: 1 },
{ id: 2, name: 'マウス', price: 3000, quantity: 2 }
]);
let taxRate = $state(0.1); // 10%
let discountCode = $state('');
let discountPercent = $state(0);
// 1. $derived - シンプルな計算
let subtotal = $derived(
cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
// 2. derived.by - 複雑な計算ロジック
let total = $derived.by(() => {
// 小計を計算
const baseAmount = subtotal;
// 割引を適用
const discountAmount = baseAmount * (discountPercent / 100);
const afterDiscount = baseAmount - discountAmount;
// 税金を計算
const tax = afterDiscount * taxRate;
// 最終金額
return {
subtotal: baseAmount,
discount: discountAmount,
tax: tax,
total: afterDiscount + tax
};
});
// 3. $effect - 副作用(ローカルストレージへの保存)
$effect(() => {
// カートの内容をローカルストレージに保存
localStorage.setItem('cart', JSON.stringify(cartItems));
// デバッグ用のログ
console.log('カートが更新されました:', {
items: cartItems.length,
total: total.total
});
});
// 4. 割引コードの検証($effectの実践例)
$effect(() => {
// 非同期処理も可能
const validateDiscount = async () => {
if (discountCode) {
try {
const response = await fetch(`/api/validate-discount/${discountCode}`);
const data = await response.json();
discountPercent = data.percent || 0;
} catch {
discountPercent = 0;
}
} else {
discountPercent = 0;
}
};
validateDiscount();
});
</script>
<!-- UI -->
<div class="cart">
<h2>ショッピングカート</h2>
{#each cartItems as item}
<div class="item">
<span>{item.name}</span>
<input type="number" bind:value={item.quantity} min="1" />
<span>¥{(item.price * item.quantity).toLocaleString()}</span>
</div>
{/each}
<div class="summary">
<div>小計: ¥{total.subtotal.toLocaleString()}</div>
{#if total.discount > 0}
<div>割引: -¥{total.discount.toLocaleString()}</div>
{/if}
<div>税金: ¥{total.tax.toLocaleString()}</div>
<div class="total">合計: ¥{total.total.toLocaleString()}</div>
</div>
<input
type="text"
bind:value={discountCode}
placeholder="割引コード"
/>
</div>
svelte
使い分けのガイドライン
$derived
を使うべき場合
// ✅ シンプルな計算
let fullName = $derived(`${firstName} ${lastName}`);
// ✅ 単一の式で表現できる
let isValid = $derived(email.includes('@') && password.length >= 8);
// ✅ 配列の変換
let upperCaseNames = $derived(names.map(n => n.toUpperCase()));
typescript
derived.by
を使うべき場合
// ✅ 複数ステップの処理
let processedData = $derived.by(() => {
const filtered = data.filter(/* ... */);
const sorted = filtered.sort(/* ... */);
const grouped = groupBy(sorted, 'category');
return grouped;
});
// ✅ 条件分岐が複雑
let displayValue = $derived.by(() => {
if (isLoading) return 'Loading...';
if (error) return `Error: ${error.message}`;
if (!data) return 'No data';
return formatData(data);
});
// ✅ デバッグが必要
let complexCalculation = $derived.by(() => {
console.log('Step 1:', value1);
const intermediate = calculateSomething(value1);
console.log('Step 2:', intermediate);
return finalCalculation(intermediate);
});
typescript
$effect
を使うべき場合
// ✅ DOM操作
$effect(() => {
const element = document.getElementById('chart');
if (element) {
renderChart(element, data);
}
});
// ✅ 外部ライブラリとの連携
$effect(() => {
const chart = new Chart(canvas, {
data: chartData,
options: chartOptions
});
return () => chart.destroy();
});
// ✅ API呼び出し
$effect(() => {
fetch(`/api/data/${id}`)
.then(res => res.json())
.then(data => result = data);
});
// ✅ タイマーやイベントリスナー
$effect(() => {
const timer = setInterval(() => {
time = new Date();
}, 1000);
return () => clearInterval(timer);
});
typescript
パフォーマンスの考慮
計算の最適化
<script lang="ts">
let items = $state<Item[]>([]);
let searchTerm = $state('');
// ❌ 非効率: $effectで結果を設定
let searchResults = $state<Item[]>([]);
$effect(() => {
searchResults = items.filter(item =>
item.name.includes(searchTerm)
);
});
// ✅ 効率的: $derivedを使用
let searchResults = $derived(
items.filter(item =>
item.name.includes(searchTerm)
)
);
// ✅ さらに複雑な場合はderived.by
let searchResults = $derived.by(() => {
if (!searchTerm) return items;
const term = searchTerm.toLowerCase();
return items.filter(item =>
item.name.toLowerCase().includes(term) ||
item.description?.toLowerCase().includes(term)
);
});
</script>
svelte
まとめ
機能 | $derived | derived.by | $effect |
---|---|---|---|
用途 | シンプルな派生値 | 複雑な派生値 | 副作用 |
値を返す | ✅ | ✅ | ❌ |
複数ステップ | ❌ | ✅ | ✅ |
副作用 | ❌ | ❌ | ✅ |
クリーンアップ | ❌ | ❌ | ✅ |
デバッグのしやすさ | 😐 | 😊 | 😊 |
パフォーマンス | 🚀 | 🚀 | 💨 |
選択の指針
- シンプルな計算 →
$derived
- 複雑な計算ロジック →
derived.by
- 副作用が必要 →
$effect
これらを適切に使い分けることで、読みやすく、保守しやすく、パフォーマンスの良いSvelteアプリケーションを構築できます。
関連ページ
基礎を学ぶ
- Runesシステム入門 - Runesの基本概念と全体像
- $stateルーン - リアクティブな状態管理の基礎
- $derivedルーン - 派生値の詳細解説
- $effectルーン - 副作用の詳細解説
さらに深く理解する
- リアクティブな状態変数とバインディングの違い - $stateとbind:の使い分け
- state.raw() vs $state()の違いと使い分け - 深いリアクティビティの制御
実践的な活用
- リアクティブストア(.svelte.js/.svelte.ts) - 複数コンポーネント間での状態共有
- クラスとリアクティビティ - オブジェクト指向とRunesの組み合わせ