Svelte のライフサイクルフック完全解説

Svelteのライフサイクルフックは、コンポーネントの生成から破棄までの各段階で実行される特別な関数です。これらを理解することで、より効率的で安定したWebアプリケーションを構築できるようになります。
本記事では、Svelteの5つの主要なライフサイクルフック(onMount
、beforeUpdate
、afterUpdate
、onDestroy
、tick
)について、実際のコード例とよくあるエラーを交えながら詳しく解説していきます。
背景
Svelteコンポーネントの動作理解の重要性
Svelteアプリケーションを開発していると、「なぜこのタイミングでAPIを呼び出すのか」「いつDOMが更新されるのか」といった疑問が生まれますよね。これらの疑問を解決するカギが、ライフサイクルフックの理解にあります。
Svelteコンポーネントは、まるで生き物のように誕生し、成長し、そして役目を終えて消えていきます。この一連の流れを適切に管理することで、ユーザーにとって快適なWebアプリケーションを提供できるのです。
ライフサイクルフックとは何か
ライフサイクルフックは、コンポーネントの特定の瞬間に実行される関数です。ReactのuseEffect
やVue.jsのmounted
に似た概念ですが、Svelteではより直感的でシンプルな書き方ができるのが特徴です。
これらのフックを使うことで、以下のようなことが可能になります:
- コンポーネントがブラウザに表示されたタイミングでデータを取得
- 状態が変更される前後での処理実行
- コンポーネントが削除される前のクリーンアップ処理
課題
ライフサイクルフックの実行タイミングがわからない
多くの開発者が最初につまずくのが、「どのタイミングで何が実行されるのか」という問題です。
typescript// よくある間違い:onMountを使わずにDOM操作を試みる
let element;
// これは動作しません!elementはまだundefined
console.log(element); // undefined
element.focus(); // TypeError: Cannot read property 'focus' of undefined
この問題は、コンポーネントがマウントされる前にDOM要素にアクセスしようとしたために発生します。
各フックの使い分けができない
「データ取得はonMount
で」「DOM操作はafterUpdate
で」といった基本的な使い分けがわからないことも多い課題です。間違った場面で使用すると、以下のようなエラーが発生します:
javascriptError: Function called outside component initialization
at onMount (svelte/internal:...)
パフォーマンスに影響する使い方をしている
ライフサイクルフックの不適切な使用は、アプリケーションのパフォーマンスを大幅に低下させる可能性があります。
typescript// 危険な例:afterUpdateで重い処理を実行
afterUpdate(() => {
// この処理は状態が変更されるたびに実行される
expensiveCalculation(); // 重い計算処理
callApiEveryUpdate(); // 不要なAPI呼び出し
});
解決策
各ライフサイクルフックの詳細解説
Svelteのライフサイクルフックは、コンポーネントの生涯において明確な役割を持っています。適切に使い分けることで、効率的で保守性の高いアプリケーションを構築できます。
実行順序と適切な使用場面
ライフサイクルフックは以下の順序で実行されます:
# | フック名 | 実行タイミング | 主な用途 |
---|---|---|---|
1 | onMount | コンポーネントがDOMにマウントされた直後 | データ取得、DOM要素の初期化 |
2 | beforeUpdate | 状態変更によるDOM更新の直前 | DOM更新前の値の保存 |
3 | afterUpdate | DOM更新の直後 | DOM操作、更新後の処理 |
4 | onDestroy | コンポーネントがDOMから削除される直前 | イベントリスナーの削除、タイマーのクリア |
5 | tick | 次のマイクロタスクのタイミング | 状態更新後のDOM反映待ち |
具体例
onMount
onMount
は、コンポーネントがブラウザのDOMに初めて描画された直後に実行される最も重要なライフサイクルフックです。
基本的なデータ取得パターン
APIからデータを取得する際の定番パターンをご紹介します:
typescript<script>
import { onMount } from 'svelte';
let users = [];
let loading = true;
let error = null;
onMount(async () => {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
users = await response.json();
} catch (err) {
error = err.message;
console.error('Failed to fetch users:', err);
} finally {
loading = false;
}
});
</script>
{#if loading}
<p>読み込み中...</p>
{:else if error}
<p class="error">エラーが発生しました: {error}</p>
{:else}
<ul>
{#each users as user}
<li>{user.name}</li>
{/each}
</ul>
{/if}
このコードでは、onMount
内でエラーハンドリングを含む非同期処理を実装しています。finally
ブロックを使うことで、成功・失敗に関わらずローディング状態を適切に終了させています。
DOM要素への直接アクセス
onMount
では、DOM要素が確実に存在するため、安全に操作を行えます:
typescript<script>
import { onMount } from 'svelte';
let inputElement;
let canvasElement;
onMount(() => {
// 入力フィールドにフォーカスを設定
inputElement.focus();
// Canvas要素のコンテキストを取得
const ctx = canvasElement.getContext('2d');
ctx.fillStyle = '#ff0000';
ctx.fillRect(10, 10, 100, 100);
// 戻り値として関数を返すことで、onDestroyと同じ効果
return () => {
console.log('コンポーネントがアンマウントされます');
};
});
</script>
<input bind:this={inputElement} placeholder="自動的にフォーカスされます" />
<canvas bind:this={canvasElement} width="200" height="200"></canvas>
onMount
から関数を返すことで、コンポーネントが破棄される際のクリーンアップ処理を定義できる点は、多くの開発者が見落としがちな便利な機能です。
beforeUpdate
beforeUpdate
は、リアクティブな状態の変更によってDOMが更新される直前に実行されます。
スクロール位置の保存
リストの更新時にスクロール位置を維持したい場合の実装例です:
typescript<script>
import { beforeUpdate, afterUpdate } from 'svelte';
let items = ['アイテム1', 'アイテム2', 'アイテム3'];
let listElement;
let shouldMaintainScroll = false;
let scrollTop = 0;
beforeUpdate(() => {
// 現在のスクロール位置を保存
if (listElement) {
scrollTop = listElement.scrollTop;
shouldMaintainScroll = true;
}
});
afterUpdate(() => {
// スクロール位置を復元
if (shouldMaintainScroll && listElement) {
listElement.scrollTop = scrollTop;
shouldMaintainScroll = false;
}
});
function addItem() {
items = [...items, `アイテム${items.length + 1}`];
}
</script>
<button on:click={addItem}>アイテムを追加</button>
<div bind:this={listElement} class="list-container">
{#each items as item}
<div class="item">{item}</div>
{/each}
</div>
<style>
.list-container {
height: 200px;
overflow-y: scroll;
border: 1px solid #ccc;
}
.item {
padding: 10px;
border-bottom: 1px solid #eee;
height: 50px;
}
</style>
この例では、beforeUpdate
でスクロール位置を記録し、afterUpdate
で復元することで、リストが更新されてもユーザーの閲覧位置を維持しています。
afterUpdate
afterUpdate
は、DOM更新が完了した直後に実行され、更新されたDOM要素に対する操作を安全に行えます。
動的なサイズ調整
コンテンツの変更に合わせてレイアウトを調整する例をご紹介します:
typescript<script>
import { afterUpdate } from 'svelte';
let content = '短いテキスト';
let containerElement;
let contentHeight = 0;
afterUpdate(() => {
if (containerElement) {
// DOM更新後の実際の高さを取得
contentHeight = containerElement.scrollHeight;
// 高さに応じてスタイルを動的に調整
if (contentHeight > 200) {
containerElement.style.overflow = 'scroll';
containerElement.style.maxHeight = '200px';
} else {
containerElement.style.overflow = 'visible';
containerElement.style.maxHeight = 'none';
}
}
});
function toggleContent() {
content = content === '短いテキスト'
? '長いテキストです。'.repeat(50)
: '短いテキスト';
}
</script>
<button on:click={toggleContent}>テキストを切り替え</button>
<div bind:this={containerElement} class="content-container">
<p>{content}</p>
<p>現在の高さ: {contentHeight}px</p>
</div>
<style>
.content-container {
border: 1px solid #ccc;
padding: 10px;
margin-top: 10px;
transition: all 0.3s ease;
}
</style>
この実装では、コンテンツの変更後にafterUpdate
で実際のDOM要素のサイズを測定し、それに基づいてスタイルを動的に調整しています。
よくあるエラーとその対処法
afterUpdate
使用時によく発生するエラーです:
typescript// 問題のあるコード:無限ループの発生
afterUpdate(() => {
// この中で状態を変更すると無限ループが発生
if (someCondition) {
counter++; // これは危険!
}
});
このエラーを回避するには、条件を適切に設定する必要があります:
typescript// 修正されたコード:条件を適切に設定
let previousValue = null;
afterUpdate(() => {
if (someValue !== previousValue) {
// 値が実際に変更された場合のみ処理を実行
performAction();
previousValue = someValue;
}
});
onDestroy
onDestroy
は、コンポーネントがDOMから削除される直前に実行され、メモリリークを防ぐために重要な役割を果たします。
イベントリスナーのクリーンアップ
適切なクリーンアップを行わないと、以下のようなエラーが発生する可能性があります:
vbnetWarning: Can't perform a React state update on an unmounted component
Memory leak detected: EventListener not removed
これを防ぐための実装例です:
typescript<script>
import { onMount, onDestroy } from 'svelte';
let windowWidth = 0;
let scrollY = 0;
function handleResize() {
windowWidth = window.innerWidth;
}
function handleScroll() {
scrollY = window.scrollY;
}
onMount(() => {
// イベントリスナーを追加
window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll);
// 初期値を設定
windowWidth = window.innerWidth;
scrollY = window.scrollY;
});
onDestroy(() => {
// イベントリスナーを確実に削除
window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll);
console.log('イベントリスナーをクリーンアップしました');
});
</script>
<div class="info">
<p>ウィンドウ幅: {windowWidth}px</p>
<p>スクロール位置: {scrollY}px</p>
</div>
タイマーとインターバルのクリーンアップ
定期的な処理を行う場合のクリーンアップ実装です:
typescript<script>
import { onMount, onDestroy } from 'svelte';
let currentTime = new Date();
let intervalId;
onMount(() => {
// 1秒ごとに時刻を更新
intervalId = setInterval(() => {
currentTime = new Date();
}, 1000);
});
onDestroy(() => {
// インターバルをクリア
if (intervalId) {
clearInterval(intervalId);
console.log('タイマーをクリアしました');
}
});
</script>
<div class="clock">
<h2>現在時刻</h2>
<p>{currentTime.toLocaleTimeString()}</p>
</div>
クリーンアップを忘れると、コンポーネントが破棄された後もタイマーが動き続け、メモリリークの原因となります。
tick関数
tick
は他のライフサイクルフックとは異なり、状態更新後のDOM反映を待つための特別な関数です。
状態更新後のDOM操作
状態を更新した直後にDOM要素にアクセスする場合の実装例です:
typescript<script>
import { tick } from 'svelte';
let items = ['項目1', '項目2', '項目3'];
let newItemInput = '';
let listElement;
async function addItem() {
if (newItemInput.trim()) {
// 新しい項目を追加
items = [...items, newItemInput.trim()];
newItemInput = '';
// DOM更新を待つ
await tick();
// 新しく追加された要素までスクロール
const lastItem = listElement.lastElementChild;
if (lastItem) {
lastItem.scrollIntoView({
behavior: 'smooth',
block: 'end'
});
}
}
}
</script>
<div class="add-item">
<input
bind:value={newItemInput}
placeholder="新しい項目を入力"
on:keydown={(e) => e.key === 'Enter' && addItem()}
/>
<button on:click={addItem}>追加</button>
</div>
<ul bind:this={listElement} class="item-list">
{#each items as item, index}
<li class="item">{index + 1}. {item}</li>
{/each}
</ul>
<style>
.item-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
}
.item {
padding: 8px;
margin: 4px 0;
background: #f5f5f5;
border-radius: 4px;
}
</style>
tick()
を使用することで、状態更新後にDOMが確実に更新されてから、スクロール処理を実行できます。
フォーカス制御での活用
動的に生成される要素にフォーカスを設定する例です:
typescript<script>
import { tick } from 'svelte';
let isEditing = false;
let editInputElement;
let itemText = 'クリックして編集';
async function startEditing() {
isEditing = true;
// DOM更新を待ってからフォーカス
await tick();
if (editInputElement) {
editInputElement.focus();
editInputElement.select(); // テキストを全選択
}
}
function finishEditing() {
isEditing = false;
}
</script>
<div class="editable-item">
{#if isEditing}
<input
bind:this={editInputElement}
bind:value={itemText}
on:blur={finishEditing}
on:keydown={(e) => e.key === 'Enter' && finishEditing()}
class="edit-input"
/>
{:else}
<span on:click={startEditing} class="display-text">
{itemText}
</span>
{/if}
</div>
<style>
.editable-item {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.display-text {
display: block;
width: 100%;
}
.edit-input {
width: 100%;
border: none;
outline: none;
font-size: inherit;
}
</style>
ライフサイクルフックの組み合わせパターン
実際のアプリケーションでは、複数のライフサイクルフックを組み合わせて使用することが一般的です。
リアルタイムデータ表示コンポーネント
WebSocketを使用したリアルタイムデータ表示の完全な実装例です:
typescript<script>
import { onMount, onDestroy, beforeUpdate, afterUpdate, tick } from 'svelte';
let messages = [];
let websocket;
let isConnected = false;
let messagesContainer;
let shouldScrollToBottom = true;
onMount(async () => {
try {
// WebSocket接続を確立
websocket = new WebSocket('wss://api.example.com/messages');
websocket.onopen = () => {
isConnected = true;
console.log('WebSocket接続が確立されました');
};
websocket.onmessage = async (event) => {
const newMessage = JSON.parse(event.data);
messages = [...messages, newMessage];
// 新しいメッセージが追加された場合、自動スクロール
await tick();
if (shouldScrollToBottom) {
scrollToBottom();
}
};
websocket.onerror = (error) => {
console.error('WebSocketエラー:', error);
};
websocket.onclose = () => {
isConnected = false;
console.log('WebSocket接続が閉じられました');
};
} catch (error) {
console.error('WebSocket接続に失敗しました:', error);
}
});
beforeUpdate(() => {
// スクロール位置をチェック
if (messagesContainer) {
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
shouldScrollToBottom = scrollTop + clientHeight >= scrollHeight - 5;
}
});
afterUpdate(() => {
// 必要に応じて自動スクロール
if (shouldScrollToBottom) {
scrollToBottom();
}
});
onDestroy(() => {
// WebSocket接続をクリーンアップ
if (websocket) {
websocket.close();
console.log('WebSocketを正常に閉じました');
}
});
function scrollToBottom() {
if (messagesContainer) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
function sendMessage() {
if (websocket && isConnected) {
const message = {
text: 'Hello from client',
timestamp: new Date().toISOString()
};
websocket.send(JSON.stringify(message));
}
}
</script>
<div class="chat-container">
<div class="connection-status">
状態: {isConnected ? '接続中' : '切断中'}
</div>
<div bind:this={messagesContainer} class="messages-container">
{#each messages as message}
<div class="message">
<span class="timestamp">{new Date(message.timestamp).toLocaleTimeString()}</span>
<span class="text">{message.text}</span>
</div>
{/each}
</div>
<button on:click={sendMessage} disabled={!isConnected}>
メッセージを送信
</button>
</div>
<style>
.chat-container {
width: 100%;
max-width: 500px;
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
}
.connection-status {
padding: 10px;
background: #f0f0f0;
font-weight: bold;
}
.messages-container {
height: 300px;
overflow-y: auto;
padding: 10px;
}
.message {
margin-bottom: 10px;
padding: 8px;
background: #f9f9f9;
border-radius: 4px;
}
.timestamp {
font-size: 0.8em;
color: #666;
margin-right: 10px;
}
</style>
この実装では、5つのライフサイクルフックすべてを効果的に活用しています:
onMount
: WebSocket接続の確立beforeUpdate
: スクロール位置の状態確認afterUpdate
: 自動スクロールの実行onDestroy
: WebSocket接続のクリーンアップtick
: メッセージ追加後のDOM更新待ち
まとめ
Svelteのライフサイクルフックは、コンポーネントの各段階で適切な処理を実行するための強力なツールです。本記事で学んだ内容を整理しましょう。
各フックの主な役割:
onMount
: 初期化処理とデータ取得beforeUpdate
: DOM更新前の状態保存afterUpdate
: DOM更新後の操作onDestroy
: リソースのクリーンアップtick
: 状態更新後のDOM反映待ち
実践で意識すべきポイント:
- エラーハンドリングを適切に実装する
- メモリリークを防ぐためのクリーンアップを忘れない
- 無限ループを避けるための条件設定を行う
- パフォーマンスを考慮した実装を心がける
これらのライフサイクルフックを適切に使い分けることで、ユーザーにとって快適で、開発者にとって保守しやすいWebアプリケーションを構築できるようになります。
ライフサイクルフックは最初は複雑に感じるかもしれませんが、一度理解すれば、Svelteの真の力を発揮できる重要な機能です。ぜひ実際のプロジェクトで活用してみてください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来