Watchers - state変化に反応する副作用管理

ロードマップ: Vue.js学習ロードマップ

公式ドキュメント: https://vuejs.org/guide/essentials/watchers.html

What(何についてか)

Vue の reactive state が変化した時に、副作用をどのように安全に実行するかを扱う。 watchwatchEffect の役割差、監視対象の指定方法、deep watcher、eager / once watcher、side effect cleanup、flush timing、自動停止と手動停止を整理する。

Why(なぜ必要か)

computed は state から派生値を作るための仕組みだが、実務では値を返すだけでは足りない。 状態変化をきっかけに API を叩く、別 state を更新する、DOM に副作用を与える、外部購読を開始・停止するといった処理が必要になる。 Watchers はそのための仕組みであり、単なる値監視ではなく副作用の寿命管理まで含む。

How(どう動くか)

1. computedwatch は役割が違う

  • 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 の種類

  • ref
  • computed ref
  • reactive 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 では newValueoldValue が同じ参照になることがある。 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 を自動追跡する。

watchwatchEffect の違い

項目watchwatchEffect
依存指定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(新)
基本APIwatch: { ... }, this.$watch()watch(), watchEffect()
source 指定key 文字列や pathref / getter / array / reactive object
cleanup文脈依存で扱うonCleanup, onWatcherCleanup() を明示的に扱える
this 依存this ベースの記述が多い<script setup> で変数と関数を直接扱う
学習上の重心option 設定の理解副作用と依存追跡の設計理解

Key Concepts

用語説明
watcherstate 変化に反応して副作用を実行する仕組み
watchsource を明示して監視する API
watchEffectcallback 内で読んだ依存を自動追跡する API
source監視対象の reactive value / getter / object / array
deep watcherネスト内部変更まで監視する watcher
eager watcherimmediate: true で初回も実行する watcher
once watcher最初の変化だけ拾って止まる watcher
cleanup前回副作用を再実行前に片付ける処理
onCleanupwatcher callback 引数で受け取る cleanup 登録口
onWatcherCleanupVue 3.5+ の cleanup 登録 API
flush timingcallback の実行タイミング制御
auto stopowner component の unmount 時に watcher が自動停止する性質