状態管理(Runes)
Svelte 5のリアクティブな状態管理
Runesとは?
日常での例え:スマートホーム
電気をつけたい
→ スイッチまで歩いて押す
「暗くなった」という状態変化
→ 自動で電気がつく
なぜ状態管理が必要?
状態管理がないと困ること:
- 変数を変更しても画面が更新されない
- 「カートに3個入ってます」の数字が変わらない
- 手動でHTMLを書き換える必要がある(大変!)
状態管理があれば:データが変わると、画面も自動で更新される!
Svelte 5の新しい書き方「Runes」
Runes(ルーン)は $ で始まる特別な関数です:
$state- 変更を追跡する状態(カウンター、入力値など)$derived- 他の状態から自動計算される値(合計金額など)$effect- 状態が変わったら実行される処理(保存、API呼び出し)$props- 親コンポーネントから受け取る値
用語解説
状態(State):アプリの「今どうなっているか」を表すデータ(例:カートの中身、ログイン状態)
リアクティブ:データが変わると、それに関連する表示も自動で変わる仕組み
コンポーネント:画面の部品。ボタン、カード、ヘッダーなど再利用できるパーツ
props(プロップス):親から子コンポーネントに渡すデータ(例:ボタンに「送信」という文字を渡す)
$state - 状態の定義
基本的な$state
変更を追跡する変数
<script>
// $stateでリアクティブな状態を作る
let count = $state(0);
let name = $state('');
let items = $state([]);
function increment() {
count++; // 自動で画面が更新される
}
function addItem() {
items.push('新しいアイテム'); // 配列の変更も検知
}
</script>
<p>カウント: {count}</p>
<button onclick={increment}>+1</button>
<input bind:value={name} />
<p>こんにちは、{name}さん</p>カウント: 0
こんにちは、さん
コード解説:$stateの動作
① 変数の宣言
$state(0) で「0」という初期値を持つ、変更を追跡する変数を作成。普通の変数との違いは、値が変わると画面も自動で更新されること。
② 関数での更新
普通のJavaScriptと同じように count++ で値を増やせます。ただし、$stateで作った変数なので、画面の「カウント: 0」も自動で「カウント: 1」に変わります。
③ bind:value で双方向バインディング
入力欄に文字を打つと name 変数が更新され、同時に {name} で表示している場所も更新されます。
処理の流れ
1. ボタンをクリック → 2. increment()が実行 → 3. count++ でcountが1に → 4. 画面の {count} が自動で「1」に更新
オブジェクトの状態
ネストしたデータも追跡
<script>
let user = $state({
name: '田中',
profile: {
age: 25,
email: 'tanaka@example.com'
}
});
function updateAge() {
// ネストしたプロパティも変更を検知!
user.profile.age++;
}
</script>
<p>{user.name}さん({user.profile.age}歳)</p>
<button onclick={updateAge}>年齢+1</button>$derived - 計算された値
$derivedとは?
他の状態から自動的に計算される値を作ります。
元の値が変わると、derivedも自動で再計算されます。
基本的な$derived
他の状態から計算
<script>
let items = $state([
{ name: 'りんご', price: 100 },
{ name: 'みかん', price: 80 },
{ name: 'バナナ', price: 120 }
]);
// itemsが変わると自動で再計算
let total = $derived(
items.reduce((sum, item) => sum + item.price, 0)
);
let count = $derived(items.length);
function addItem() {
items.push({ name: '新商品', price: 150 });
// totalとcountは自動更新!
}
</script>
<p>商品数: {count}</p>
<p>合計: {total}円</p>商品数: 3
合計: 300円
itemsが変わると自動更新
コード解説:$derivedの動作
① 元のデータ($state)
商品データの配列。これが「元」になるデータです。
② 合計金額の自動計算
reduce():配列の各要素を足し合わせる JavaScript のメソッド$derived():itemsが変わると total も自動で再計算される- 100 + 80 + 120 = 300円
③ 商品数の自動計算
配列の長さ(3)を自動で追跡。商品が追加されると自動で更新されます。
addItem()を実行すると...
1. items に新商品(150円)が追加される → 2. total が自動で 450 に更新 → 3. count が自動で 4 に更新
手動で total = ... と書き直す必要がないのが $derived の便利なところ!
$derived.by - 複雑な計算
関数で計算ロジックを書く
<script>
let items = $state([...]);
let filter = $state('all'); // 'all' | 'expensive' | 'cheap'
// 複雑なロジックは $derived.by を使う
let filteredItems = $derived.by(() => {
if (filter === 'all') return items;
if (filter === 'expensive') {
return items.filter(item => item.price >= 100);
}
return items.filter(item => item.price < 100);
});
let message = $derived.by(() => {
const count = filteredItems.length;
if (count === 0) return '商品がありません';
return `${count}件の商品があります`;
});
</script>
<select bind:value={filter}>
<option value="all">すべて</option>
<option value="expensive">100円以上</option>
<option value="cheap">100円未満</option>
</select>
<p>{message}</p>$effect - 副作用
$effectとは?
状態が変わったときに自動で実行される処理を定義します。
ログ出力、ローカルストレージ保存、外部APIの呼び出しなどに使います。
基本的な$effect
状態変化時に実行
<script>
let searchQuery = $state('');
let results = $state([]);
// searchQueryが変わるたびに実行
$effect(() => {
console.log('検索クエリ:', searchQuery);
// 空でなければAPIを呼ぶ
if (searchQuery.length > 2) {
fetch(`/api/search?q=${searchQuery}`)
.then(r => r.json())
.then(data => {
results = data;
});
}
});
</script>
<input bind:value={searchQuery} placeholder="検索..." />クリーンアップ処理
effectの後片付け
<script>
let isConnected = $state(false);
$effect(() => {
// WebSocket接続
const ws = new WebSocket('wss://example.com');
ws.onopen = () => {
isConnected = true;
};
ws.onclose = () => {
isConnected = false;
};
// クリーンアップ関数を返す
return () => {
ws.close(); // コンポーネント破棄時に実行
};
});
</script>
<p>接続状態: {isConnected ? '接続中' : '切断'}</p>ローカルストレージとの連携
よくある使用例
<script>
// 初期値はローカルストレージから
let theme = $state(
localStorage.getItem('theme') || 'light'
);
// themeが変わるたびに保存
$effect(() => {
localStorage.setItem('theme', theme);
document.documentElement.classList.toggle('dark', theme === 'dark');
});
function toggleTheme() {
theme = theme === 'light' ? 'dark' : 'light';
}
</script>
<button onclick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'} テーマ切替
</button>$props - プロパティ
コンポーネントのprops
親から値を受け取る
<!-- Button.svelte -->
<script>
// propsを受け取る
let {
label,
variant = 'primary', // デフォルト値
disabled = false,
onclick // イベントハンドラ
} = $props();
</script>
<button
class="btn btn-{variant}"
{disabled}
{onclick}
>
{label}
</button>
<!-- 親コンポーネントから使う -->
<Button
label="送信"
variant="primary"
onclick={() => console.log('クリック')}
/>TypeScriptとの組み合わせ
型安全なprops
<script lang="ts">
interface Props {
title: string;
count: number;
items?: string[]; // オプショナル
onSave: (data: string) => void;
}
let {
title,
count,
items = [],
onSave
}: Props = $props();
</script>
<h2>{title}</h2>
<p>カウント: {count}</p>
<ul>
{#each items as item}
<li>{item}</li>
{/each}
</ul>グローバル状態
コンポーネント間で状態を共有するには?
別ファイルで状態を定義し、importして使います。
Svelte 5では.svelte.tsファイルでrunesが使えます。
グローバルストア
.svelte.tsファイルで状態管理
// src/lib/stores/counter.svelte.ts
let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count++;
}
export function decrement() {
count--;
}
// ----- 使う側 -----
// src/routes/+page.svelte
<script>
import { getCount, increment, decrement } from '$lib/stores/counter.svelte';
// リアクティブに値を取得
let count = $derived(getCount());
</script>
<p>カウント: {count}</p>
<button onclick={increment}>+</button>
<button onclick={decrement}>-</button>ユーザー状態の例
認証状態の管理
// src/lib/stores/auth.svelte.ts
interface User {
id: number;
name: string;
email: string;
}
let user = $state<User | null>(null);
export function getUser() {
return user;
}
export function isLoggedIn() {
return user !== null;
}
export function login(userData: User) {
user = userData;
}
export function logout() {
user = null;
}
// ----- 使う側 -----
<script>
import { getUser, isLoggedIn, logout } from '$lib/stores/auth.svelte';
let user = $derived(getUser());
let loggedIn = $derived(isLoggedIn());
</script>
{#if loggedIn}
<p>ようこそ、{user.name}さん</p>
<button onclick={logout}>ログアウト</button>
{:else}
<a href="/login">ログイン</a>
{/if}ようこそ、田中さん