Files
agent-skills/skills/vue-best-practices/reference/watch-vs-watcheffect.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

187 lines
4.7 KiB
Markdown

---
title: Choose watch vs watchEffect Based on Dependency Control Needs
impact: MEDIUM
impactDescription: Wrong choice leads to unnecessary re-runs or missed dependency tracking
type: efficiency
tags: [vue3, watch, watchEffect, watchers, reactivity, best-practices]
---
# Choose watch vs watchEffect Based on Dependency Control Needs
**Impact: MEDIUM** - Using `watch` when `watchEffect` would be cleaner leads to repetitive code. Using `watchEffect` when `watch` is needed can cause unexpected re-runs or missed dependencies (especially with async).
Use `watchEffect` for simple cases where the callback uses the same state as what should trigger it. Use `watch` when you need precise control over what triggers the callback, access to old values, or lazy execution.
## Task Checklist
- [ ] Use `watchEffect` when callback logic uses the same state it should react to
- [ ] Use `watch` when you need old value comparison
- [ ] Use `watch` when you need lazy execution (not immediate)
- [ ] Use `watch` for async callbacks with dependencies after await
- [ ] Use `watch` when callback should not run on initial mount
## Comparison Table
| Feature | `watch` | `watchEffect` |
|---------|---------|---------------|
| Dependency tracking | Explicit (you specify) | Automatic (uses accessed properties) |
| Lazy by default | Yes (runs only on change) | No (runs immediately) |
| Access old value | Yes | No |
| Async dependency tracking | Full control | Only before first await |
| Multiple sources | Array syntax | Automatic |
**When to prefer `watchEffect`:**
```vue
<script setup>
import { ref, watchEffect } from 'vue'
const todoId = ref(1)
const data = ref(null)
// GOOD: watchEffect is cleaner when callback uses same state
watchEffect(async () => {
const response = await fetch(
`https://api.example.com/todos/${todoId.value}`
)
data.value = await response.json()
})
</script>
```
**When to prefer `watch`:**
```vue
<script setup>
import { ref, watch } from 'vue'
const todoId = ref(1)
const data = ref(null)
// BETTER with watch when:
// 1. You need old value
watch(todoId, (newId, oldId) => {
console.log(`Changed from ${oldId} to ${newId}`)
})
// 2. You don't want immediate execution
watch(todoId, () => {
// Only runs when todoId changes, not on mount
fetchData()
})
// 3. You have dependencies after await
watch(todoId, async (id) => {
const response = await fetch(`/api/todos/${id}`)
// More reactive access here still triggers correctly
// because we explicitly specified todoId as the source
})
</script>
```
## Avoid Redundant Code with watchEffect
```vue
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const category = ref('all')
const results = ref([])
// BAD: Repetitive - listing same deps in source and using in callback
watch(
[searchQuery, category],
([query, cat]) => {
fetchResults(query, cat) // Same variables repeated
}
)
// GOOD: watchEffect removes repetition
watchEffect(() => {
fetchResults(searchQuery.value, category.value)
})
</script>
```
## Use watch for Lazy Behavior
```vue
<script setup>
import { ref, watch, watchEffect } from 'vue'
const userId = ref(null)
// BAD: Runs immediately even when userId is null
watchEffect(() => {
if (userId.value) {
loadUserProfile(userId.value)
}
})
// GOOD: Only runs when userId actually changes
watch(userId, (id) => {
if (id) {
loadUserProfile(id)
}
})
// ALSO GOOD: watch with immediate when you need both behaviors
watch(
userId,
(id) => {
if (id) loadUserProfile(id)
},
{ immediate: true } // Explicit about running immediately
)
</script>
```
## Use watch for Old Value Comparison
```vue
<script setup>
import { ref, watch } from 'vue'
const status = ref('pending')
// Only watch() provides old value
watch(status, (newStatus, oldStatus) => {
if (oldStatus === 'pending' && newStatus === 'approved') {
showApprovalNotification()
}
if (oldStatus === 'approved' && newStatus === 'rejected') {
showRejectionWarning()
}
})
</script>
```
## Use watch for Complex Async Dependencies
```vue
<script setup>
import { ref, watch } from 'vue'
const filters = ref({ status: 'active', sort: 'date' })
const page = ref(1)
const results = ref([])
// BETTER: watch with explicit sources for async
// All dependencies tracked regardless of await placement
watch(
[filters, page],
async ([currentFilters, currentPage]) => {
const data = await fetchWithFilters(currentFilters)
// These are still correctly tracked:
results.value = paginateResults(data, currentPage)
},
{ deep: true }
)
</script>
```
## Reference
- [Vue.js Watchers - watch vs. watchEffect](https://vuejs.org/guide/essentials/watchers.html#watch-vs-watcheffect)