watchは、データが変わったときに、決まった処理を実行する仕組みです。入力値を保存する、条件が変わったらAPIを呼ぶ、といった「変化をきっかけにした処理」に使います。この記事では、Vue 3(<script setup>)を前提に、基本から、つまずきやすい挙動までを動くコードで整理します。
基本の書き方
watch(監視する値, コールバック)の形で書きます。コールバックには、変更後の値と、変更前の値が渡されます。
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const message = ref('')
// count が変わるたびに実行される
watch(count, (newValue, oldValue) => {
message.value = `カウントが ${oldValue} から ${newValue} に変わりました`
})
</script>
<template>
<button @click="count++">+1</button>
<p>{{ message }}</p>
</template>
computedとの使い分け
watchを使う前に、まずcomputedで済まないかを考えます。ほかの値から計算できる値はcomputed、状態の変化に応じて何かを実行する副作用はwatch、という使い分けが基本です。たとえば、文字列の長さを別の変数に入れる処理は、watchではなくcomputedで書きます。
<script setup>
import { ref, computed } from 'vue'
const message = ref('Hello, Vue.js!')
// 他の値から計算できるものは computed で書く(watch で代入しない)
const length = computed(() => message.value.length)
</script>
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
// 値が変わったら外部に作用する処理(ここでは保存)は watch の出番
watch(keyword, (newValue) => {
localStorage.setItem('keyword', newValue)
})
</script>
computedの詳しい使い方はcomputedプロパティで動的なデータ処理を行う方法をご覧ください。
監視できる対象と、getterの渡し方
watchの第1引数には、ref、reactiveオブジェクト、getter関数、またはそれらの配列を渡せます。注意したいのは、reactiveのプロパティです。watch(state.count, …)は、数値そのものを渡すことになり、監視になりません。() => state.countのように、関数で渡します。
<script setup>
import { reactive, watch } from 'vue'
const state = reactive({ count: 0 })
// NG:数値そのものを渡しても、監視にならない
// watch(state.count, (v) => { ... })
// OK:getter(関数)で渡す
watch(
() => state.count,
(count) => {
console.log(`count は ${count}`)
}
)
</script>
複数の値をまとめて監視する
<script setup>
import { ref, watch } from 'vue'
const x = ref(1)
const y = ref(2)
// 配列で複数のソースを監視(どちらかが変わると実行)
watch([x, y], ([newX, newY], [oldX, oldY]) => {
console.log(`x: ${oldX}→${newX}, y: ${oldY}→${newY}`)
})
</script>
deepオプションと、oldValueの注意
オブジェクトの内部の変更まで拾うのが、deepです。挙動は、渡し方によって変わります。
reactiveのオブジェクトを直接渡すと、暗黙に深く監視されます。- getterで渡すと、そのオブジェクトが置き換えられたときだけ発火します。内部の変更も拾いたいなら、
deep: trueを付けます。 - ネストした変更では、オブジェクト自体が置き換わらない限り、
newValueとoldValueは同じオブジェクトです。変更前の値を比べる用途には使えません。 - 深い監視は、中のプロパティをすべてたどるため、大きなデータではコストが高くなります。必要なときだけ使ってください。
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({ name: 'Alice', profile: { age: 25 } })
// reactive を直接渡すと、暗黙に deep になる
watch(user, (newValue, oldValue) => {
// ネストした変更では newValue === oldValue(同じオブジェクト)
console.log('変更されました', newValue.profile.age)
})
// getter で渡すと、オブジェクトが置き換えられたときだけ発火する
watch(
() => user.profile,
() => { console.log('profile が置き換えられました') }
)
// getter で渡したうえで、ネストの変更も拾いたいときは deep: true
watch(
() => user.profile,
() => { console.log('profile の中身が変わりました') },
{ deep: true }
)
user.profile.age++ // 1つ目と3つ目が発火
</script>
immediateとonce
immediate: trueを付けると、コールバックが、ウォッチャーの作成時にすぐ1回実行されます。「コンポーネントがマウントされた後」ではなく、作成時(<script setup>の実行時)です。初期値で一度処理したいときに使います。once: trueはVue 3.4以降で、最初の変更の1回だけ実行して終わります。
<script setup>
import { ref, watch } from 'vue'
const query = ref('vue')
// immediate: true ならウォッチャーの作成時に1回、すぐに実行される
watch(
query,
(newValue) => {
console.log('検索:', newValue)
},
{ immediate: true }
)
// once: true(Vue 3.4 以降)なら、最初の変更の1回だけ実行される
watch(query, () => console.log('最初の変更'), { once: true })
</script>
flushでコールバックの実行タイミングを変える
コールバックは、既定ではDOMが更新される前に実行されます。更新後のDOMを参照したいときは、flush: 'post'を指定します。
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 既定ではDOMの更新前にコールバックが動く。
// 更新後のDOMを触りたいときは flush: 'post'
watch(count, () => {
console.log(document.querySelector('#count').textContent)
}, { flush: 'post' })
</script>
監視を止める
<script setup>やsetupの中で同期的に作ったウォッチャーは、コンポーネントが破棄されると自動で止まります。手動で止めたい場合は、watchが返す関数を呼びます。
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// watch は停止用の関数を返す
const unwatch = watch(count, () => {
console.log('監視中')
})
// 監視をやめる
unwatch()
</script>
古いリクエストを中止する
値が素早く何度も変わるときは、前のリクエストの結果が、後から返ってくることがあります。onWatcherCleanupを使うと、次の実行の前に、前回の処理を片付けられます。ここでは、AbortControllerで前のリクエストを中止しています。
<script setup>
import { ref, watch, onWatcherCleanup } from 'vue'
const id = ref(1)
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/items/${newId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => { /* 結果を使う */ })
// 次の実行の前に呼ばれる。古いリクエストを中止する
onWatcherCleanup(() => controller.abort())
})
</script>
非同期データの取得の基本は非同期データの取得と表示で解説しています。
watchEffectとの違い
watchは監視する値を明示し、変わったときだけ実行します。watchEffectは、コールバックの中で使った値を自動で追跡し、最初に必ず1回実行します。詳しい使い分けはwatch・watchEffectの違いと使い分けをご覧ください。
Options APIでの書き方
Options APIでは、watchオプションに書きます。既存のコードを読むときのために、書き方を押さえておくと安心です。
export default {
data() {
return { question: '', user: { name: 'Alice' } }
},
watch: {
// メソッドの形で書く
question(newQuestion, oldQuestion) {
// question が変わったときの処理
},
// オプションを付けるときは handler を使う
user: {
handler(newValue) { /* ... */ },
deep: true,
immediate: true
},
// ドットでつないだパスも指定できる
'user.name'(newValue) { /* ... */ }
}
}
関連記事
- 入力値の双方向バインディングは双方向データバインディングでフォームの入力値を反映させる方法
- イベントの扱いはv-onでDOMイベントを購読する方法
- 条件による表示はv-ifで条件分岐させる方法
よくある質問(FAQ)
computed、データの変化をきっかけに何かを実行する処理(保存、API呼び出しなど)はwatchです。watchの中で別の変数に代入しているなら、computedで書けないか見直してください。reactiveのプロパティをwatch(obj.count, …)のように渡している可能性があります。() => obj.countの形で渡してください。また、getterで渡したオブジェクトの内部だけが変わった場合は、deep: trueが必要です。newValueとoldValueが同じオブジェクトを指します。変更前の値が必要なら、変更前にコピーを取っておいてください。まとめ
watchは、データの変化をきっかけに、副作用を実行するための仕組みです。計算で済む値はcomputedで書き、reactiveのプロパティはgetterで渡し、deepのoldValueは同じオブジェクトであることに注意します。非同期の処理では、onWatcherCleanupで古いリクエストを片付けると、安全に書けます。
