[Medium] watch vs watchEffect
1. What are watch and watchEffect?
What are
watchandwatchEffect?
watch and watchEffect are Vue 3 APIs for reacting to changes in reactive state.
watch
Definition: explicitly watches one or more sources, and runs callback when they change.
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
const message = ref('Hello');
// watch a single source
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`);
});
// watch multiple sources
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
console.log('count or message changed');
});
</script>
watchEffect
Definition: runs immediately and automatically tracks reactive dependencies used inside its callback.
<script setup>
import { ref, watchEffect } from 'vue';
const count = ref(0);
const message = ref('Hello');
// auto-tracks count and message
watchEffect(() => {
console.log(`count: ${count.value}, message: ${message.value}`);
// re-runs when count/message changes
});
</script>
2. watch vs watchEffect: Key Differences
Main differences between
watchandwatchEffect
1. Source declaration
watch: explicit source(s).
const count = ref(0);
const message = ref('Hello');
watch(count, (newVal, oldVal) => {
console.log('count changed');
});
watch([count, message], ([newCount, newMessage]) => {
console.log('count or message changed');
});
watchEffect: implicit dependency tracking.
const count = ref(0);
const message = ref('Hello');
watchEffect(() => {
console.log(count.value); // tracked automatically
console.log(message.value); // tracked automatically
});
2. Execution timing
watch: lazy by default; runs only after source changes.
const count = ref(0);
watch(count, (newVal) => {
console.log('run');
});
count.value = 1; // triggers callback
watchEffect: runs immediately, then re-runs on dependency updates.
const count = ref(0);
watchEffect(() => {
console.log('run'); // immediate first run
console.log(count.value);
});
count.value = 1; // runs again
3. Access to old value
watch: gives newValue and oldValue.
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(`from ${oldVal} to ${newVal}`);
});
watchEffect: no direct old value.
const count = ref(0);
watchEffect(() => {
console.log(count.value); // current value only
});
4. Stopping watchers
Both return a stop function.
const stopWatch = watch(count, (newVal) => {
console.log(newVal);
});
const stopEffect = watchEffect(() => {
console.log(count.value);
});
stopWatch();
stopEffect();
3. When to use watch vs watchEffect?
When should you choose each API?
Use watch when
- You need explicit sources.
watch(userId, (newId) => {
fetchUser(newId);
});
- You need old value.
watch(count, (newVal, oldVal) => {
console.log(`from ${oldVal} to ${newVal}`);
});
- You need lazy execution.
watch(searchQuery, (newQuery) => {
if (newQuery.length > 2) {
search(newQuery);
}
});
- You need fine-grained control (
immediate,deep, etc.).
watch(
() => user.value.id,
(newId) => {
fetchUser(newId);
},
{ immediate: true, deep: true }
);
Use watchEffect when
- You want automatic dependency tracking.
watchEffect(() => {
if (user.value && permissions.value.includes('admin')) {
loadAdminData();
}
});
- You do not need old value.
watchEffect(() => {
console.log(`current count: ${count.value}`);
});
- You want immediate first run.
watchEffect(() => {
updateChart(count.value, message.value);
});
4. Common Interview Questions
Common interview questions
Question 1: execution order
Explain output and order:
const count = ref(0);
const message = ref('Hello');
watch(count, (newVal) => {
console.log('watch:', newVal);
});
watchEffect(() => {
console.log('watchEffect:', count.value, message.value);
});
count.value = 1;
message.value = 'World';
Click to view answer
watch is lazy (no immediate run), but watchEffect runs immediately.
Expected sequence:
watchEffect: 0 Hello(initial run)watch: 1(count changed)watchEffect: 1 Hello(count changed)watchEffect: 1 World(message changed)
Key points:
watchonly reacts to explicitly watched sourcewatchEffectreacts to any reactive dependency used in callback
Question 2: old value with watchEffect
How do you access old value when using watchEffect?
Click to view answer
watchEffect does not provide old value directly.
Option 1: keep your own previous ref
const count = ref(0);
const prevCount = ref(0);
watchEffect(() => {
console.log(`from ${prevCount.value} to ${count.value}`);
prevCount.value = count.value;
});
Option 2: use watch
watch(count, (newVal, oldVal) => {
console.log(`from ${oldVal} to ${newVal}`);
});
Recommendation: if old value is required, prefer watch.
Question 3: choose watch or watchEffect
Choose API for each scenario:
// Scenario 1: reload user data when userId changes
const userId = ref(1);
// Scenario 2: enable submit when form is valid
const form = reactive({ username: '', password: '' });
const isValid = computed(() => form.username && form.password);
// Scenario 3: search with debounce on keyword changes
const searchQuery = ref('');
Click to view answer
Scenario 1: userId changes -> watch
watch(userId, (newId) => {
fetchUser(newId);
});
Scenario 2: form validity side effect -> watchEffect
watchEffect(() => {
submitButton.disabled = !isValid.value;
});
Scenario 3: debounced search -> watch
let timeoutId;
watch(searchQuery, (newQuery) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
search(newQuery);
}, 300);
});
Selection rule:
- explicit source / old value / control options ->
watch - auto dependency tracking + immediate run ->
watchEffect
5. Best Practices
Best practices
Recommended
// 1) explicit source -> watch
watch(userId, (newId) => {
fetchUser(newId);
});
// 2) auto track multiple dependencies -> watchEffect
watchEffect(() => {
if (user.value && permissions.value.includes('admin')) {
loadAdminData();
}
});
// 3) need old value -> watch
watch(count, (newVal, oldVal) => {
console.log(`from ${oldVal} to ${newVal}`);
});
// 4) cleanup
onUnmounted(() => {
stopWatch();
stopEffect();
});
Avoid
// 1) avoid unmanaged async side effects in watchEffect
watchEffect(async () => {
const data = await fetchData();
// potential race/leak if not managed
});
// 2) avoid overusing watchEffect
watchEffect(() => {
console.log(count.value); // watch(count, ...) may be clearer
});
// 3) avoid mutating tracked source in same effect (risk loops)
watchEffect(() => {
count.value++; // may cause infinite loop
});
6. Interview Summary
Interview summary
Quick memory
watch:
- explicit source declaration
- lazy by default
- old value available
- best for controlled scenarios
watchEffect:
- automatic dependency tracking
- immediate execution
- no old value
- best for concise reactive side effects
Rule of thumb:
- explicit control ->
watch - automatic tracking ->
watchEffect - old value needed ->
watch - immediate initial run ->
watchEffect
Sample answer
Q: What is the difference between watch and watchEffect?
Both react to reactive changes in Vue 3.
watchtracks explicitly declared sources and gives old/new values; it is lazy by default.watchEffectruns immediately and auto-tracks dependencies used inside the callback, but it does not provide old value. Usewatchfor precision and control; usewatchEffectfor automatic dependency collection.
Q: When should I use each?
Use
watchwhen you need explicit source control, old values, or options like debounce/deep/immediate. UsewatchEffectwhen you want an immediate run and automatic tracking across multiple related reactive values.