Watchers - state変化に反応する副作用管理
ロードマップ: Vue.js学習ロードマップ
公式ドキュメント: https://vuejs.org/guide/essentials/watchers.html
What(何についてか)
Vue の reactive state が変化した時に、副作用をどのように安全に実行するかを扱う。
watch と watchEffect の役割差、監視対象の指定方法、deep watcher、eager / once watcher、side effect cleanup、flush timing、自動停止と手動停止を整理する。
Why(なぜ必要か)
computed は state から派生値を作るための仕組みだが、実務では値を返すだけでは足りない。
状態変化をきっかけに API を叩く、別 state を更新する、DOM に副作用を与える、外部購読を開始・停止するといった処理が必要になる。
Watchers はそのための仕組みであり、単なる値監視ではなく副作用の寿命管理まで含む。
How(どう動くか)
1. computed と watch は役割が違う
computedは既存 state から派生値を作るwatchは state 変化をきっかけに callback を実行するwatchEffectは callback 内で読んだ依存を自動追跡して再実行する
watch の基本例
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>watch は返り値を作るためではなく、副作用を起こすために使う。
2. watch は source を明示する
watch の基本形は次のとおりである。
watch(source, callback, options?)callback 側は必要な引数だけ受け取ればよい。
watch(x, (newX, oldX) => {})
watch(x, (newX) => {})
watch(x, (newX) => {}, { immediate: true })監視できる source の種類
refcomputed refreactive object- getter function
- 複数 source の配列
const x = ref(0)
const y = ref(0)
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
watch([x, y], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})reactive object の一部は getter で監視する
const obj = reactive({ count: 0 })
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)watch(obj.count, ...) のように書くと、その場で評価された number を渡すことになり、reactive source を監視できない。
3. deep watcher はネスト内部変更も拾う
watch はデフォルトでは shallow であり、監視対象そのものが置き換わった時に反応する。
ネスト内部の mutation まで拾いたい場合は deep: true を使う。
watch(
() => state.someObject,
(newValue, oldValue) => {
// nested mutation でも反応
},
{ deep: true }
)reactive object を直接 watch() に渡した場合は、暗黙に deep watcher になる。
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// nested mutation でも反応
})deep watcher の注意点
nested mutation では newValue と oldValue が同じ参照になることがある。
deep watcher は「差分スナップショット」を渡すのではなく、「何か変わった」という通知に近い。
また、大きなデータ構造での deep watch は高コストになりやすいため慎重に使う。
4. eager / once watcher で実行回数を制御する
immediate: true
watch は通常 lazy で、source が変わるまで callback を実行しない。
初回にも同じ処理を走らせたい場合は immediate: true を使う。
watch(source, callback, { immediate: true })これは「初回実行 + 以後の変化追従」に向く。
初期 state を ref() で持つことと、初回副作用を走らせることは別問題である。
once: true
最初の変化だけ拾って自動停止したい場合に使う。
watch(source, callback, { once: true })once は「初回変化で 1 回だけ」、immediate は「最初から 1 回走らせる」という違いがある。
5. watchEffect は依存を自動追跡する
const todoId = ref(1)
const data = ref(null)
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})watchEffect は callback を即実行し、その中で読まれた reactive dependency を自動追跡する。
watch と watchEffect の違い
| 項目 | watch | watchEffect |
|---|---|---|
| 依存指定 | source を明示する | callback 内で読んだ依存を自動追跡する |
| 初回実行 | デフォルトではしない | 即実行する |
| 明示性 | 高い | 低いが記述量は少ない |
| 適した用途 | どの source を見るか厳密に制御したい時 | callback が読む依存をそのまま追いたい時 |
自動追跡の本質
watchEffect が拾うのは「読んだ reactive source」であり、「書いただけの state」ではない。
todoId.valueの読み出し →todoIdが依存として追跡されるdata.value = ...の書き込み →dataは依存としては追跡されない
つまり watchEffect は runtime に「この callback が何に依存しているか」を推論させる仕組みである。
6. Side Effect Cleanup で前回副作用を片付ける
watcher は再実行されるため、前回副作用の cleanup が必要になることがある。 典型例は fetch の abort、timer 解除、event listener 解除、subscription 停止である。
onCleanup
watch(id, (newId, oldId, onCleanup) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal })
onCleanup(() => {
controller.abort()
})
})onWatcherCleanup()(Vue 3.5+)
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal })
onWatcherCleanup(() => {
controller.abort()
})
})cleanup は「コンポーネント全体の終了処理」ではなく、「watcher 1 回分の副作用の寿命管理」である。
onUnmounted とは粒度が違う。
7. flush timing で callback 実行タイミングを制御する
watcher callback は実行タイミングを制御できる。
デフォルト(pre)
親コンポーネント更新後、owner component の DOM 更新前に callback が走る。 更新後 DOM を見たい場合は不向きである。
flush: 'post'
watch(source, callback, { flush: 'post' })
watchEffect(callback, { flush: 'post' })更新後 DOM を扱いたい時に使う。
watchPostEffect() という短縮 API もある。
flush: 'sync'
watch(source, callback, { flush: 'sync' })
watchEffect(callback, { flush: 'sync' })変更検知直後に同期実行する。
watchSyncEffect() という短縮 API もある。
ただしバッチングされず頻繁に発火しやすいため、高コストになりやすい。
8. watcher は普通は自動停止される
setup() / <script setup> 内で同期的に作られた watch / watchEffect は、owner component が unmount された時に自動停止される。
通常は手動停止を意識しなくてよい。
watchEffect(() => {
// auto-stopped on unmount
})ただし非同期に作ると自動停止に乗らないことがある
setTimeout(() => {
watchEffect(() => {
// not automatically bound
})
}, 100)このような場合は手動停止が必要になることがある。 そのため watcher は同期的に作り、必要なら callback 内で条件分岐する設計が推奨される。
手動停止
const unwatch = watchEffect(() => {
// ...
})
unwatch()Watchers の全体フロー
flowchart LR A["reactive state 変化"] --> B["watch / watchEffect が検知"] B --> C["必要なら前回 cleanup 実行"] C --> D["callback 実行"] D --> E["副作用実行 API / DOM / state update"]
実務での判断軸
派生値なら computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)特定 source を厳密に見たいなら watch
watch(userId, async (id) => {
data.value = await fetchUser(id)
})初回も含めて同じ処理をしたいなら immediate
watch(userId, async (id) => {
data.value = await fetchUser(id)
}, { immediate: true })callback 内で読んだ依存をそのまま追いたいなら watchEffect
watchEffect(() => {
console.log(route.params.id)
console.log(locale.value)
})深い object 全体を雑に監視する前に getter で狙い撃ちを検討する
watch(() => form.profile.name, (name) => {
// ...
})更新後 DOM に反応したいなら post flush
watch(source, () => {
// DOM measurement after update
}, { flush: 'post' })Options API → Composition API 差分(補足)
| 項目 | Options API(旧) | Composition API(新) |
|---|---|---|
| 基本API | watch: { ... }, this.$watch() | watch(), watchEffect() |
| source 指定 | key 文字列や path | ref / getter / array / reactive object |
| cleanup | 文脈依存で扱う | onCleanup, onWatcherCleanup() を明示的に扱える |
this 依存 | this ベースの記述が多い | <script setup> で変数と関数を直接扱う |
| 学習上の重心 | option 設定の理解 | 副作用と依存追跡の設計理解 |
Key Concepts
| 用語 | 説明 |
|---|---|
| watcher | state 変化に反応して副作用を実行する仕組み |
watch | source を明示して監視する API |
watchEffect | callback 内で読んだ依存を自動追跡する API |
| source | 監視対象の reactive value / getter / object / array |
| deep watcher | ネスト内部変更まで監視する watcher |
| eager watcher | immediate: true で初回も実行する watcher |
| once watcher | 最初の変化だけ拾って止まる watcher |
| cleanup | 前回副作用を再実行前に片付ける処理 |
onCleanup | watcher callback 引数で受け取る cleanup 登録口 |
onWatcherCleanup | Vue 3.5+ の cleanup 登録 API |
| flush timing | callback の実行タイミング制御 |
| auto stop | owner component の unmount 時に watcher が自動停止する性質 |