DEV Community

Cover image for 10 Common Vue.js Mistakes and How to Avoid Them
Gervais Yao Amoah
Gervais Yao Amoah

Posted on

10 Common Vue.js Mistakes and How to Avoid Them

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>
Enter fullscreen mode Exit fullscreen mode

✅ Correct:

<li v-for="item in items" :key="item.id">{{ item.name }}</li>
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

This watcher will not react to nested changes.

✅ Correct:

watch(() => formData, (newVal) => console.log(newVal), { deep: true })
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

✅ Correct:

const data = useFetchUserData()
if (user.value.isLoggedIn) {
  // use data conditionally instead of declaring it conditionally
}
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

You can then emit updates to the parent when necessary:

watch(userLocal, (newVal) => emit('update:user', newVal), { deep: true })
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

✅ Correct:

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

name and age are now plain variables, not reactive.

✅ Correct:

const state = reactive({ name: 'John', age: 30 })
const { name, age } = toRefs(state)
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

✅ Correct:
If you need to replace the entire reference, use ref() instead:

const state = ref({})
state.value = { ...newState }
Enter fullscreen mode Exit fullscreen mode

Or if using reactive(), mutate properties instead of replacing the object:

Object.assign(state, newState)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)