Vue 3のライフサイクルを確認する ── トレジャーボックスでマウント・更新・アンマウント
トレジャーボックスを画面に置き、アイテムを取り出し、最後に片づける。そんな小さな操作を使って、Vue 3のライフサイクルフックを確認してみます。
今回は、操作のたびに画面上の表示色とメッセージが変わるようにして、onMounted、onUpdated、onUnmountedのどれが発動したのかを追いやすくしました。
まだ途中段階なので、勘違いなどあれば教えていただけるとうれしいです。
今日やったこと
1つの操作につき1つのフックを確認できる、小さな画面を作りました。
Vueでは、コンポーネントが画面に表示されるとき、状態変更でDOMが更新されるとき、画面から取り除かれるときに、ライフサイクルフックへ処理を書けます。今回はonMounted、onUpdated、onUnmountedに絞っています。Vue公式のライフサイクルフックも見ながら整理しました。 ja.vuejs
- ① トレジャーボックスを置く:
onMounted - ② アイテムを取り出す:
onUpdated - ③ トレジャーボックスを片づける:
onUnmounted - 最初から試す: 画面と表示内容を初期状態へ戻す
親コンポーネントでは、現在発動したフックを表示します。表示色は、未実行ならグレー、マウント後は青、更新後は緑、アンマウント後はオレンジに変わるようにしました。
<!-- App.vue -->
<script setup>
import { nextTick, reactive, ref } from 'vue'
import TreasureBoxCard from './components/TreasureBoxCard.vue'
// 最初はトレジャーボックスカードを画面へ置かない
const isTreasureBoxVisible = ref(false)
// 現在発動したフックを、画面上の案内エリアへ表示する
const lifecycleStatus = reactive({
hook: '未実行',
message: '①「トレジャーボックスを置く」から始める',
kind: 'idle'
})
// 操作の順番を管理する
const currentStep = ref(1)
// リセット操作でアンマウントしたとき、結果表示を上書きしないための値
const isResetting = ref(false)
// フック名・説明・表示色をまとめて更新する
const showStatus = (hook, message, kind) => {
lifecycleStatus.hook = hook
lifecycleStatus.message = message
lifecycleStatus.kind = kind
}
// ① 子コンポーネントを画面に表示する
const mountTreasureBox = () => {
isTreasureBoxVisible.value = true
}
// ③ 子コンポーネントを画面から外す
const unmountTreasureBox = () => {
isTreasureBoxVisible.value = false
}
// 子コンポーネントの onMounted から呼ばれる
const handleMounted = () => {
currentStep.value = 2
showStatus(
'onMounted',
'カードが画面に表示された後、入力欄へフォーカスを当てた',
'mounted'
)
}
// 子コンポーネントの onUpdated から呼ばれる
const handleUpdated = (itemName) => {
currentStep.value = 3
showStatus(
'onUpdated',
`「${itemName}」の追加によるDOM更新が完了した`,
'updated'
)
}
// 子コンポーネントの onUnmounted から呼ばれる
const handleUnmounted = () => {
// リセットのために外した場合は、通常のアンマウントとして表示しない
if (isResetting.value) {
return
}
currentStep.value = 4
showStatus(
'onUnmounted',
'カードが画面から取り除かれた後に実行された',
'unmounted'
)
}
// 初期状態へ戻し、もう一度順番に確認できるようにする
const resetDemo = async () => {
isResetting.value = true
isTreasureBoxVisible.value = false
// 子コンポーネントを外すDOM更新が終わるまで待つ
await nextTick()
currentStep.value = 1
showStatus(
'未実行',
'①「トレジャーボックスを置く」から始める',
'idle'
)
isResetting.value = false
}
</script>
<template>
<main class="app">
<h1>Vue ライフサイクル確認</h1>
<!-- フックに応じて、背景色と文字色が変化する -->
<section
class="status"
:class="`status-${lifecycleStatus.kind}`"
>
<p>現在の手順: {{ currentStep }}</p>
<p class="hook-name">発動したフック: {{ lifecycleStatus.hook }}</p>
<p>{{ lifecycleStatus.message }}</p>
</section>
<section class="controls">
<!-- ① カードがないときだけ押せる -->
<button
type="button"
:disabled="isTreasureBoxVisible || currentStep !== 1"
@click="mountTreasureBox"
>
① トレジャーボックスを置く
</button>
<!-- ② はカード内のボタンで操作する -->
<p v-if="currentStep === 2">
次は、カード内の「アイテムを取り出す」を押す
</p>
<!-- ③ アイテム追加後に押せる -->
<button
type="button"
:disabled="!isTreasureBoxVisible || currentStep !== 3"
@click="unmountTreasureBox"
>
③ トレジャーボックスを片づける
</button>
<button type="button" class="reset-button" @click="resetDemo">
最初から試す
</button>
</section>
<!-- true のときだけ、子コンポーネントがマウントされる -->
<TreasureBoxCard
v-if="isTreasureBoxVisible"
:on-mounted="handleMounted"
:on-updated="handleUpdated"
:on-unmounted="handleUnmounted"
/>
</main>
</template>
<style scoped>
/* 全体は装飾を抑え、白背景と濃い文字を基本にする */
.app {
width: min(100%, 680px);
margin: 24px auto;
padding: 24px;
color: #1f2937;
background: #ffffff;
border: 1px solid #374151;
border-radius: 8px;
}
/* 発動したフックを表示する共通スタイル */
.status {
margin: 16px 0;
padding: 16px;
border: 1px solid;
border-radius: 6px;
}
/* 何も実行していない初期状態 */
.status-idle {
color: #374151;
background: #f3f4f6;
border-color: #6b7280;
}
/* onMounted の表示色 */
.status-mounted {
color: #1e3a8a;
background: #dbeafe;
border-color: #2563eb;
}
/* onUpdated の表示色 */
.status-updated {
color: #14532d;
background: #dcfce7;
border-color: #16a34a;
}
/* onUnmounted の表示色 */
.status-unmounted {
color: #78350f;
background: #fef3c7;
border-color: #d97706;
}
/* フック名をメッセージより目立たせる */
.hook-name {
margin: 4px 0;
font-size: 1.2rem;
font-weight: bold;
}
/* 操作ボタンを並べる */
.controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
/* 通常の操作ボタン */
button {
padding: 8px 12px;
color: #ffffff;
font: inherit;
background: #1d4ed8;
border: 1px solid #1e3a8a;
border-radius: 4px;
cursor: pointer;
}
/* 押せないボタンは色とカーソルで区別する */
button:disabled {
color: #374151;
background: #e5e7eb;
border-color: #9ca3af;
cursor: not-allowed;
}
/* リセット操作は通常操作と分ける */
.reset-button {
background: #4b5563;
border-color: #374151;
}
/* キーボード操作中の位置を見つけやすくする */
button:focus-visible,
input:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 2px;
}
</style>
このコードでは、画面上のstatus-mounted、status-updated、status-unmountedというクラスが切り替わります。色の変化とフック名をセットで見ることで、「今はどの段階なのか」を把握しやすくしたいです。
![]() |
「色だけで判断するのではなく、`onMounted`の名前も一緒に出すと分かりやすいね。青は“表示された後”、緑は“更新された後”という目印にできそう。」 |
子コンポーネントでは、マウント後に入力欄へフォーカスし、アイテムを追加したときだけ更新結果を親へ伝えます。
<!-- components/TreasureBoxCard.vue -->
<script setup>
import {
ref,
onMounted,
onUpdated,
onUnmounted,
useTemplateRef
} from 'vue'
// 親コンポーネントへ、フック発動を伝える関数
const props = defineProps({
onMounted: {
type: Function,
required: true
},
onUpdated: {
type: Function,
required: true
},
onUnmounted: {
type: Function,
required: true
}
})
// 画面に表示するリアクティブな値
const playerName = ref('')
const acquiredItem = ref('')
// テンプレート側の ref="player-name-input" と対応する
const playerNameInput = useTemplateRef('player-name-input')
// onUpdated をアイテム追加のときだけ親へ知らせるための値
let isGettingItem = false
// ② アイテムを取り出す
const acquireItem = () => {
isGettingItem = true
acquiredItem.value = '小さな鍵'
}
// カードが画面に表示された後に実行される
onMounted(() => {
// マウント後なので、入力欄のDOMへフォーカスを当てられる
playerNameInput.value?.focus()
// 親画面へ onMounted の発動を伝える
props.onMounted()
})
// 状態変更によるDOM更新が終わった後に実行される
onUpdated(() => {
// プレイヤー名の入力では、今回の表示を変えない
if (!isGettingItem) {
return
}
isGettingItem = false
// アイテム追加による更新であることを親へ伝える
props.onUpdated(acquiredItem.value)
})
// カードが画面から取り除かれた後に実行される
onUnmounted(() => {
// 親は画面に残るため、結果を表示できる
props.onUnmounted()
})
</script>
<template>
<section class="treasure-box">
<h2>トレジャーボックス</h2>
<!-- useTemplateRef() で参照し、onMounted後にフォーカスする -->
<label>
プレイヤー名
<input
ref="player-name-input"
v-model="playerName"
type="text"
placeholder="表示後に自動でフォーカス"
>
</label>
<p v-if="playerName">
{{ playerName }} はトレジャーボックスを調べている
</p>
<!-- 1回だけ押せるため、更新のきっかけを追いやすい -->
<button
type="button"
:disabled="Boolean(acquiredItem)"
@click="acquireItem"
>
② アイテムを取り出す
</button>
<section class="item">
<h3>入手したアイテム</h3>
<!-- 初期状態と更新後の表示を見比べる -->
<p v-if="acquiredItem">
{{ acquiredItem }}
</p>
<p v-else>
まだアイテムを取り出していない
</p>
</section>
</section>
</template>
<style scoped>
/* 子コンポーネントも白背景で簡潔にする */
.treasure-box {
margin-top: 20px;
padding: 16px;
color: #1f2937;
background: #ffffff;
border: 1px solid #374151;
border-radius: 8px;
}
/* 入力欄を操作対象として分かりやすくする */
input {
display: block;
width: 100%;
max-width: 320px;
margin-top: 6px;
padding: 8px;
color: #111827;
font: inherit;
background: #ffffff;
border: 1px solid #4b5563;
border-radius: 4px;
}
/* アイテム表示を区切る */
.item {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #9ca3af;
}
</style>
useTemplateRef('player-name-input')は、テンプレートのref="player-name-input"と対応しています。DOM要素はマウント後に参照できるため、onMountedの中でfocus()を実行しています。Vue公式のテンプレート参照にも、同じようにuseTemplateRef()とonMounted()を組み合わせる例があります。 ja.vuejs
つまずいたところ
onUpdatedが、アイテム追加以外でも呼ばれる点で少し迷いました。
onUpdatedは更新の種類を選ばない
プレイヤー名へ文字を入力すると、画面に表示する名前も変化します。このときもDOM更新が起こるため、onUpdatedが呼ばれます。
今回はisGettingItemを使い、「アイテムを取り出す」操作の直後だけ、親画面の表示色とメッセージを変更するようにしました。onUpdatedはリアクティブな状態変更によってDOMツリーが更新された後に呼ばれるため、何の更新を確認したいのかを整理する必要がありそうです。 ja.vuejs
![]() |
「入力欄の文字も、アイテムの追加も、Vueにとっては画面更新なんだね。`onUpdated`では、どの更新を見たいのかを自分で決める必要がありそう。」 |
onUnmountedは後片づけに使う
「③ トレジャーボックスを片づける」を押すと、v-ifがfalseになり、カードが画面から消えます。その後、親の表示エリアはオレンジへ変わり、onUnmountedが発動したことを確認できます。
実際のアプリでは、タイマーの停止、イベントリスナーの解除、外部ライブラリの破棄など、画面から消えた後に残したくない処理をonUnmountedへ置くことがありそうです。
![]() |
「カードが消えたあとも、タイマーやイベントだけ動き続けたら困りそう。`onUnmounted`は、外した装備の効果を解除する場所のように考えると覚えやすい。」 |
今の理解(メモ)
画面に出す・表示を変える・画面から外す、という流れに分けると整理しやすくなりました。
- 仮想DOM: Vueがメモリ上で持つUIの構造情報。トレジャーボックスやアイテム表示の設計図のようなもの
- マウント: 仮想DOMをもとに実DOMを作り、コンポーネントを画面へ表示すること
- PATCH: 状態変更後に新旧の仮想DOMを比較し、必要な実DOMの変更を反映すること
- アンマウント: コンポーネントを実DOMから取り除くこと
-
useTemplateRef(): テンプレートにあるDOM要素や子コンポーネントを参照するもの
onMountedの使いどころ
コンポーネントが画面に表示された後、DOMを使う処理を置く場所です。今回は入力欄へのフォーカスを置きました。地図、グラフ、動画プレイヤーなどをDOM要素へ初期化する場面でも使えそうです。 ja.vuejs
onUpdatedの使いどころ
更新済みのDOMを使う処理を置く場所です。例えばアイテムログが増えた後、ログ欄を末尾までスクロールする処理はここへ置けそうです。フック内でリアクティブな値を更新すると再更新が起こる可能性があるため、値を変える処理はイベント関数側へ置くほうがよさそうです。 ja.vuejs
onUnmountedの使いどころ
コンポーネントが消えた後、不要な処理を片づける場所です。画面にメッセージを出すことよりも、タイマー停止やイベント解除のような後片づけで使うものだと今は理解しています。 ja.vuejs
![]() |
「ライフサイクルフックは、Vueが勝手に何かをしてくれる仕組みというより、“今ならこの処理を置ける”と教えてくれる目印なんだね。」 |
次にやりたいこと
今回の3操作を起点に、使いどころをもう少し試したいです。
-
onMountedでタイマーを開始し、カードが表示されている間だけ秒数を増やす -
onUnmountedでclearInterval()を呼び、カードを片づけた後にタイマーが止まるか確認する -
onUpdatedでアイテム追加後にログ欄を末尾までスクロールする -
v-ifをv-showへ変更し、カードを隠した場合にonUnmountedが発動するか比べる - アイテムを複数にし、
v-forで一覧を増やしたときのPATCHを確認する
今日の整理
今回の画面は、1操作ずつフックを確認する流れにしました。
ゲーム画面のマップ全体を毎回作り直すのではなく、トレジャーボックスの中身が変わった部分だけを更新するようなイメージで考えると、仮想DOMとPATCHの役割を今は整理しやすく感じています。
| 用語・フック | 役割 | 今回の画面で見えること |
|---|---|---|
| 仮想DOM | UI構造をメモリ上で表す | カードやアイテム表示の構造をVueが持つ |
| マウント | 実DOMを作り画面へ置く | カードが出現する |
| PATCH | 差分を実DOMへ反映する | アイテム表示が変わる |
onMounted |
マウント後に実行する | 青の表示、入力欄へのフォーカス |
onUpdated |
DOM更新後に実行する | 緑の表示、アイテム追加 |
onUnmounted |
アンマウント後に実行する | オレンジの表示、カード消去 |
useTemplateRef() |
DOM要素を参照する | 入力欄を取得する |
おわりに
今回は、トレジャーボックスを置く・アイテムを取り出す・片づけるという3操作に分けて、ライフサイクルフックを確認しました。表示色とフック名を一緒に変える形にすると、どの操作で何が発動したのかを追いやすく感じます。
まだ仮想DOMやPATCHの内部処理を細かく説明できる段階ではありませんが、画面の変化とライフサイクルフックを結び付けると、Vueでコンポーネントを扱うイメージが少し持てるようになりました。
学習中の内容なので、勘違いや理解不足もあると思います。もし気になる点があれば教えてもらえるとうれしいです。
注意事項
本ブログに掲載している内容は、私個人の見解であり、
所属する組織の立場や戦略、意見を代表するものではありません。
あくまでエンジニアとしての経験や考えを発信していますのでご了承ください。

