What Causes This Error?
This error occurs when a reactive effect (like watchEffect or computed) modifies a value that triggers itself to run again indefinitely.
The Problem
import { ref, watchEffect } from 'vue'
const count = ref(0)
// ❌ Effect modifies what it reads
watchEffect(() => {
console.log(count.value)
count.value++ // Triggers effect again!
})
The Fix
Avoid Self-Modification
const count = ref(0)
const logCount = ref(0)
// ✅ Read and write different values
watchEffect(() => {
logCount.value = count.value
console.log('Count:', count.value)
})
Use Watch for Reactions
const count = ref(0)
// ✅ Watch doesn't auto-track
watch(count, (newVal) => {
console.log('Count changed to:', newVal)
// Safe to modify other state here
})
Common Scenarios
Counter with Effect
// ❌ Infinite loop
watchEffect(() => {
count.value = count.value + 1
})
// ✅ Use interval instead
onMounted(() => {
const interval = setInterval(() => {
count.value++
}, 1000)
onUnmounted(() => clearInterval(interval))
})
Transforming Data
// ❌ Infinite loop
const data = ref([1, 2, 3])
watchEffect(() => {
data.value = data.value.map(x => x * 2)
})
// ✅ Use computed for transformations
const doubledData = computed(() =>
data.value.map(x => x * 2)
)
Syncing State
// ❌ Infinite loop - each modifies the other
const a = ref(0)
const b = ref(0)
watchEffect(() => {
b.value = a.value + 1
})
watchEffect(() => {
a.value = b.value - 1
})
// ✅ One-way sync only
watch(a, (val) => {
b.value = val + 1
})
Form Normalization
// ❌ Infinite loop
const email = ref('')
watchEffect(() => {
email.value = email.value.toLowerCase()
})
// ✅ Use watch with guard
watch(email, (val) => {
const lower = val.toLowerCase()
if (email.value !== lower) {
email.value = lower
}
})
// ✅ Or use computed setter
const normalizedEmail = computed({
get: () => email.value,
set: (val) => { email.value = val.toLowerCase() }
})
Recursive Updates
// ❌ Creates infinite updates
const tree = reactive({
children: []
})
watchEffect(() => {
tree.children.push({ id: tree.children.length })
})
// ✅ Use explicit triggers
function addChild() {
tree.children.push({ id: tree.children.length })
}
Using flush Options
// Sometimes helps avoid loops
watch(source, callback, {
flush: 'sync' // Be careful with this
})
// Post flush for DOM updates
watch(source, callback, {
flush: 'post'
})
Quick Checklist
- Effects should read values, not modify them
- Use
watchinstead ofwatchEffectfor reactions - Use
computedfor derived values - Add guards to prevent redundant updates
- Avoid circular dependencies between watchers
- Keep reactivity unidirectional when possible