Avoiding Infinite Watch Loops in Vue Production Builds
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)
updatingitems
.watch(items)
emittingupdate:modelValue
, which triggers the parent to updateprops.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
- Production Reactivity Differences: These can expose issues hidden in development.
- Avoid Overwatching: Use conditional logic or direct emissions to avoid loops.
- 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!