Fix: Maximum Recursive Updates Exceeded in Vue.js

Error message:
Maximum recursive updates exceeded.
Reactivity System 2025-01-25

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