vuedraggable Vue 3 Integration: Common Pitfalls and Best Practices
Successful vuedraggable Vue 3 integration requires reactive array references, stable unique keys for every item, and the modern v-model:list syntax to ensure Vue's patchKeyedChildren algorithm correctly reorders DOM nodes without remounting components.
Integrating drag-and-drop functionality into Vue 3 applications often leads to subtle reactivity bugs when developers misunderstand how the framework's renderer synchronizes DOM state with data. This guide examines how vuedraggable Vue 3 implementations interact with Vue's core reactivity system and keyed diffing algorithm to help you avoid common integration failures.
How vuedraggable Interacts with Vue 3 Core
vuedraggable works by synchronizing a reactive array with the order of DOM elements created by Vue's renderer. When the component renders, Vue's keyed-children diff algorithm (patchKeyedChildren in packages/runtime-core/src/renderer.ts) decides whether an element should be moved, inserted, or removed. If the array or keys are not configured correctly, Vue cannot compute minimal DOM updates, resulting in items disappearing, v-model bindings failing to emit, or unnecessary re-mounts that reset component state.
Critical Vue Core Files for vuedraggable Integration
| File | Role in vuedraggable Vue 3 Integration |
|---|---|
packages/runtime-dom/src/directives/vModel.ts |
Implements the v-model directive that stores the assigner function on the vnode and invokes it on input events. vuedraggable relies on this to push array updates back to parent components. |
packages/runtime-core/src/renderer.ts |
Contains the patchKeyedChildren algorithm that efficiently reorders DOM nodes based on stable keys—essential for drag-and-drop reordering without state loss. |
packages/reactivity/src/ref.ts |
Defines ref() which creates reactive containers for lists bound to vuedraggable. Without this reactivity, v-model updates fail silently. |
packages/runtime-core/src/compat/componentVModel.ts |
Compatibility layer for legacy v-model usage; demonstrates why the modern v-model:list syntax prevents prop naming confusion. |
packages/runtime-core/src/helpers/useModel.ts |
Helper for defineModel/useModel that normalizes prop names to modelValue, useful when building custom wrappers around vuedraggable. |
Common vuedraggable Vue 3 Pitfalls
Understanding how Vue core processes your data helps diagnose these frequent integration failures:
| Symptom | Root Cause | Vue Core Mechanism |
|---|---|---|
| List does not update after drag | Bound array is not a reactive ref/reactive object |
No reactive trigger means vModelDynamic never calls the assigner, so update:modelValue is not emitted |
| Items lose internal state (inputs reset) | Using loop index as :key |
patchKeyedChildren treats them as different nodes, unmounting old components and mounting fresh ones |
| Duplicate items appear | Mutating the same array reference inside a watcher or @change without creating a new array |
Vue sees the same reference, performs no diff, and appends instead of replacing |
v-model fires on every mouse move |
Binding directly to a non-array prop (e.g., v-model="dragged") |
The vModel directive listens to input events, but the payload type mismatch causes erratic behavior in vuedraggable |
Parent does not receive @update:modelValue |
Parent component does not declare emits: ['update:modelValue'] or uses old modelValue prop |
Vue's runtime-core validates emitted events; undeclared calls are ignored in development mode |
vuedraggable Vue 3 Best Practices
Follow these guidelines to align with Vue 3's reactivity and rendering model:
1. Declare Reactive Array References
Always wrap your list in ref() or reactive() to ensure the reactivity system tracks mutations:
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'First' },
{ id: 2, name: 'Second' },
])
This guarantees that vModelDynamic in packages/runtime-dom/src/directives/vModel.ts can invoke the assigner function when vuedraggable emits updates.
2. Use Stable, Unique Keys
Assign a stable :key using your item's primary identifier, not the loop index:
<draggable v-model:list="items" item-key="id">
<template #item="{ element }">
<div :key="element.id">{{ element.name }}</div>
</template>
</draggable>
This allows patchKeyedChildren in packages/runtime-core/src/renderer.ts to perform DOM moves instead of destructive remounts.
3. Adopt Modern v-model:list Syntax
Use the Vue 3 specific v-model:list binding rather than legacy v-model:
<!-- Correct for Vue 3 -->
<draggable v-model:list="items" />
<!-- Avoid: legacy syntax that may trigger compat layer issues -->
<draggable v-model="items" />
The compat layer in packages/runtime-core/src/compat/componentVModel.ts can introduce subtle prop naming confusion that breaks drag-and-drop updates.
4. Declare Emits in Parent Components
When using the Options API, explicitly declare the update event:
export default {
emits: ['update:modelValue'],
data() {
return {
items: [{ id: 1, name: 'Alpha' }]
}
}
}
Vue's runtime-core validates emitted events; undeclared events are ignored in development mode, causing silent failures.
5. Avoid Direct Index Mutations
Never mutate arrays using index assignments that bypass reactivity tracking:
// Wrong - Vue cannot detect this change
items.value[0] = { id: 99, name: 'New' }
// Correct - creates new array reference
items.value = [
{ id: 99, name: 'New' },
...items.value.slice(1)
]
Alternatively, use reactive array methods like push, splice, or sort that Vue's reactivity system wraps.
6. Handle Complex Objects with Clone Events
When working with nested data structures that require custom copy semantics, use the clone or move events rather than default mutations:
<draggable
v-model:list="items"
item-key="id"
:clone="cloneItem"
group="shared"
>
<template #item="{ element }">
<div>{{ element.name }}</div>
</template>
</draggable>
function cloneItem(item: Item): Item {
return { ...item, id: Date.now() } // Ensure unique ID for clone
}
7. Test Nested Draggable Components Carefully
When nesting draggable instances, ensure each level maintains unique keys scoped to that level:
<draggable v-model:list="groups" item-key="groupId">
<template #item="{ element: group }">
<div :key="group.groupId">
<h3>{{ group.name }}</h3>
<draggable v-model:list="group.items" item-key="itemId">
<template #item="{ element: item }">
<div :key="item.itemId">{{ item.name }}</div>
</template>
</draggable>
</div>
</template>
</draggable>
Conflicting keys across nesting levels cause Vue's patchKeyedChildren to misidentify nodes, leading to erratic drag behavior.
Implementation Examples
Composition API with TypeScript
<script setup lang="ts">
import { ref } from 'vue'
import draggable from 'vuedraggable'
interface Task {
id: number
title: string
status: 'todo' | 'done'
}
const tasks = ref<Task[]>([
{ id: 1, title: 'Review PR', status: 'todo' },
{ id: 2, title: 'Update docs', status: 'todo' },
{ id: 3, title: 'Deploy staging', status: 'done' },
])
const logUpdate = () => {
console.log('New order:', tasks.value.map(t => t.id))
}
</script>
<template>
<draggable
v-model:list="tasks"
item-key="id"
class="task-list"
@update="logUpdate"
>
<template #item="{ element }">
<div :key="element.id" class="task-item">
<span :class="element.status">{{ element.title }}</span>
</div>
</template>
</draggable>
</template>
<style scoped>
.task-list { display: flex; flex-direction: column; gap: 0.5rem; }
.task-item { padding: 1rem; background: #f5f5f5; border-radius: 4px; cursor: move; }
.todo { color: #d32f2f; }
.done { color: #388e3c; text-decoration: line-through; }
</style>
Options API with Explicit Event Declaration
export default {
components: { draggable },
emits: ['update:modelValue'], // Critical for Vue 3 event validation
data() {
return {
items: [
{ id: 1, name: 'Item A' },
{ id: 2, name: 'Item B' },
{ id: 3, name: 'Item C' },
]
}
},
methods: {
onDragEnd() {
console.log('Drag ended, new order:', this.items)
}
}
}
<template>
<draggable
v-model:list="items"
item-key="id"
@end="onDragEnd"
>
<template #item="{ element }">
<div :key="element.id" class="draggable-item">
{{ element.name }}
</div>
</template>
</draggable>
</template>
Summary
- Use reactive references: Wrap lists in
ref()orreactive()so Vue's reactivity system inpackages/reactivity/src/ref.tscan track changes and triggerv-modelupdates. - Provide stable keys: Always use unique item identifiers (like
id) for:keyanditem-keyprops to enablepatchKeyedChildreninpackages/runtime-core/src/renderer.tsto perform efficient DOM moves rather than destructive remounts. - Adopt modern syntax: Use
v-model:listinstead of legacyv-modelto avoid compatibility layer issues inpackages/runtime-core/src/compat/componentVModel.ts. - Declare emits explicitly: When using the Options API, declare
emits: ['update:modelValue']so Vue's runtime-core validates and forwards events correctly. - Avoid index mutations: Never use index assignments on reactive arrays; instead use array methods or create new array references to ensure
vModelDynamicinpackages/runtime-dom/src/directives/vModel.tsdetects changes.
Frequently Asked Questions
Why does my list not update after dragging items in vuedraggable Vue 3?
This occurs when the bound array lacks reactivity. Vue's vModelDynamic directive in packages/runtime-dom/src/directives/vModel.ts requires a reactive container to invoke the assigner function. If you bind a plain JavaScript array instead of a ref() or reactive() object, the update:modelValue event emits but the parent state never updates because no reactivity trigger exists.
Why do my input fields reset when I drag items in vuedraggable?
This happens when you use the loop index as the :key prop instead of a stable unique identifier. Vue's patchKeyedChildren algorithm in packages/runtime-core/src/renderer.ts uses keys to determine if a DOM node can be moved or must be recreated. When keys change during reordering (as indices do), Vue unmounts the old component and mounts a fresh one, destroying any internal state like form input values.
What is the difference between v-model and v-model:list in vuedraggable Vue 3?
The v-model:list syntax is the native Vue 3 approach for array bindings, while legacy v-model relies on the compatibility layer in packages/runtime-core/src/compat/componentVModel.ts. Using v-model:list explicitly tells Vue to treat the binding as a list update rather than a single value, preventing prop naming confusion that can cause the parent component to miss update:modelValue events during drag operations.
How do I prevent vuedraggable from mutating my original array reference?
Always assign a new array reference when modifying the list outside of vuedraggable's internal handlers. Vue's reactivity system in packages/reactivity/src/ref.ts tracks references, not deep mutations. If you need to manually update the list, use items.value = [...items.value, newItem] or array methods like push() and splice() that Vue wraps with reactivity triggers, rather than direct index assignments like items.value[0] = newItem which bypass tracking.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →