採点されません。匿名です。 間違えることは学習の一部です。読むだけでもかまいません。

Vue — Lifecycle Hooks

テクノロジ系 / L03 lifecycle — ライフサイクルと算出プロパティ

Vue — Lifecycle Hooks

主要な hook

hookタイミング
onBeforeMountDOM作成前
onMountedDOM作成後 (頻出)
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つの型に構造化する

単一オブジェクト + 配列 + 辞書 + ネストを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本に代入する。

  1. onMounted は1つにまとめる(落とし穴1)

1リクエストで取れるペイロードを2つの onMounted に割らない。取得は1回。

  1. loading / error を表示に出す(落とし穴2)

loading = ref(true)errorMsg = ref<string | null>(null) を用意し、

try で取得 → catcherrorMsg に格納(console.error だけで終わらせない)→ finallyloading を落とす。

テンプレートで「読み込み中 / エラー / 本体」の3状態を描き分ける。

  1. 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 があり、catcherrorMsg を画面に出す(console.error だけにしない)
  • テンプレートに loading / error / 本体 の3状態がある
  • id 取得が useRoute() 経由で統一されている
  • ref<any> が消え、detail.value?.work.title のように型付きでアクセスできる

提出

submission.md に4問の回答を。