Vue 3: watch vs watchEffect — когда что использовать и не стрелять себе в ногу

Если ты используешь Composition API, ты точно сталкивался с watch и watchEffect. Они похожи, но ведут себя по-разному — и неправильный выбор легко приводит к лишним рендерам, багам и «магии». Разберёмся, где какой инструмент уместен, с примерами из реальной разработки.


В чём разница на пальцах

Коротко:

  • watch — когда ты знаешь, за чем следить
  • watchEffect — когда ты не хочешь думать о зависимостях

watch — контроль и предсказуемость

watch отслеживает конкретный источник (ref, computed, getter).

Пример

import { ref, watch } from 'vue'const search = ref('')
const results = ref([])watch(search, async (newValue, oldValue) => {
// вызывается только при изменении search
if (!newValue) {
results.value = []
return
} const res = await fetch(`/api?q=${newValue}`)
results.value = await res.json()
})

Что важно

  • зависимость указываешь явно
  • есть доступ к oldValue
  • можно контролировать момент вызова (immediate, flush)

watchEffect — реактивная магия

watchEffect автоматически собирает зависимости изнутри функции.

Пример

import { ref, watchEffect } from 'vue'const userId = ref(1)
const user = ref(null)watchEffect(async () => {
// Vue сам поймёт, что userId — зависимость
const res = await fetch(`/api/users/${userId.value}`)
user.value = await res.json()
})

Что важно

  • зависимости определяются автоматически
  • нет oldValue
  • выполняется сразу при создании

Паттерн 1: API-запросы — почти всегда watch

Почему не watchEffect?

Потому что он может зацепить лишние зависимости.

watchEffect(() => {
console.log(userId.value) // если тут случайно появится ещё одна реактивная переменная —
// она тоже станет зависимостью
})

👉 В итоге запрос может триггериться неожиданно.

Правильный вариант

watch(userId, async (id) => {
const res = await fetch(`/api/users/${id}`)
user.value = await res.json()
})

Вывод:
API-запросы → watch


Паттерн 2: Синхронизация состояния — watchEffect

Когда тебе нужно просто «держать всё в актуальном состоянии».

const firstName = ref('John')
const lastName = ref('Doe')
const fullName = ref('')watchEffect(() => {
fullName.value = `${firstName.value} ${lastName.value}`
})

👉 Тут не хочется перечислять зависимости.

Но! В реальности лучше использовать computed:

const fullName = computed(() => `${firstName.value} ${lastName.value}`)

Паттерн 3: Работа с несколькими зависимостями

watch

watch([a, b], ([newA, newB]) => {
console.log(newA, newB)
})

watchEffect

watchEffect(() => {
console.log(a.value, b.value)
})

Когда что?

  • если зависимостей немного и они фиксированы → watch
  • если зависимостей много или они динамические → watchEffect

Паттерн 4: Дебаг и читаемость

watch — легче дебажить

watch(userId, (newVal, oldVal) => {
console.log('changed from', oldVal, 'to', newVal)
})

watchEffect — сложнее понять триггер

watchEffect(() => {
console.log(userId.value)
console.log(settings.value)
})

👉 Какая переменная вызвала эффект? Неочевидно.


Паттерн 5: Побочные эффекты (side effects)

Оба подходят, но:

  • watch — для контролируемых эффектов
  • watchEffect — для быстрых реакций

Пример: сохранение в localStorage

watch(
() => form.value,
(val) => {
localStorage.setItem('form', JSON.stringify(val))
},
{ deep: true }
)

👉 Здесь важно контролировать изменения → watch


Подводные камни

1. watchEffect может вызвать лишние запросы

watchEffect(() => {
fetch(`/api/${userId.value}`) console.log(theme.value) // ← внезапная зависимость
})

👉 любое изменение theme → новый запрос


2. async внутри watchEffect

watchEffect(async () => {
const res = await fetch(`/api/${id.value}`)
})

⚠️ Vue отслеживает зависимости только до первого await

👉 после await реактивность может потеряться


3. очистка эффектов

watchEffect((onCleanup) => {
const controller = new AbortController() fetch('/api', { signal: controller.signal }) // отменяем предыдущий запрос
onCleanup(() => controller.abort())
})

👉 важно для debounce / race conditions


Когда что использовать — краткая таблица

СценарийИспользовать
API-запросwatch
Явная зависимостьwatch
Нужно oldValuewatch
Быстрая реактивная логикаwatchEffect
Много зависимостейwatchEffect
ПрототипированиеwatchEffect

TL;DR

  • watch — контроль, стабильность, продакшен
  • watchEffect — удобство, но с риском скрытых зависимостей
  • API-запросы и бизнес-логика → почти всегда watch
  • watchEffect хорошо подходит для быстрых реактивных эффектов и прототипов
  • если сомневаешься — бери watch

Итог

watch и watchEffect — не взаимозаменяемые инструменты. Первый даёт контроль и предсказуемость, второй — скорость разработки. Основная ошибка — использовать watchEffect там, где важна точность зависимостей (например, API-запросы). В продакшене лучше быть чуть более явным и использовать watch. А watchEffect оставить для простых эффектов и случаев, где список зависимостей неочевиден.