ライフサイクルフック

Vue 3 コンポーネントの生成・更新・破棄に合わせて処理を実行する方法を学ぶ。

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 と資源の開始・終了を対で管理すると、画面遷移や再利用に強いコンポーネントになります。

Sources