Vue コンポーネントは、初期化、DOM への反映、リアクティブな値による更新、画面からの取り外しという段階を通ります。ライフサイクルフックは、その特定の段階で処理を登録するための API です。
ライフサイクルフックは「いつ処理するか」を表す手段であり、すべてのロジックを置く場所ではありません。まずは、DOM が必要なのか、外部の購読を開始・停止したいのかを考えます。
ライフサイクル全体とフック
Composition API では、setup() または <script setup> の同期的な実行中にフックを登録します。よく使うフックだけでなく、Vue 3 には次のフックがあります。
| 段階 | フック | 主な用途 |
|---|---|---|
| 作成前後(Options API) | beforeCreate / created |
Vue 2 互換の Options API における初期化 |
| DOM 反映の前後 | onBeforeMount / onMounted |
初回 DOM 反映の前後に処理する |
| 更新の前後 | onBeforeUpdate / onUpdated |
リアクティブな変更による DOM 更新の前後 |
| 破棄の前後 | onBeforeUnmount / onUnmounted |
資源の停止と後始末 |
| エラー | onErrorCaptured |
子孫コンポーネントのエラーを捕捉する |
| 描画追跡(開発時) | onRenderTracked / onRenderTriggered |
レンダー依存と再レンダー原因を診断する |
| KeepAlive の出入り | onActivated / onDeactivated |
キャッシュされたコンポーネントの再表示・非表示 |
Options API を使う Vue 3 コンポーネントでは、対応する名前から on を外します。たとえば mounted()、updated()、unmounted() のように定義します。beforeCreate と created は Options API にあるフックで、Composition API では setup() の実行が初期化の入口になります。
onMounted はコンポーネントの初回レンダーが終わり、DOM ノードが作られた後に呼ばれます。テンプレート参照を使う処理や、ブラウザ専用 API を扱う処理に向いています。
基本例: 購読の開始と停止
ウィンドウ幅を表示するコンポーネントを例にします。イベントリスナーは登録したままにせず、コンポーネントが不要になったら解除します。
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
const width = ref(0)
const updateWidth = () => { width.value = window.innerWidth }
onMounted(() => {
updateWidth()
window.addEventListener('resize', updateWidth)
})
onUnmounted(() => {
window.removeEventListener('resize', updateWidth)
})
</script>
<template>
<p>画面幅: {{ width }}px</p>
</template>
onMounted と onUnmounted は開始と終了の対です。タイマー、WebSocket、ResizeObserver、外部ライブラリのインスタンスなども、作成した場所に対応する破棄処理を置きます。
更新フックを使う前に考える
onUpdated はリアクティブな状態変更で DOM が更新された後に呼ばれます。しかし、通常のデータ変換や state の更新をここに書くと、更新 → フック → 更新の循環を作りやすくなります。
- 表示用の値を計算するなら
computed - 値の変化に応じて副作用を起こすなら
watchまたはwatchEffect - 更新済み DOM を読む必要があるときだけ
onUpdated
DOM 更新の完了を一度だけ待ちたい場合は、nextTick() も選択肢です。ライフサイクルフックを使うこと自体ではなく、必要なタイミングを最小限に選ぶことが大切です。
エラーを捕捉する: onErrorCaptured
onErrorCaptured は、子孫コンポーネントのレンダー、イベントハンドラ、ライフサイクルフックなどで発生したエラーを捕捉します。エラー監視サービスへ送信したり、代替 UI を表示したりする境界として利用できます。
import { onErrorCaptured, ref } from 'vue'
const hasError = ref(false)
onErrorCaptured((error, instance, info) => {
reportError(error, { component: instance?.$options.name, info })
hasError.value = true
return false
})
false を返すと、エラーが親方向へ伝播しなくなります。エラー状態を表示するコンポーネント自身が再び同じエラーを起こすと無限ループになるため、fallback はできるだけ単純に保ちます。アプリ全体の未捕捉エラーは app.config.errorHandler で扱う選択肢もあります。
KeepAlive と activated / deactivated
<KeepAlive> に囲まれた動的コンポーネントは、切り替え時に破棄されずキャッシュされます。このとき onUnmounted は呼ばれず、onDeactivated が呼ばれます。再び表示されると onActivated が呼ばれます。
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
タブが非表示の間は停止したいポーリングやキーボード操作があれば onDeactivated で止め、onActivated で再開します。コンポーネントが完全に不要になる資源の解放は、引き続き onUnmounted で行います。
renderTracked / renderTriggered は診断用
onRenderTracked はレンダー中にどのリアクティブな値が追跡されたか、onRenderTriggered はどの変更が再レンダーを引き起こしたかを受け取ります。これらは開発モードでのデバッグ用で、通常の業務ロジックには使いません。
onRenderTriggered((event) => {
console.debug('再レンダーの原因', event.key, event.type)
})
想定外に頻繁な再レンダーや、意図しない依存関係を調べるときに役立ちます。イベントの詳細な形にアプリケーションの動作を依存させないようにします。
登録は同期的に行う
Composition API のライフサイクルフックは、現在のコンポーネントインスタンスに結び付けられます。そのため、setTimeout や await の後から onMounted を登録することはできません。
// 適切: setup の実行中に登録する
onMounted(() => initializeMap())
// 不適切: 非同期コールバック内では現在のインスタンスが分からない
setTimeout(() => onMounted(() => initializeMap()), 100)
非同期初期化が必要でも、フック自体は先に登録し、その内部で非同期関数を呼びます。通信が完了する前に破棄される可能性がある処理は、キャンセルや結果の無視も設計します。
よくある落とし穴
onMountedをデータ取得専用の場所にして、読み込み状態やエラー処理を持たないonUpdated内で state を変更し、無限更新を起こす- リスナーやタイマーを
onUnmountedで解除しない <KeepAlive>内でもonUnmountedが呼ばれると思い込むonRenderTrackedやonRenderTriggeredを本番ロジックに使う- SSR でも実行される前提で、
windowやdocumentにセットアップ時から触れる
ライフサイクルフックは外部世界と Vue のコンポーネントを接続する境界です。DOM と資源の開始・終了を対で管理すると、画面遷移や再利用に強いコンポーネントになります。