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

4.7 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Choose watch vs watchEffect Based on Dependency Control Needs MEDIUM Wrong choice leads to unnecessary re-runs or missed dependency tracking efficiency
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:

<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:

<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

<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

<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

<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

<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