Vue — Lifecycle Hooks
テクノロジ系 / L03 lifecycle — ライフサイクルと算出プロパティ
Vue — Lifecycle Hooks
主要な hook
| hook | タイミング |
|---|---|
onBeforeMount | DOM作成前 |
onMounted | DOM作成後 (頻出) |
onBeforeUpdate | 再描画前 |
onUpdated | 再描画後 |
onBeforeUnmount | 破棄前 |
onUnmounted | 破棄後 (頻出) |
onErrorCaptured | 子のエラー捕捉 |
onMounted
<script setup lang="ts">
import { onMounted, ref } from "vue";
const inputRef = ref<HTMLInputElement | null>(null);
onMounted(() => {
inputRef.value?.focus();
});
</script>
<template>
<input ref="inputRef" />
</template>DOM が出来上がってから実行したい処理。
DOM 要素へのアクセス・focus・スクロール・サードパーティ初期化など。
onUnmounted (cleanup)
<script setup>
import { onMounted, onUnmounted } from "vue";
let timer: number;
onMounted(() => {
timer = setInterval(() => console.log("tick"), 1000);
});
onUnmounted(() => {
clearInterval(timer);
});
</script>タイマー・イベントリスナの解除をここで。
API 取得は watchEffect 推奨
<!-- ❌ 古いパターン -->
<script setup>
onMounted(async () => {
const res = await fetch(`/api/users/${userId.value}`);
user.value = await res.json();
});
watch(userId, async (id) => {
const res = await fetch(`/api/users/${id}`);
user.value = await res.json();
});
// → 初回 + 変化、両方書く必要がある
</script>
<!-- ✅ watchEffect で1つ -->
<script setup>
watchEffect(async () => {
const res = await fetch(`/api/users/${userId.value}`);
user.value = await res.json();
});
</script>ref で DOM 取得
<script setup lang="ts">
import { ref, onMounted } from "vue";
const inputEl = ref<HTMLInputElement | null>(null);
onMounted(() => {
inputEl.value?.focus();
});
</script>
<template>
<input ref="inputEl" type="text" />
</template>template の ref="inputEl" と ref(...) 変数が紐づく。
エラー境界
<script setup>
import { onErrorCaptured } from "vue";
onErrorCaptured((err, instance, info) => {
console.error("child error:", err, info);
// false を返すと親へ伝播停止
return false;
});
</script>子コンポーネントのエラーを捕捉。
React の ErrorBoundary と同じ用途。
まとめ
onMountedで DOM準備後、onUnmountedで破棄時 cleanup- API取得は
watchEffectで「初回 + 変化」を1つに - template ref は
ref(null)変数とref="name"で紐付け onErrorCapturedでエラー境界
L03 完。次は L04 ルーティング。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Lv1: 基本 — onMounted / onUnmounted
問題
<!-- Timer.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const tick = ref(0);
let id: number;
onMounted(() => {
console.log("mounted");
id = setInterval(() => tick.value++, 1000);
});
onUnmounted(() => {
console.log("unmounted");
clearInterval(id);
});
</script>
<template>
<p>{{ tick }}</p>
</template>
<!-- 親 -->
<script setup>
const show = ref(true);
</script>
<template>
<button @click="show = !show">Toggle</button>
<Timer v-if="show" />
</template>トグルで mount / unmount を観察。タイマーがメモリリークしないことを確認。
合格基準
- mount / unmount ログ
- unmount で interval 解除
Lv2: 応用 — template ref で DOM 取得
問題
<script setup lang="ts">
import { ref, onMounted } from "vue";
const inputEl = ref<HTMLInputElement | null>(null);
onMounted(() => {
inputEl.value?.focus();
});
</script>
<template>
<input ref="inputEl" type="text" />
</template>ページ読み込み時に input に自動 focus。
合格基準
- mount 時に focus
- ref と template ref の紐付き
Lv3: 発展 — onErrorCaptured
問題
<script setup lang="ts">
import { onErrorCaptured } from "vue";
onErrorCaptured((err, instance, info) => {
console.error("child error:", err, info);
return false; // 親への伝播を止める
});
</script>
<template>
<Boom /> <!-- 子で throw する -->
</template>
<!-- Boom.vue -->
<script setup>
throw new Error("boom");
</script>エラー境界として動作。
合格基準
- 子の例外を親で捕捉
- React の ErrorBoundary と同等
Lv4: 実戦 — onMounted async 取得 + ペイロードの interface 化 + 落とし穴回収
題材
作品詳細ページ(架空のオープン作品 API を想定)。
ルートの :id から1件の作品詳細を取得して表示する。
API は https://api.example.test/works/:id で次の JSON を返すものとする(実在サービス・実データは使わない / フィールド名も架空中立)。
// GET https://api.example.test/works/42
{
"work": { "id": 42, "title": "サンプル作品", "releaseYear": 2020, "runtime": 118 },
"members": [
{ "id": 1, "name": "出演者A", "role": "主演" },
{ "id": 2, "name": "出演者B", "role": "助演" }
],
"categories": { "drama": "ドラマ", "mystery": "ミステリ" }
}落とし穴入りの starter(これを直す)
<!-- WorkDetail.vue — 直すべき悪い例 -->
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import router from "../router";
const work = ref<any>(null);
const members = ref<any[]>([]);
const categories = ref<any>({});
// 落とし穴1: onMounted を2つに割って直列で取りに行っている(不要な分割)
onMounted(async () => {
// 落とし穴3: useRoute() を使わず router 直叩きで id を取る(画面ごとに書き方がバラバラ)
const id = router.currentRoute.value.params.id;
const res = await fetch(`https://api.example.test/works/${id}`);
const data = await res.json();
work.value = data.work;
members.value = data.members;
});
onMounted(async () => {
const id = router.currentRoute.value.params.id;
const res = await fetch(`https://api.example.test/works/${id}`);
const data = await res.json();
categories.value = data.categories;
// 落とし穴2: success だけ扱い、loading / error の表示が無い(失敗は握り潰し)
});
</script>
<template>
<div v-if="work">
<h1>{{ work.title }}</h1>
<ul>
<li v-for="m in members" :key="m.id">{{ m.name }} / {{ m.role }}</li>
</ul>
</div>
</template>やること
- 通信結果ペイロードを1つの型に構造化する
単一オブジェクト + 配列 + 辞書 + ネストを1型でまとめてから ref に入れる。
interface Work {
id: number;
title: string;
releaseYear: number;
runtime: number;
}
interface Member {
id: number;
name: string;
role: string;
}
interface WorkDetail {
work: Work; // 単一オブジェクト
members: Member[]; // 配列
categories: Record<string, string>; // 辞書
}
const detail = ref<WorkDetail | null>(null);
work / members / categories をバラバラの ref で持たず、ref<WorkDetail | null>(null) 1本に代入する。
onMountedは1つにまとめる(落とし穴1)
1リクエストで取れるペイロードを2つの onMounted に割らない。取得は1回。
loading/errorを表示に出す(落とし穴2)
loading = ref(true) と errorMsg = ref<string | null>(null) を用意し、
try で取得 → catch で errorMsg に格納(console.error だけで終わらせない)→ finally で loading を落とす。
テンプレートで「読み込み中 / エラー / 本体」の3状態を描き分ける。
idの取り方をuseRoute()に統一する(落とし穴3)
router.currentRoute.value.params 直叩きをやめ、const route = useRoute() で route.params.id を読む。
画面ごとに書き方が割れないよう、1つの取り方に揃える。
合格基準
- ペイロードが
interface WorkDetail(単一オブジェクト + 配列 + 辞書 + ネスト)1型に構造化され、ref<WorkDetail | null>(null)に代入されている onMountedが1つだけ(async () => { ... }内でawait取得)try/catch/finallyがあり、catchがerrorMsgを画面に出す(console.errorだけにしない)- テンプレートに loading / error / 本体 の3状態がある
id取得がuseRoute()経由で統一されているref<any>が消え、detail.value?.work.titleのように型付きでアクセスできる
提出
submission.md に4問の回答を。