Каждый, кто использует Vue для разработки или только его изучает, так или иначе встречается с необходимостью выполнить какое‑либо побочное действие при изменении значений, и сразу в голове возникает мысль о двух методах‑наблюдателях — Watch и WatchEffect.
Как работают эти 2 метода можно узнать из великолепной документации Vue, а в этой статье мы посмотрим на примеры самых часто используемых компонентов вместе с наблюдателями — по 2 компонента на каждый метод — а заодно вы сможете больше понять принцип их работы.
По ходу статьи также будут приведены некоторые полезные библиотеки Vue, которые часто используются в разработке.
Watch
Поисковая строка
Начнем с более простого примера.
Первым делом давайте создадим простой компонент, который будет содержать поле для ввода (обычный инпут) и с помощью v-model
привяжем к нему реактивную переменную:
<script setup>
import { ref } from 'vue';
const search = ref('');
</script>
<template>
<input v-model="search" type="text" placeholder="Поиск по сайту">
</template>
Если вы не знакомы с работой v-model
, то об этом можете почитать в документации.
Теперь самое интересное — это логика работы поиска: при изменении строки поиска будет выполняться запрос к серверу с соответствующим значением. В качестве сервера с данными будем использовать JSONPlaceholder с адресом сервера, где search — поисковой запрос:
https://jsonplaceholder.typicode.com/posts?q={search}
При вводе символов в поисковую строку необходимо создавать запрос на сервер и сохранять данные в реактивное значение для их последующего отображения.
Напишем в скрипте функцию для запроса данных fetchPosts(val)
, которая состоит из одного параметра, в который будет попадать обновленное значение поиска:
const fetchPosts = async (val) => {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?q=${val}`);
if (!response.ok) {
throw new Error('Что-то пошло не так');
}
const data = await response.json();
return data;
} catch (e) {
console.error(e);
return [];
}
};
Будем вызывать эту функцию каждый раз, когда мы вводим новое значение и выводить полученные данные в консоль. Здесь нам и нужен наблюдатель за значением поиска для выполнения побочного действия. Используем метод Watch,
т.к. отслеживаем всего одно значение. Первым аргументом будет отслеживаемое значение (без обращения к search.value
, Vue сделает это за нас), а вторым аргументом выступает колбэк‑функция, первым параметром которой будет обновленное значение поиска:
watch(search, async (newVal) => {
const data = await fetchPosts(newVal);
console.log(data);
});
Поиск готов, при каждом изменении символа в строке поиска вызывается функция fetchPosts()
, а затем данные выводятся в консоль.
Необходимо также позаботиться о том, чтобы не выполнять запросы через каждый введенный символ, а только тогда, когда пользователь закончит печатать. В ином случае будет отсылаться очень много запросов на сервер, что может ухудшить его производительность.

Для этого используем отложенный наблюдатель WatchDebounced
вместо обычного Watch
, который содержит библиотека утилит VueUse. Эта библиотека используется в большинстве проектов Vue. Достаточно настроить время задержки:
watchDebounced(search, async (newVal) => {
const data = await fetchPosts(newVal);
console.log(data);
}, { debounce: 800, maxWait: 1600 });
Теперь при длинном значении поиска вместо запроса на каждый изменённый символ, как на предыдущем изображении, мы обойдемся меньшим количеством запросов на итоговое значение поиска и намного сократим число запросов к серверу:

Как можете заметить количество запросов поиска сократилось практически до 1 вместо 34. Очень заметная оптимизация. Двигаемся к следующему примеру.
Валидация форм
Наблюдатели также часто используются при валидации форм без использования сторонних библиотек.
Составим шаблон простой формы для подписки на рассылку, состоящей из 3-х полей: имя, почта и чекбокс о согласии с политикой обработки ПД:
<template>
<form>
<label for="name">Имя</label>
<input type="text" id="name" name="name" placeholder="Введите имя">
<span>Текст ошибки</span>
<label for="email">Email</label>
<input type="email" id="email" name="email" placeholder="example@gmail.com">
<span>Текст ошибки</span>
<input type="checkbox" id="agreement" name="agreement">
<label for="agreement">Согласен с политикой обрабоки <a href="#">персональных данных</a></label>
<span>Текст ошибки</span>
<button type="submit">Отправить</button>
</form>
</template>
Создадим реактивный объект, содержащий значения всех полей и сообщений об ошибках:
<script setup>
import { reactive } from 'vue';
const fields = reactive({
name: { value: '', error: '' },
email: { value: '', error: '' },
agreement: { value: false, error: '' }
});
</script>
Не забудем также привязать поля формы к соответствующим полям реактивного объекта с помощью v-model
и распределить их ошибки:
<template>
<form @submit.prevent="handleSubmit">
<label for="name">Имя</label>
<input v-model="fields.name.value" type="text" id="name" name="name" placeholder="Введите имя">
<span v-if="fields.name.error">{{ fields.name.error }}</span>
<label for="email">Email</label>
<input v-model="fields.email.value" type="email" id="email" name="email" placeholder="example@gmail.com">
<span v-if="fields.email.error">{{ fields.email.error }}</span>
<input v-model="fields.agreement.value" type="checkbox" id="agreement" name="agreement">
<label for="agreement">Согласен с политикой обрабоки <a href="#">персональных данных</a></label>
<span v-if="fields.agreement.error">{{ fields.agreement.error }}</span>
<button type="submit">Отправить</button>
</form>
</template>
Перед тем, как перейти к разработке валидации, создадим базовую функцию проверки ошибок и отправки данных формы. С помощью computed
создадим вычисляемое значение hasErrors,
в котором проходит проверка всех значений объекта fields
на наличие ошибок:
const hasErrors = computed(() => {
return Object.values(fields).some(field => field.error);
});
И напишем обработчик отправки формы, который просто выводит в консоль сообщение об успешной отправке, если нет ни одной ошибки формы, а затем сбрасывает все значения формы до исходного состояния:
const handleSubmit = async () => {
if (hasErrors.value) return;
try {
console.log('Форма отправлена:', {
name: fields.name.value,
email: fields.email.value,
agreement: fields.agreement.value
});
fields.name = { value: '', error: '' };
fields.email = { value: '', error: '' };
fields.agreement = { value: false, error: '' }
} catch (error) {
console.error('Ошибка отправки формы:', error);
}
};
Теперь самое интересное. Нужно наблюдать за каждым полем формы и при его изменении проводить валидацию. Но есть один нюанс – Watch
не умеет отслеживать вложенные поля объектов, которые передаются напрямую. В этом случае следует применить функцию-getter, которая вернет необходимое значение. Посмотрим на пример валидации поля имени:
// ✅ Правильный способ – используется функция-getter
watch(() => fields.name.value, (newName) => {
fields.name.error = newName.trim() ? '' : 'Обязательное поле';
});
// ❌ Такой способ не сработает. Vue не сможет отследить переданное значение
watch(fields.name.value, (newName) => {
fields.name.error = newName.trim() ? '' : 'Обязательное поле';
});
Выше мы также следим за новым значением имени и у реактивного объекта fields
устанавливаем соответствующее сообщение об ошибке.
Похожим образом добавим валидацию остальных полей:
// Валидация email
watch(() => fields.email.value, (newEmail) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
fields.email.error = !newEmail.trim()
? 'Обязательное поле'
: !emailRegex.test(newEmail) ? 'Некорректный email' : '';
});
// Валидация соглашения
watch(() => fields.agreement.value, (newAgreement) => {
fields.agreement.error = newAgreement ? '' : 'Требуется согласие';
});
Последний момент, который стоит учесть, это проблема при отправке формы, как только компонент формы появился на странице. Дело в том, что Watch
не вызывается при создании компонента, и форма будет отправлена даже с неправильными значениями, т.к. ошибок полей по умолчанию нет.
Есть 2 возможных решения. Первый способ заключается в передаче третьего аргумента с параметрами в наблюдатель. Чтобы данный наблюдатели сработали в момент появления компонента, используется опция immediate
со значение true
:
watch(() => fields.name.value, (newName) => {
...
}, { immediate: true });
// ----
watch(() => fields.email.value, (newEmail) => {
...
}, { immediate: true });
// ----
watch(() => fields.agreement.value, (newAgreement) => {
...
}, { immediate: true });
Теперь форма валидируется сразу, как только пользователь ее увидит. Но такой способ выглядит нелогично, ведь ошибки отображаются всегда. Поэтому воспользуемся следующим способом, а этот останется только в качестве демонстрации.
Второй способ заключается в том, чтобы вынести логику валидации полей в отдельные функции и вызывать их также при отправке формы. Итоговый компонент:
<script setup>
import { reactive, computed, watch } from 'vue';
const fields = reactive({
name: { value: '', error: '' },
email: { value: '', error: '' },
agreement: { value: false, error: '' }
});
const hasErrors = computed(() => {
return Object.values(fields).some(field => field.error);
});
const validateName = (field) => {
fields.name.error = field.trim() ? '' : 'Обязательное поле';
}
const validateEmail = (field) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
fields.email.error = !field.trim()
? 'Обязательное поле'
: !emailRegex.test(field)
? 'Некорректный email'
: '';
}
const validateAgreement = (field) => {
fields.agreement.error = field ? '' : 'Требуется согласие';
}
const validateAll = () => {
validateName(fields.name.value);
validateEmail(fields.email.value);
validateAgreement(fields.agreement.value);
}
const handleSubmit = async () => {
validateAll();
if (hasErrors.value) return;
try {
console.log('Форма отправлена:', {
name: fields.name.value,
email: fields.email.value,
agreement: fields.agreement.value
});
fields.name = { value: '', error: '' };
fields.email = { value: '', error: '' };
fields.agreement = { value: false, error: '' }
} catch (error) {
console.error('Ошибка отправки формы:', error);
}
};
// Валидация имени
watch(() => fields.name.value, (newName) => {
validateName(newName);
});
// Валидация email
watch(() => fields.email.value, (newEmail) => {
validateEmail(newEmail);
});
// Валидация соглашения
watch(() => fields.agreement.value, (newAgreement) => {
validateAgreement(newAgreement);
});
</script>
<template>
<form @submit.prevent="handleSubmit">
<label for="name">Имя</label>
<input v-model="fields.name.value" type="text" id="name" name="name" placeholder="Введите имя">
<span v-if="fields.name.error">{{ fields.name.error }}</span>
<label for="email">Email</label>
<input v-model="fields.email.value" type="email" id="email" name="email" placeholder="example@gmail.com">
<span v-if="fields.email.error">{{ fields.email.error }}</span>
<input v-model="fields.agreement.value" type="checkbox" id="agreement" name="agreement">
<label for="agreement">Согласен с политикой обрабоки <a href="#">персональных данных</a></label>
<span v-if="fields.agreement.error">{{ fields.agreement.error }}</span>
<button type="submit">Отправить</button>
</form>
</template>
Таким простым образом мы можем подключить живую валидацию формы без использования сторонних библиотек.
Также существуют очень полезные библиотеки для валидации форм. Их стоит использовать для форм с более сложной логикой и большим количеством полей. С ними стоит обязательно познакомиться, т.к. в проектах они используются достаточно часто: VeeValidate и, если используете TypeScript, ZOD схемы. Обе библиотеки отлично работают в связке друг с другом.
WatchEffect
Наблюдатель WatchEffect
очень похож на Watch
, но с рядом отличий:
Не принимает аргумент с отслеживаемой переменной. Вместо этого, он отслеживает изменения всех реактивных значений, которые читаются внутри функции-колбека этого наблюдателя. То есть чаще всего этот метод используется для наблюдения за несколькими значениями одновременно.
В отличие от
Watch
,WatchEffect
срабатывает сразу при создании компонента и это поведение нельзя изменить
Если попытаться привести Watch
к такому же поведению, что и WatchEffect
, то получится следующее:
// Не задаем параметров и не передаем отслеживаемое значение
watchEffect(() => {
...
});
// Передаем отслеживаемое значение первым аргументом, а также объект
// с параметрами
watch(reactiveValue, () => {
...
}, { deep: true, immediate: true })
Для того, чтобы наглядно показать разницу и возможности WatchEffect
, посмотрим на предыдущий пример с реализацией валидации форм.
Валидация форм
В прошлом примере мы использовали три раздельных наблюдателя за каждым полем. С помощью WatchEffect
мы можем легко объединить их.
Вместо такого кода:
// Валидация имени
watch(() => fields.name.value, (newName) => {
validateName(newName);
});
// Валидация email
watch(() => fields.email.value, (newEmail) => {
validateEmail(newEmail);
});
// Валидация соглашения
watch(() => fields.agreement.value, (newAgreement) => {
validateAgreement(newAgreement);
});
Получаем следующий:
watchEffect(() => {
validateName(fields.name.value);
validateEmail(fields.email.value);
validateAgreement(fields.agreement.value);
});
Код получился простым, но теперь при изменении любого поля проходит валидация сразу всей формы, а также валидация срабатывает при создании компонента из-за особенностей WatchEffect
.
Исправим проблему с валидацией всей формы при редактировании одного значения. Для этого заведем обычную переменную за пределами наблюдателя, которая будет хранить предыдущие значения формы:
const prevValues = {
name: '',
email: '',
agreement: false,
};
Затем добавим в наблюдатель проверку текущих значений формы с предыдущими. Если есть разница, значит было изменено определенное поле и именно у него выполняем валидацию. В конце не забудем поменять данные предыдущих значений:
watchEffect(() => {
if (fields.name.value !== prevValues.name) validateName(fields.name.value);
if (fields.email.value !== prevValues.email) validateEmail(fields.email.value);
if (fields.agreement.value !== prevValues.agreement) validateAgreement(fields.agreement.value);
prevValues.name = fields.name.value;
prevValues.email = fields.email.value;
prevValues.agreement = fields.agreement.value;
});
Таким способом мы убили сразу двух зайцев: валидируется только определенное поле при изменении, а также валидация не производится при создании компонента.
Код выше срабатывает из-за того, что мы явно прочитали реактивные значения объекта
fields
Перейдем к последнему компоненту.
Фильтрация товаров
Компонент связан с множественным выборов параметров, которые будут влиять на отображение товаров. Ниже приведен готовый компонент с данными о товарах и логикой их отображения:
<script setup>
import { ref } from 'vue'
// Исходный список товаров
const items = ref([
{ id: 1, name: 'Ноутбук', category: 'Электроника', price: 80000 },
{ id: 2, name: 'Кроссовки', category: 'Одежда', price: 6000 },
{ id: 3, name: 'Смартфон', category: 'Электроника', price: 50000 },
{ id: 4, name: 'Книга', category: 'Книги', price: 800 },
{ id: 5, name: 'Футболка', category: 'Одежда', price: 1200 },
{ id: 6, name: 'Холодильник', category: 'Бытовая техника', price: 45000 },
{ id: 7, name: 'Сковорода', category: 'Кухня', price: 2500 },
{ id: 8, name: 'Чайник', category: 'Бытовая техника', price: 3000 },
{ id: 9, name: 'Пылесос', category: 'Бытовая техника', price: 15000 },
{ id: 10, name: 'Рюкзак', category: 'Аксессуары', price: 3500 }
])
// Категории для селекта
const categories = [...new Set(items.value.map(i => i.category))]
</script>
<template>
<div>
<h1>Список товаров</h1>
<!-- Список товаров -->
<div>
<div v-for="item in items" :key="item.id">
<h2>{{ item.name }}</h2>
<p>Категория: {{ item.category }}</p>
<p>Цена: {{ item.price }}₽</p>
</div>
<p v-if="items.length === 0">Нет товаров по заданным критериям</p>
</div>
</div>
</template>
Кратко, что здесь происходит:
Массив товаров items состоит из продуктов. Каждый объект продукта имеет название, категорию и цену.
С помощью цикла
v-for
динамически отображаются все продукты на странице.Если элементов для отображения нет, то выводит сообщение об этом.
Теперь добавим шаблон формы и реактивный объект, к которому мы привяжем поля фильтров с помощью v-model
:
<script setup>
// ...
const filters = reactive({
name: '',
category: '',
maxPrice: null,
});
</script>
<template>
<div>
<input v-model="filters.name" type="text" placeholder="Поиск по названию" />
<select v-model="filters.category">
<option value="">Все категории</option>
<option v-for="cat in categories" :key="cat" :value="cat">{{ cat }}</option>
</select>
<input v-model.number="filters.maxPrice" type="number" placeholder="Макс. цена" />
</div>
// ...
</template>
Также заметьте, что в привязке цены был использован модификатор .number
, который преобразовывает значение в число (по умолчанию в значение попадает строка, несмотря на тип поля number
).
Теперь давайте создадим новое реактивное значение, которое будет хранить отфильтрованные товары и с помощью WatchEffect будем отслеживать, что если изменился любой фильтр, то будем проходить по исходным задачам и выбирать те из них, что соответствуют критериям:
const filteredItems = ref([])
watchEffect(() => {
filteredItems.value = items.value.filter(item => {
const matchName = item.name.toLowerCase().includes(filters.name.toLowerCase())
const matchCategory = filters.category ? item.category === filters.category : true
const matchPrice = filters.maxPrice ? item.price <= filters.maxPrice : true
return matchName && matchCategory && matchPrice
})
})
В коде выше мы явно читаем поля реактивного объекта filters
, отчего watchEffect срабатывает каждый раз, при изменении любого фильтра.
Осталось лишь подменить items
на filteredItems
в шаблоне:
<div>
<div v-for="item in filteredItems" :key="item.id">
<h2>{{ item.name }}</h2>
<p>Категория: {{ item.category }}</p>
<p>Цена: {{ item.price }}₽</p>
</div>
<p v-if="filteredItems.length === 0">Нет товаров по заданным критериям</p>
</div>
Теперь фильтрация полностью готова. Но давайте посмотрим на то, как мы можем улучшить код.
Сейчас мы внутри WatchEffect
меняем значение одной реактивной переменной – filteredItems. Vue поддерживает встроенный метод computed
, напоминающий watchEffect
, с тем отличием, что computed
обязательно должен возвращать какое-либо значение и записывать его в переменную. Получится следующее:
// Было
const filteredItems = ref([])
watchEffect(() => {
filteredItems.value = items.value.filter(item => {
...
})
})
// Cтало
const filteredItems = computed(() => items.value.filter(item => {
...
}))
Таким образом сделаем вывод:
Если
watchEffect
своей логикой изменяет значение одной реактивной переменной, то его можно заменить с помощьюcomputed
, сразу вернув значение в нужную переменную, которая при этом останется реактивной.
Теперь вы знаете
В каких случаях применять
watch
иwatchEffect
.Способы реализации компонентов поиска, валидации форм и фильтрации товаров по множественным фильтрам.
Несколько полезных библиотек, облегчающих процесс разработки.
К комментариях смело указывайте, как бы Вы улучшили представленные в статье компоненты.
Если статья была Вам полезна, можете также посетить мой Телеграм канал, в котором найдете другую полезную для себя информацию – https://t.me/sanwed_it