はじめに
この記事では、Vue の computed と watch の使い分けを整理します。
どちらもリアクティブな値に関係するため、最初は似て見えます。
ただし、役割は同じではありません。
結論だけ先に言うと、判断基準は次です。
- 表示用の値を作るなら
computed - 値の変化をきっかけに処理を走らせるなら
watch
この軸で考えると、computed と watch の使い分けはかなり整理しやすくなります。
先に結論
computed は値として使います。
例えば、姓と名からフルネームを作るような処理です。
<script setup lang="ts">
import { computed, ref } from "vue"
const lastName = ref("山田")
const firstName = ref("太郎")
const fullName = computed(() => `${lastName.value} ${firstName.value}`)
</script>
<template>
<p>{{ fullName }}</p>
</template>
一方で、watch は値の変化をきっかけに処理を動かすために使います。
例えば、検索文字列が変わったら API を呼ぶような処理です。
<script setup lang="ts">
import { ref, watch } from "vue"
const searchText = ref("")
watch(searchText, async (text) => {
await fetch(`/api/search?q=${encodeURIComponent(text)}`)
})
</script>
かなり雑に言うと、次の分け方です。
-
computedは結果を画面や別の処理で使う -
watchは変化をきっかけに何かを実行する
computed は表示用の値を作るときに使う
computed は、リアクティブな値から別の値を作るときに使います。
例えば、金額と税率から税込価格を作る場合です。
<script setup lang="ts">
import { computed, ref } from "vue"
const price = ref(1000)
const taxRate = ref(0.1)
const priceWithTax = computed(() => price.value * (1 + taxRate.value))
</script>
<template>
<p>{{ priceWithTax }}円</p>
</template>
この場合、priceWithTax は状態ではありません。
price と taxRate から作れる派生値です。
そのため、別の ref にして手動で更新するより、computed にした方が自然です。
computed に向いているのは、次のような値です。
- 表示用に整形した文字列
- フィルタ済みの配列
- 合計金額
- 入力値から作るバリデーション結果
- ボタンを押せるかどうかの判定
どれも「元の値から作れる値」です。
watch は値の変化で処理したいときに使う
watch は、値が変わったことをきっかけに処理を動かすときに使います。
例えば、検索条件が変わったら一覧を再取得する場合です。
<script setup lang="ts">
import { ref, watch } from "vue"
const keyword = ref("")
const items = ref<string[]>([])
watch(keyword, async (value) => {
const response = await fetch(`/api/items?keyword=${encodeURIComponent(value)}`)
items.value = await response.json()
})
</script>
この処理の主目的は、表示用の値を計算することではありません。
keyword の変化をきっかけに、外部へ問い合わせています。
このような処理は watch の方が向いています。
watch に向いているのは、次のような処理です。
- API を呼ぶ
-
localStorageに保存する - URL クエリを更新する
- 外部ライブラリへ値を反映する
- 値が変わったタイミングでログを送る
共通しているのは、副作用があることです。
watch で表示用の値を作るのは避ける
watch でも、別の ref に値を代入すれば表示用の値を作れます。
ただし、これは基本的には避けた方がよいです。
例えば次のような書き方です。
<script setup lang="ts">
import { ref, watch } from "vue"
const lastName = ref("山田")
const firstName = ref("太郎")
const fullName = ref("")
watch([lastName, firstName], ([newLastName, newFirstName]) => {
fullName.value = `${newLastName} ${newFirstName}`
})
</script>
このコードでも動きます。
ただし、fullName は lastName と firstName から作れる値です。
この場合は computed の方が素直です。
<script setup lang="ts">
import { computed, ref } from "vue"
const lastName = ref("山田")
const firstName = ref("太郎")
const fullName = computed(() => `${lastName.value} ${firstName.value}`)
</script>
watch で派生値を作ると、更新処理が増えます。
また、初期値の設定や更新漏れも気にする必要があります。
元の値から作れるだけなら、まず computed を考える方が安全です。
computed の中で副作用を起こさない
逆に、computed の中で API 通信や保存処理をするのも避けた方がよいです。
例えば次のような書き方です。
const result = computed(async () => {
const response = await fetch(`/api/search?q=${searchText.value}`)
return await response.json()
})
これは computed の役割から外れています。
computed は、依存している値から別の値を作るためのものです。
外部と通信したり、どこかへ保存したりする場所ではありません。
API 通信をしたいなら、watch で変化を見て処理する方が読みやすいです。
watch(searchText, async (text) => {
const response = await fetch(`/api/search?q=${encodeURIComponent(text)}`)
results.value = await response.json()
})
computed は値を作る場所です。
watch は処理を走らせる場所です。
この分け方を守ると、コードの意図が読みやすくなります。
よくある判断例
実務で迷いやすい例を、判断基準で整理します。
フォームの入力値からエラーメッセージを作るなら computed です。
const emailError = computed(() => {
if (!email.value) {
return "メールアドレスを入力してください"
}
if (!email.value.includes("@")) {
return "メールアドレスの形式が正しくありません"
}
return ""
})
これは email から表示用の値を作っているだけです。
一方で、入力値が変わるたびに下書きを保存するなら watch です。
watch(body, (value) => {
localStorage.setItem("draft-body", value)
})
これはブラウザの保存領域へ書き込んでいます。
つまり副作用があります。
チェックボックスの状態から送信ボタンを押せるか判定するなら computed です。
const canSubmit = computed(() => {
return agreed.value && name.value.length > 0
})
ログイン状態が変わったら画面遷移するなら watch です。
watch(isLoggedIn, (value) => {
if (value) {
router.push("/home")
}
})
このように、値を作っているのか、処理を起こしているのかで分けると判断しやすいです。
迷ったらまず computed を検討する
迷ったときは、まず computed で書けないかを考えるとよいです。
理由は、画面に必要な値の多くは「今ある状態から作れる値」だからです。
例えば次のようなものです。
- 表示名
- 合計金額
- 絞り込み結果
- 並び替え結果
- 入力エラー
- ボタンの disabled 判定
これらは、元の値が変われば自然に変わるべき値です。
この種の値を watch で手動更新すると、コードが少しずつ複雑になります。
watch を使うのは、「値を作る」だけでは済まないときです。
- 通信する
- 保存する
- 画面遷移する
- 外部ライブラリを操作する
- DOM やブラウザ API と同期する
このような処理が出てきたら、watch を検討します。
まとめ
computed と watch は、どちらも Vue のリアクティブな値に関係します。
ただし、使う目的は違います。
ポイントをまとめると次のとおりです。
- 表示用の値を作るなら
computed - 値の変化をきっかけに処理するなら
watch -
computedは値として使う -
watchは副作用に使う -
watchで派生値を作るのは基本避ける -
computedの中で API 通信や保存処理をするのは避ける
迷ったら、まず「これは値を作っているのか、処理を起こしているのか」を考えると整理しやすいです。
値を作っているなら computed。
処理を起こしているなら watch。
この分け方だけでも、Vue のコードはかなり読みやすくなります。
Vue のリアクティブな書き方全体を React と比較したい場合は、別記事「VueのComposition APIとReact Hooksは何が似ていて何が違うのか」も関連します。