As Vue.js continues to dominate the front-end ecosystem, many developers (even experienced ones) still fall into common traps that can lead to poor performance, reactivity issues, and maintainability headaches. Whether you’re building small components or large-scale enterprise applications, understanding these mistakes can drastically improve your code quality and performance.
In this article, we’ll go through 10 of the most common Vue.js mistakes, explain why they happen, and show how to fix them properly.
1. Omitting the key
Attribute or Using Index in v-for
One of the most overlooked issues in Vue.js is the improper use of the key
attribute within v-for
loops.
Using the index as the key or omitting it entirely can lead to unexpected rendering behavior and performance issues. Vue relies on key
to track elements efficiently between re-renders. Without a unique identifier, Vue may mistakenly reuse DOM elements, leading to bugs like incorrect state retention between list items.
❌ Wrong:
<li v-for="(item, index) in items" :key="index">{{ item.name }}</li>
✅ Correct:
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
Always use a unique, stable identifier from your data, such as an id
or uuid
.
2. Prop Drilling Instead of Using Provide/Inject or Global State
When components become deeply nested, developers often fall into prop drilling, passing props down multiple layers just to reach a deeply nested child component. This approach quickly becomes hard to maintain and error-prone.
Instead, leverage Vue’s provide/inject API or global state management solutions like Pinia or Vuex.
✅ Use Provide/Inject Example:
// Parent
provide('user', userData)
// Child
const user = inject('user')
For larger applications, centralized state management improves scalability and debugging.
3. Watching Arrays and Objects Incorrectly
Vue’s reactivity system doesn’t deeply track changes inside nested objects or arrays unless explicitly told to. Developers often make the mistake of setting up watchers without the { deep: true }
option.
❌ Wrong:
watch(() => formData, (newVal) => console.log(newVal))
This watcher will not react to nested changes.
✅ Correct:
watch(() => formData, (newVal) => console.log(newVal), { deep: true })
The deep
option ensures Vue watches every nested property, making it essential for complex forms or nested data structures.
4. Calling Composables in the Wrong Place
With the Composition API, composables (useSomething()
) are an essential pattern for reusing logic. However, calling them conditionally or inside loops breaks Vue’s reactivity tracking and lifecycle handling.
❌ Wrong:
if (user.value.isLoggedIn) {
const data = useFetchUserData()
}
✅ Correct:
const data = useFetchUserData()
if (user.value.isLoggedIn) {
// use data conditionally instead of declaring it conditionally
}
Always call composables at the top level of the setup()
function, not inside conditions or loops.
You can also call a composable inside another composable, as long as it is at the top level.
5. Mutating Props Directly
One of the most common Vue.js beginner mistakes is mutating props directly. Props are read-only and designed for one-way data flow from parent to child.
When you modify a prop inside a child component, Vue will warn you, and for good reason. It can cause unpredictable state changes and hard-to-debug behavior.
✅ Correct Solution: Create a local copy of the prop and modify that.
const props = defineProps(['user'])
const userLocal = ref({ ...props.user })
You can then emit updates to the parent when necessary:
watch(userLocal, (newVal) => emit('update:user', newVal), { deep: true })
This preserves the unidirectional data flow and keeps your state predictable.
6. Forgetting to Clean Up Manual Event Listeners
Vue automatically handles event bindings declared in templates, but when you manually add event listeners (e.g., using window.addEventListener
), you must also manually remove them to prevent memory leaks.
❌ Wrong:
onMounted(() => {
window.addEventListener('resize', handleResize)
})
✅ Correct:
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
Neglecting cleanup can cause performance degradation and unexpected behavior over time.
7. Expecting Non-Reactive Dependencies to Trigger Updates
Developers sometimes assume that computed properties or watchers will automatically react to all dependencies. However, Vue only tracks reactive sources.
If a computed property relies on a non-reactive variable, it won’t trigger updates when that variable changes.
✅ Tip: Wrap all reactive sources in ref()
or reactive()
so Vue can track them properly.
const count = ref(0)
const double = computed(() => count.value * 2)
Ensure your computed logic is based solely on reactive data, not plain JavaScript variables.
8. Destructuring Reactive Data Without toRefs
Destructuring from a reactive
object can break reactivity, since Vue loses track of the original proxy references.
❌ Wrong:
const state = reactive({ name: 'John', age: 30 })
const { name, age } = state
name
and age
are now plain variables, not reactive.
✅ Correct:
const state = reactive({ name: 'John', age: 30 })
const { name, age } = toRefs(state)
Using toRefs()
ensures that reactivity is preserved after destructuring, maintaining proper re-renders.
9. Replacing Reactive State Incorrectly
Vue’s reactivity system cannot track entire object replacements when using reactive()
. Developers often reassign the whole object, unintentionally breaking reactivity.
❌ Wrong:
state = { ...newState }
✅ Correct:
If you need to replace the entire reference, use ref()
instead:
const state = ref({})
state.value = { ...newState }
Or if using reactive()
, mutate properties instead of replacing the object:
Object.assign(state, newState)
This ensures the component stays reactive and updates correctly in the DOM.
10. Manual DOM Manipulation Instead of Using Template Refs
Vue is built to abstract away DOM manipulation. Directly touching the DOM with document.querySelector()
or innerHTML
can lead to inconsistent UI updates and break reactivity.
If you absolutely need to access a DOM element, use template refs.
✅ Example:
<template>
<div ref="myDiv"></div>
</template>
<script setup>
const myDiv = ref(null)
onMounted(() => {
myDiv.value.focus()
})
</script>
This approach respects Vue’s lifecycle and ensures you interact with elements only after they’ve been mounted.
Final Thoughts
Avoiding these common Vue.js mistakes will help you write cleaner, more maintainable, and bug-free applications. Understanding how Vue’s reactivity system, props, and lifecycle hooks work under the hood is the key to mastering it.
By following best practices like using toRefs
, cleaning up listeners, and respecting unidirectional data flow, you’ll ensure your app remains performant and easy to debug, even as it grows in complexity.
Top comments (0)