Avoiding Infinite Watch Loops in Vue Production Builds

Dr. Adam Nielsen
2 min readJan 22, 2025

--

Vue.js reactivity is powerful, but subtle differences between development (npm run dev) and production (npm run build) builds can cause unexpected issues like infinite loops in watch setups. Here’s a quick guide to understanding and fixing this problem.

The Problem: Infinite Loop in Production

A Vue component with v-model binding to props.modelValue and watchers to sync a local items array and emit changes can create an infinite loop in production:

Example

<template>
<div>
<ul>
<li v-for="(item, index) in items" :key="index">{{ item }}</li>
</ul>
<button @click="addItem">Add Item</button>
</div>
</template>

<script setup>
import { ref, watch } from "vue";
const props = defineProps({
modelValue: Array,
});
const emit = defineEmits(["update:modelValue"]);
const items = ref([...props.modelValue]);
watch(() => props.modelValue, (newValue) => {
items.value = [...newValue];
}, { immediate: true });
watch(items, (newValue) => {
emit("update:modelValue", newValue);
});
function addItem() {
items.value.push(`Item ${items.value.length + 1}`);
}
</script>

Why It Happens

In production, Vue optimizes reactivity and tracks references more aggressively. In development mode, Vue allows more leniency with reactivity checks and doesn’t strictly enforce optimizations, which is why these loops might not occur during local testing. This leads to:

  • watch(props.modelValue) updating items.
  • watch(items) emitting update:modelValue, which triggers the parent to update props.modelValue, creating a loop.

Fixing the Loop

Choose one of the following solutions depending on your use case:

Option 1: Use Conditional Updates

Prevent redundant updates by comparing values:

watch(() => props.modelValue, (newValue) => {
if (JSON.stringify(newValue) !== JSON.stringify(items.value)) {
items.value = [...newValue];
}
}, { immediate: true });

watch(items, (newValue) => {
if (JSON.stringify(newValue) !== JSON.stringify(props.modelValue)) {
emit("update:modelValue", newValue);
}
});

Option 2: Emit Updates Directly

Emit updates only when modifying items, eliminating the need for one watch function:

function addItem() {
items.value.push(`Item ${items.value.length + 1}`);
emit("update:modelValue", items.value);
}

Key Takeaways

  1. Production Reactivity Differences: These can expose issues hidden in development.
  2. Avoid Overwatching: Use conditional logic or direct emissions to avoid loops.
  3. Simplify: Emit updates only when necessary.

By managing reactivity carefully, you can prevent infinite loops and ensure stable, performant Vue applications. Share your experiences in the comments!

--

--

Dr. Adam Nielsen
Dr. Adam Nielsen

Written by Dr. Adam Nielsen

PHD in math. and Laravel / Vue Full-Stack-Developer

No responses yet