$derived - 派生値
$derived
ルーンは、他のリアクティブな値から自動的に計算される値を作成します。依存する値が変更されると、派生値も自動的に再計算されます。
基本的な使い方
シンプルな計算
2倍: 0
2乗: 0
偶数: true
<script lang="ts">
let count = $state(0);
// countが変更されると自動的に再計算
let doubled = $derived(count * 2);
let squared = $derived(count ** 2);
let isEven = $derived(count % 2 === 0);
function increment() {
count++;
}
</script>
<button onclick={increment}>カウント: {count}</button>
<p>2倍: {doubled}</p>
<p>2乗: {squared}</p>
<p>偶数: {isEven}</p>
svelte
Click fold/expand code
Vue やReactとの比較
- Vue の
computed
と同じ概念 - React の
useMemo
に似ているが、依存関係の指定が不要 - 自動的に依存関係を追跡し、必要な時だけ再計算
複数の依存関係
フルネーム: 山田 太郎
表示名: 山田 太郎様
文字数: 5
<script lang="ts">
let firstName = $state('太郎');
let lastName = $state('山田');
let separator = $state(' ');
// 複数の値に依存する派生値
let fullName = $derived(
lastName + separator + firstName
);
// さらに派生値から派生
let displayName = $derived(
`${fullName}様`
);
let nameLength = $derived(fullName.length);
</script>
<input bind:value={firstName} placeholder="名" />
<input bind:value={lastName} placeholder="姓" />
<select bind:value={separator}>
<option value=" ">スペース</option>
<option value="・">中点</option>
<option value="">なし</option>
</select>
<p>フルネーム: {fullName}</p>
<p>表示名: {displayName}</p>
<p>文字数: {nameLength}</p>
svelte
Click fold/expand code
ブロック構文での使用
複雑な計算には、ブロック構文を使用できます。
商品数: 10点
小計: ¥239,000
割引: -¥11,950
割引後: ¥227,050
税額: ¥22,705
合計: ¥249,755
<script lang="ts">
interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
let products = $state<Product[]>([
{ id: 1, name: 'ノートPC', price: 100000, quantity: 2 },
{ id: 2, name: 'マウス', price: 3000, quantity: 5 },
{ id: 3, name: 'キーボード', price: 8000, quantity: 3 }
]);
let taxRate = $state(0.1);
let discountRate = $state(0.05);
// ブロック構文で複雑な計算
let summary = $derived(() => {
const subtotal = products.reduce((sum, product) => {
return sum + product.price * product.quantity;
}, 0);
const discount = subtotal * discountRate;
const afterDiscount = subtotal - discount;
const tax = afterDiscount * taxRate;
const total = afterDiscount + tax;
return {
subtotal,
discount,
afterDiscount,
tax,
total,
itemCount: products.reduce((sum, p) => sum + p.quantity, 0)
};
});
</script>
<div class="summary">
<p>商品数: {summary().itemCount}点</p>
<p>小計: ¥{summary().subtotal?.toLocaleString() ?? 0}</p>
<p>割引: -¥{summary().discount?.toLocaleString() ?? 0}</p>
<p>割引後: ¥{summary().afterDiscount?.toLocaleString() ?? 0}</p>
<p>税額: ¥{summary().tax?.toLocaleString() ?? 0}</p>
<p>合計: ¥{summary().total?.toLocaleString() ?? 0}</p>
</div>
svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Click fold/expand code
配列とオブジェクトの処理
フィルタリングとソート
<script lang="ts">
interface Task {
id: number;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
dueDate: Date;
}
let tasks = $state<Task[]>([
// タスクデータ
]);
let showCompleted = $state(false);
let sortBy = $state<'priority' | 'dueDate'>('priority');
let searchQuery = $state('');
// フィルタリングされたタスク
let filteredTasks = $derived(() => {
let result = tasks;
// 完了タスクのフィルタ
if (!showCompleted) {
result = result.filter(t => !t.completed);
}
// 検索フィルタ
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter(t =>
t.title.toLowerCase().includes(query)
);
}
// ソート
result = [...result].sort((a, b) => {
if (sortBy === 'priority') {
const priorityOrder = { high: 0, medium: 1, low: 2 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
} else {
return a.dueDate.getTime() - b.dueDate.getTime();
}
});
return result;
});
// 統計情報
let stats = $derived(() => {
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
const pending = total - completed;
const highPriority = tasks.filter(
t => t.priority === 'high' && !t.completed
).length;
return { total, completed, pending, highPriority };
});
</script>
svelte
グルーピング
<script lang="ts">
interface Item {
category: string;
name: string;
value: number;
}
let items = $state<Item[]>([
{ category: '食品', name: 'りんご', value: 100 },
{ category: '食品', name: 'バナナ', value: 80 },
{ category: '家電', name: 'テレビ', value: 50000 },
{ category: '家電', name: '冷蔵庫', value: 80000 },
{ category: '衣類', name: 'シャツ', value: 3000 }
]);
// カテゴリごとにグループ化
let groupedItems = $derived(() => {
const groups = new Map<string, Item[]>();
for (const item of items) {
if (!groups.has(item.category)) {
groups.set(item.category, []);
}
groups.get(item.category)!.push(item);
}
return groups;
});
// カテゴリごとの合計
let categoryTotals = $derived(() => {
const totals = new Map<string, number>();
for (const [category, categoryItems] of groupedItems) {
const total = categoryItems.reduce(
(sum, item) => sum + item.value, 0
);
totals.set(category, total);
}
return totals;
});
</script>
svelte
$derived.by - 明示的な派生値
関数を使用して派生値を作成する別の方法です。
<script lang="ts">
let searchTerm = $state('');
let data = $state<string[]>([]);
// $derived.byを使用した明示的な派生値
let searchResults = $derived.by(() => {
if (!searchTerm) return data;
const term = searchTerm.toLowerCase();
return data.filter(item =>
item.toLowerCase().includes(term)
);
});
// 通常の$derivedと同じ(好みの問題)
let searchResults2 = $derived(() => {
// 同じロジック
});
</script>
svelte
非同期処理との組み合わせ
非同期派生値の注意
$derived
は同期的に値を返す必要があります。非同期処理には$effect
を組み合わせて使用します。
<script lang="ts">
let userId = $state(1);
let userData = $state<User | null>(null);
let loading = $state(false);
let error = $state<Error | null>(null);
// URLは同期的に派生
let apiUrl = $derived(
`/api/users/${userId}`
);
// 非同期処理は$effectで実行
$effect(async () => {
loading = true;
error = null;
try {
const response = await fetch(apiUrl);
if (!response.ok) throw new Error('Failed to fetch');
userData = await response.json();
} catch (e) {
error = e as Error;
} finally {
loading = false;
}
});
</script>
svelte
実践例:シンプルなフィルタリング
フィルタ条件
〜
該当商品: 6件 平均価格: ¥62,500 平均評価: ★4.2 在庫あり: 5件
ノートPC Pro
パソコン
¥150,000
在庫あり
ワイヤレスマウス
周辺機器
¥3,000
在庫あり
機械式キーボード
周辺機器
¥12,000
在庫なし
ウェブカメラ HD
周辺機器
¥8,000
在庫あり
デスクトップPC
パソコン
¥200,000
在庫あり
USBハブ
周辺機器
¥2,000
在庫あり
<script lang="ts">
interface Product {
id: number;
name: string;
category: string;
price: number;
inStock: boolean;
rating: number;
}
let products = $state<Product[]>([
{ id: 1, name: 'ノートPC Pro', category: 'パソコン', price: 150000, inStock: true, rating: 4.5 },
{ id: 2, name: 'ワイヤレスマウス', category: '周辺機器', price: 3000, inStock: true, rating: 4.0 },
{ id: 3, name: '機械式キーボード', category: '周辺機器', price: 12000, inStock: false, rating: 4.8 },
{ id: 4, name: 'ウェブカメラ HD', category: '周辺機器', price: 8000, inStock: true, rating: 3.5 },
{ id: 5, name: 'デスクトップPC', category: 'パソコン', price: 200000, inStock: true, rating: 4.7 },
{ id: 6, name: 'USBハブ', category: '周辺機器', price: 2000, inStock: true, rating: 3.8 }
]);
// フィルタ条件
let searchQuery = $state('');
let selectedCategory = $state('all');
let minPrice = $state(0);
let maxPrice = $state(300000);
let onlyInStock = $state(false);
let minRating = $state(0);
// カテゴリ一覧を動的に生成
let categories = $derived.by(() => {
const cats = new Set(products.map(p => p.category));
return ['all', ...Array.from(cats)];
});
// フィルタリングされた商品
let filteredProducts = $derived.by(() => {
return products.filter(product => {
// 検索クエリ
if (searchQuery && !product.name.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
// カテゴリ
if (selectedCategory !== 'all' && product.category !== selectedCategory) {
return false;
}
// 価格範囲
if (product.price < minPrice || product.price > maxPrice) {
return false;
}
// 在庫
if (onlyInStock && !product.inStock) {
return false;
}
// 評価
if (product.rating < minRating) {
return false;
}
return true;
});
});
// 統計情報
let stats = $derived.by(() => {
const total = filteredProducts.length;
const avgPrice = total > 0
? filteredProducts.reduce((sum, p) => sum + p.price, 0) / total
: 0;
const avgRating = total > 0
? filteredProducts.reduce((sum, p) => sum + p.rating, 0) / total
: 0;
const inStockCount = filteredProducts.filter(p => p.inStock).length;
return {
total,
avgPrice: Math.round(avgPrice),
avgRating: avgRating.toFixed(1),
inStockCount
};
});
</script>
<div class="search-filter-demo">
<div class="filters">
<h3>フィルタ条件</h3>
<div class="filter-group">
<label for="search">検索:</label>
<input
id="search"
type="text"
bind:value={searchQuery}
placeholder="商品名で検索..."
/>
</div>
<div class="filter-group">
<label for="category">カテゴリ:</label>
<select id="category" bind:value={selectedCategory}>
{#each categories as category}
<option value={category}>
{category === 'all' ? '全て' : category}
</option>
{/each}
</select>
</div>
<div class="filter-group">
<label for="min-price">価格範囲:</label>
<div class="range-inputs">
<input
id="min-price"
type="number"
bind:value={minPrice}
min="0"
max={maxPrice}
/>
<span>〜</span>
<input
id="max-price"
type="number"
bind:value={maxPrice}
min={minPrice}
/>
</div>
</div>
<div class="filter-group">
<label>
<input type="checkbox" bind:checked={onlyInStock} />
在庫ありのみ
</label>
</div>
<div class="filter-group">
<label for="min-rating">最低評価: {minRating}</label>
<input
id="min-rating"
type="range"
bind:value={minRating}
min="0"
max="5"
step="0.5"
/>
</div>
</div>
<div class="results">
<div class="stats">
<span>該当商品: {stats.total}件</span>
<span>平均価格: ¥{stats.avgPrice.toLocaleString()}</span>
<span>平均評価: ★{stats.avgRating}</span>
<span>在庫あり: {stats.inStockCount}件</span>
</div>
<div class="product-list">
{#if filteredProducts.length === 0}
<p class="no-results">該当する商品がありません</p>
{:else}
{#each filteredProducts as product}
<div class="product-card">
<h4>{product.name}</h4>
<p class="category">{product.category}</p>
<p class="price">¥{product.price.toLocaleString()}</p>
<p class="rating">★ {product.rating}</p>
<p class="stock" class:out-of-stock={!product.inStock}>
{product.inStock ? '在庫あり' : '在庫なし'}
</p>
</div>
{/each}
{/if}
</div>
</div>
</div>
<style>
.search-filter-demo {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
padding: 1rem;
}
.filters {
background: #f5f5f5;
padding: 1rem;
border-radius: 8px;
}
.filter-group {
margin-bottom: 1rem;
width: 100%;
}
.filter-group label {
display: block;
margin-bottom: 0.25rem;
font-weight: bold;
}
.filter-group input[type="text"],
.filter-group input[type="number"],
.filter-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.range-inputs {
display: flex;
align-items: center;
gap: 0.5rem;
}
.range-inputs input {
width: 80px;
}
.stats {
display: flex;
gap: 1rem;
padding: 1rem;
background: #e8f4ff;
border-radius: 4px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.product-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.product-card {
padding: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
}
.product-card h4 {
margin: 0 0 0.5rem 0;
color: #333;
}
.product-card p {
margin: 0.25rem 0;
font-size: 0.9rem;
}
.category {
color: #666;
}
.price {
font-weight: bold;
color: #ff3e00;
}
.stock {
color: green;
}
.stock.out-of-stock {
color: #999;
}
.no-results {
grid-column: 1 / -1;
text-align: center;
padding: 2rem;
color: #666;
}
</style>
svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
Click fold/expand code
パフォーマンス最適化
メモ化
$derived
は自動的にメモ化されます。同じ依存関係の値では再計算されません。
<script lang="ts">
let numbers = $state([1, 2, 3, 4, 5]);
let multiplier = $state(2);
// この計算は依存関係が変わらない限り実行されない
let expensiveCalculation = $derived(() => {
console.log('計算実行'); // 依存関係が変わった時のみ出力
return numbers.reduce((sum, n) => {
// 重い計算をシミュレート
for (let i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
return sum + n * multiplier;
}, 0);
});
</script>
svelte
細分化
大きな派生値を小さな部分に分割することで、パフォーマンスを向上できます。
<script lang="ts">
// ❌ 悪い例:すべてを1つの派生値で計算
let everything = $derived(() => {
const filtered = items.filter(/* ... */);
const sorted = filtered.sort(/* ... */);
const grouped = groupBy(sorted, /* ... */);
const stats = calculateStats(grouped);
return { filtered, sorted, grouped, stats };
});
// ✅ 良い例:段階的に派生値を作成
let filtered = $derived(() => items.filter(/* ... */));
let sorted = $derived(() => [...filtered].sort(/* ... */));
let grouped = $derived(() => groupBy(sorted, /* ... */));
let stats = $derived(() => calculateStats(grouped));
</script>
svelte
まとめ
$derived
ルーンは:
- 自動追跡 - 依存関係を自動的に検出
- メモ化 - 不要な再計算を避ける
- 型安全 - TypeScriptの型推論が機能
- 宣言的 - 計算ロジックを明確に表現
他のフレームワークとの比較
- Vue:
computed
とほぼ同じ - React:
useMemo
と似ているが、依存配列が不要 - Angular: Computed signalsと類似
- MobX:
computed
と同じ概念
次のステップ
$effect - 副作用 では、リアクティブな値の変更に応じて副作用を実行する方法を学びます。