Fix: Infinite Loop in Effect in Vue.js

Error message:
Detected infinite loop in effect.
Reactivity System 2025-01-25

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 watch instead of watchEffect for reactions
  • Use computed for derived values
  • Add guards to prevent redundant updates
  • Avoid circular dependencies between watchers
  • Keep reactivity unidirectional when possible