What Causes This Error?
This error occurs when a reactive effect triggers itself in an infinite loop. Vue detects that the same update is being triggered repeatedly and stops to prevent freezing the browser.
The Problem
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// ❌ Watch modifies the value it's watching
watch(count, (newVal) => {
count.value = newVal + 1 // Triggers the watch again!
})
</script>
<script setup>
import { ref, computed } from 'vue'
const items = ref([1, 2, 3])
// ❌ Computed modifies reactive state
const sortedItems = computed(() => {
items.value.sort() // Mutates original array!
return items.value
})
</script>
The Fix
Avoid Modifying Watched Values
const count = ref(0)
const doubledCount = ref(0)
// ✅ Modify a different value
watch(count, (newVal) => {
doubledCount.value = newVal * 2
})
Use Computed for Derived State
const count = ref(0)
// ✅ Computed doesn't modify, just derives
const doubledCount = computed(() => count.value * 2)
Don’t Mutate in Computed
const items = ref([1, 3, 2])
// ❌ Mutates original
const sorted = computed(() => {
items.value.sort()
return items.value
})
// ✅ Create new array
const sorted = computed(() => {
return [...items.value].sort()
})
Common Scenarios
Watch Triggering Itself
// ❌ Infinite loop
const form = ref({ name: '' })
watch(form, (newForm) => {
form.value = { ...newForm, updated: Date.now() }
}, { deep: true })
// ✅ Use separate tracking
const form = ref({ name: '' })
const lastUpdated = ref(null)
watch(form, () => {
lastUpdated.value = Date.now()
}, { deep: true })
Parent-Child Update Cycle
<!-- ❌ Parent updates child, child emits, parent updates... -->
<!-- Parent -->
<script setup>
const value = ref('')
watch(value, () => {
value.value = value.value.toUpperCase() // Triggers child update
})
</script>
<template>
<ChildInput v-model="value" />
</template>
<!-- ✅ Handle transformation in one place -->
<script setup>
const value = ref('')
function updateValue(newVal) {
value.value = newVal.toUpperCase()
}
</script>
<template>
<ChildInput :model-value="value" @update:model-value="updateValue" />
</template>
Reactive Array Operations
// ❌ In-place mutation in effect
const numbers = ref([3, 1, 2])
watchEffect(() => {
numbers.value.reverse() // Triggers effect again!
console.log(numbers.value)
})
// ✅ Create derived state
const numbers = ref([3, 1, 2])
const reversed = computed(() => [...numbers.value].reverse())
watchEffect(() => {
console.log(reversed.value) // Safe
})
Form Validation Loop
// ❌ Validation modifies the form
watch(form, () => {
if (!form.value.email.includes('@')) {
form.value.errors = ['Invalid email'] // Triggers watch!
}
})
// ✅ Separate errors from form data
const form = ref({ email: '' })
const errors = ref([])
watch(() => form.value.email, (email) => {
errors.value = email.includes('@') ? [] : ['Invalid email']
})
Computed with Side Effects
// ❌ Computed with side effect
const count = ref(0)
const doubled = computed(() => {
console.log('Computing...') // OK
count.value++ // ❌ Side effect!
return count.value * 2
})
// ✅ Pure computed
const doubled = computed(() => count.value * 2)
// If you need side effects, use watchEffect
watchEffect(() => {
console.log('Count changed:', count.value)
})
Using flush Options
// Sometimes helps with timing issues
watch(source, callback, {
flush: 'post' // Run after DOM updates
})
watch(source, callback, {
flush: 'sync' // Run synchronously (careful!)
})
Breaking the Cycle with Flags
const data = ref('')
let isUpdating = false
watch(data, (newVal) => {
if (isUpdating) return
isUpdating = true
data.value = newVal.trim()
isUpdating = false
})
Quick Checklist
- Don’t modify the value you’re watching
- Use computed for derived state (pure functions)
- Create new arrays/objects instead of mutating
- Separate input data from validation errors
- Avoid side effects in computed properties
- Use flags to prevent recursive updates if necessary
- Check for parent-child update cycles