Vue — watch / watchEffect
テクノロジ系 / L03 lifecycle — ライフサイクルと算出プロパティ
Vue — watch / watchEffect
computed と watch の役割分担
| 用途 | |
|---|---|
| computed | 値の派生 (=何を表示するか) |
| watch | 副作用 (=変化した時に何かする) |
「変化を検知してログを取る」「変化したら API 呼ぶ」 → watch。
watch — 明示的に対象指定
<script setup>
import { ref, watch } from "vue";
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(`${oldVal} → ${newVal}`);
});
</script>第1引数: 監視対象 (ref / computed / 関数)
第2引数: コールバック (new, old が来る)
複数監視
<script setup>
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
// どちらかが変わると発火
});
</script>watchEffect — 自動追跡
<script setup>
import { ref, watchEffect } from "vue";
const userId = ref(1);
const user = ref<User | null>(null);
watchEffect(async () => {
// 内部で参照した ref が自動で依存になる
const res = await fetch(`/api/users/${userId.value}`);
user.value = await res.json();
});
</script>→ 依存を 明示せずに自動追跡 (React の useEffect の良いとこ取り)。
初回も実行される。
immediate オプション
<script setup>
// watch のデフォルトは「変化したら」発火 (初回呼ばれない)
watch(userId, fetchUser, { immediate: true });
</script>watchEffect は常に immediate。
watch は明示で immediate: true。
deep オプション
<script setup>
const user = ref({ name: "Taro", age: 20 });
// ❌ user.name を変えても発火しない
watch(user, (v) => console.log(v));
// ✅ deep: true で深く監視
watch(user, (v) => console.log(v), { deep: true });
</script>object/配列の中身の変化を見たい時に。
cleanup
<script setup>
watchEffect(async (onCleanup) => {
const ctrl = new AbortController();
onCleanup(() => ctrl.abort());
const res = await fetch(`/api/users/${userId.value}`, { signal: ctrl.signal });
user.value = await res.json();
});
</script>onCleanup で 次の実行前 に呼ばれる関数を登録。
race condition / メモリリーク対策。
React useEffect との対比
| React | Vue | |
|---|---|---|
| 副作用 | useEffect | watch / watchEffect |
| 依存配列 | 手動 [a, b] | 自動 (watchEffect) or 明示 (watch) |
| cleanup | return () => ... | onCleanup(() => ...) |
| 初回実行 | 常に | watch は明示 (immediate) |
Vue の方が 依存追跡が楽、React は明示で安全寄り。
まとめ
- 派生値は computed、副作用は watch
watch(target, cb)で明示監視watchEffect(cb)で自動追跡immediate / deep / onCleanupのオプション- React useEffect との対応関係を理解
最後は ライフサイクルフック。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Lv1: 基本 — watch と watchEffect
問題
<script setup>
const count = ref(0);
const name = ref("");
// watch: 明示
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`);
});
// watchEffect: 自動追跡
watchEffect(() => {
console.log("effect:", count.value, name.value);
});
</script>count や name を変えて、両方のログを観察。
合格基準
- watch は count 変更時のみ
- watchEffect は count or name どちらでも + 初回も実行
Lv2: 応用 — fetch を watchEffect で
問題
<script setup lang="ts">
import { ref, watchEffect } from "vue";
const userId = ref(1);
const user = ref<any>(null);
watchEffect(async (onCleanup) => {
const ctrl = new AbortController();
onCleanup(() => ctrl.abort());
user.value = null;
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId.value}`, { signal: ctrl.signal });
user.value = await res.json();
});
</script>
<template>
<input v-model.number="userId" type="number" />
<pre>{{ user }}</pre>
</template>userId を変えると自動再 fetch + 古い fetch はキャンセル。
合格基準
- userId 変化で自動再取得
- onCleanup で abort
Lv3: 発展 — deep watch
問題
<script setup>
const user = ref({ name: "Taro", profile: { age: 20 } });
// ❌ deep 無しでは profile.age を変えても発火しない
watch(user, () => console.log("watch (no deep)"));
// ✅ deep: true
watch(user, () => console.log("watch deep"), { deep: true });
setTimeout(() => {
user.value.profile.age = 21; // どちらが発火する?
}, 1000);
</script>deep を使わない場合と使う場合の差を確認。
合格基準
- deep なしでは発火しない
- deep: true で発火
- 性能トレードオフを理解
提出
submission.md に3問の回答を。