Vueには、データの変化に反応して処理を実行する仕組みが2つあります。watchとwatchEffectです。どちらも副作用を扱うための仕組みで、一見似ていますが、依存する値の指定方法や、実行のタイミングが違います。この記事では、Vue 3を前提に、違いを整理して、使い分けの指針を示します。watchそのものの詳しい使い方はwatchを活用してデータの変化を監視する方法をご覧ください。
watchとwatchEffectの違い
最大の違いは、「何に反応するか」を自分で指定するか、自動に任せるかです。watchは、監視する値を第1引数で明示し、その値が変わったときだけコールバックを呼びます。コールバックの中で使った値は追跡しません。一方、watchEffectは、関数の中で使ったリアクティブな値を自動で追跡し、依存が変わるたびに再実行します。
| 比較項目 | watch | watchEffect |
|---|---|---|
| 依存する値 | 第1引数で明示する | 関数の中で使った値を自動で追跡する |
| 初回の実行 | しない(lazy)。immediate: trueで実行できる |
最初にすぐ1回実行する |
| 旧値 | 受け取れる(oldValue) |
受け取れない |
| クリーンアップ | 第3引数のonCleanup |
コールバックの引数のonCleanup |
公式は、watchは依存の追跡と副作用を分けるので、いつコールバックが動くかを細かく制御できると説明しています。watchEffectは、追跡と実行が一体で、コードが短くなる反面、依存関係が見えにくくなります。
同じ処理を両方で書いて比べる
「IDが変わったらデータを取得する」という同じ処理を、2通りで書きます。まずwatchです。監視する値を明示し、最初にも実行するためにimmediate: trueを付けています。
<script setup>
import { ref, watch } from 'vue'
const todoId = ref(1)
const data = ref(null)
// 監視する値(todoId)を第1引数で明示する
// watch は lazy なので、最初にも実行したいなら immediate: true を付ける
watch(
todoId,
async () => {
data.value = null
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
},
{ immediate: true }
)
</script>
次にwatchEffectです。依存の指定が不要で、最初の実行もしてくれるため、短く書けます。
<script setup>
import { ref, watchEffect } from 'vue'
const todoId = ref(1)
const data = ref(null)
// 関数の中で使った todoId が、自動で追跡される。最初に1回すぐ実行される
watchEffect(async () => {
data.value = null
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
</script>
watchEffectの落とし穴:awaitの後は追跡されない
watchEffectが依存を追跡するのは、同期的に実行されている間だけです。公式は、asyncのコールバックでは、最初のawaitより前にアクセスした値だけが追跡されると明記しています。awaitの後で参照した値は、変わっても再実行されません。
<script setup>
import { ref, watchEffect } from 'vue'
const a = ref(1)
const b = ref(1)
watchEffect(async () => {
const x = a.value // await の前:追跡される
await Promise.resolve()
const y = b.value // await の後:追跡されない
console.log(x, y)
})
// a を変えると再実行される
// b を変えても、再実行されない
</script>
非同期の処理で、awaitの後に使う値にも反応させたい場合は、その値をawaitの前に変数へ取り出しておくか、watchで明示的に監視してください。
旧値が必要なら watch
変更前の値と比べたい場合は、watchを使います。watchEffectは旧値を受け取れません。
<script setup>
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
// watch:変更前の値(oldValue)を受け取れる
watch(count, (newValue, oldValue) => {
console.log(`${oldValue} → ${newValue}`)
})
// watchEffect:旧値は受け取れない。現在の値だけを使う
watchEffect(() => {
console.log(`現在の値は ${count.value}`)
})
</script>
クリーンアップの渡し方
タイマーや、途中で打ち切りたい通信など、次の実行の前に片付けたい処理には、onCleanupを使います。どちらにも用意されていますが、渡される場所が違います。watchは第3引数、watchEffectはコールバックの引数です。戻り値で関数を返す書き方ではありません。
<script setup>
import { ref, watch, watchEffect } from 'vue'
const id = ref(1)
// watch:第3引数に onCleanup が渡される
watch(id, (newId, oldId, onCleanup) => {
const timer = setInterval(() => console.log('watch', newId), 1000)
onCleanup(() => clearInterval(timer))
})
// watchEffect:コールバックの引数に onCleanup が渡される
// (戻り値で関数を返すのではない)
watchEffect((onCleanup) => {
const timer = setInterval(() => console.log('effect', id.value), 1000)
onCleanup(() => clearInterval(timer))
})
</script>
実行タイミング:flush
どちらも、コールバックは、既定ではコンポーネントのDOMが更新される前に実行されます。更新後のDOMを参照したいときは、flush: 'post'を指定します。watchEffectには、同じ意味のwatchPostEffectと、同期的に実行するwatchSyncEffectという別名もあります。同期的なウォッチャーはまとめ処理が効かず、値が何度も変わるたびに実行されるので、乱用は避けてください。
<script setup>
import { ref, watch, watchEffect, watchPostEffect } from 'vue'
const count = ref(0)
// 既定(flush: 'pre'):コンポーネントのDOM更新の前に実行される
watchEffect(() => { /* ... */ })
// 更新後のDOMを触りたいとき(どちらも同じ意味)
watchEffect(() => { /* ... */ }, { flush: 'post' })
watchPostEffect(() => { /* ... */ })
// watch でも同じオプションが使える
watch(count, () => { /* ... */ }, { flush: 'post' })
</script>
止める・一時停止する
watchもwatchEffectも、停止用のハンドルを返します。<script setup>やsetupの中で同期的に作ったものは、コンポーネントの破棄で自動的に止まります。手動で止めたいときだけ、ハンドルを使います。pauseとresumeが使えるバージョンは、公式のAPIリファレンスで確認してください。
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// どちらの関数も、停止用のハンドルを返す
const { stop, pause, resume } = watch(count, () => {
console.log('変更されました')
})
pause() // 一時停止
resume() // 再開
stop() // 完全に停止
</script>
使い分けの指針
- watch:反応する値を明確にしたいとき、変更前の値が必要なとき、最初は実行したくないとき。
- watchEffect:複数の値に反応する処理を短く書きたいとき、最初にすぐ実行したいとき。ただし、
awaitの後の値は追跡されない点に注意。 - ほかの値から計算できるだけなら、どちらでもなく
computedです。詳しくはcomputedプロパティで動的なデータ処理を行う方法をご覧ください。
迷ったら、依存が分かりやすいwatchから始めるのがおすすめです。処理が単純で、反応する値が自明なときだけwatchEffectに切り替えると、読み手が混乱しにくくなります。
関連記事
- 通信で取得するデータの扱いは非同期データの取得と表示
- JSONファイルの取得はJSONファイルからデータを取得する方法
- 入力値の扱いは双方向データバインディングでフォームの入力値を反映させる方法
よくある質問(FAQ)
asyncのコールバックでは、最初のawaitより前にアクセスした値だけが追跡されます。onCleanupに、片付ける関数を渡します。戻り値で関数を返す書き方は、Vueではありません。{ immediate: true }を第3引数に渡します。watchは、既定では、監視する値が変わるまでコールバックを呼びません。watchを使います。コールバックの第2引数でoldValueを受け取れます。watchEffectは、旧値を受け取れません。まとめ
watchは反応する値を明示し、変わったときだけ動いて、旧値も受け取れます。watchEffectは依存を自動で追跡し、最初にすぐ動いて、短く書けます。awaitの後の値が追跡されないことと、クリーンアップを引数で受け取ることを押さえておけば、どちらも安全に使えます。

